虚拟机保护逆向入门
虚拟机保护逆向入门
虚拟机保护逆向入门
本文原创作者:b1ngo
远程也是可以通过的:
RCTF 2018 Simple vm
这个题是今年RCTF2018的一个入门题吧,感谢这位师傅的出题和分享,学
到了很多。题如其名,这是一个虚拟机程序,题目本身给了一个ELF64位的可执行程序,给了一个bin文件,这是opcode文件。
vm_run在sub_400896函数,进去之后发现没有代码加密或者混淆,直接可
以反编译和生成CFG图:
从结构可以看出虚拟机一般的switch/case结构,所以loc_4008A2可以理解为vm_dispatcher,各个case为vm_handler。赛后做这个题目,我们就用最傻的方法,一步步调试这个vm,结合opcode,理解各个handler。在IDA的反编译结果中,出现了两个比较关键的全局变量:dword_6010A4和c。我们可以将之认为是用于临时存放opcode指令和代码的变量。
给出p.bin文件的解析:
“””
01 # 读取addr赋值给指针
30000000 # 带读取的addr
15 # 读取字符串所在addr赋值给vm.c
00010000 # Inputflag字符串长度地址,后面就是字符串
#dword_6010A4是字符串的ASCII,c是字符串所在的地址
# 这部分完成了打印字符串
0E # c++
12 # dword_6010A4 = p[c]
0B # putchar(dword_6010A4);
0C # 循环
00010000 # 循环次数所在地址(也就是字符串的长度)
35000000 # 每次循环开始的指针
# 这部分完成了循环
15 c = READ_INT();
10010000 # 要读入的字符串长度地址
0E # c
0A # dword_6010A4 = getchar();
66 # nop
16 # p[c] = dword_6010A4存放到0x111位置处
0C # 循环
10010000 # 循环的次数
47000000 # 每次循环开始的指针
03 #取0x140偏移处的内容0x20赋值给dword_6010A4
40010000
10 # c = dword_6010A4;
11 # dword_6010A4 += val;
F1000000 # val
13 # dword_6010A4 =p[dword_6010A4]; 到这个位置,就将输入的ASCII赋值到dword_6010A4中了
04 # p[dst] = dword_6010A4;
43010000 # dst 0x143
#开始对输入做变化,0x141~0x144可以理解为变化临时存放为位置
08 # dword_6010A4 =~(dword_6010A4
18 # OP_JNE
60010000
0C # 循环
46010000
B6000000
“””
这就是这个程序的执行流,调试过程十分麻烦,vm实在是太磨人了。理解
上述过程,其实就是对输入做异或,并与p.bin文件部分内容进行比较的过程,然后写出逆向算法,解出flag:
“””
# -*- coding:utf-8 -*-
a ="1018431415474017101D4B121F49481853540157515305565A08585F0A0C5809"
a = a.decode("hex")
a = [ord(i) for i in a]
print a
f = ""
for i in range(len(a)):
f += chr(a[i]^(i+32))
print f
“””
其实这种方法算是最麻烦的,该方法是从汇编层面调试二进制程序,我还看
到更为简单的一种方法是通过IDA反编译结果(https://expend20.github.io/2018/05/24/RCTF-simple-vm.html),稍微改改之后,重新编译,然后就可以进行源码级调试。这里的修改基本不用动IDA的反编译结果,就是需要加些IDA的宏定义,在每个case后面都使用printf输出当前指针位置,就可以自动的分析出程序的执行流,十分方便。
从赛后复盘的角度是可以慢慢调试虚拟机的,搞清每个字节码的意义,然后
完整的理解虚拟机解释执行过程。不过随着虚拟机的复杂,这种方法将会非常费时,应该学习一些自动化的处理方法。
RCTF 2018 magic
又是今年RCTF的一个Re题,题目也出很好,我感觉还挺难的,赛后看了一
些其他师傅的WP,感觉收获很多。这里前面爆破time的第一次检查不再赘述(吾爱有个师傅的处理方法很不错https://www.52pojie.cn/thread-742361-1-1.html),只说说第二个对输入的检查。当第一次对时间检查通过之后,程序执行流来到了函数sub_4023B1位置:
scanf之后,对输入进行加密,加密后的数据作为虚拟机输入。这个地方是利用setjmp/longjmp实现的一个虚拟机,虚拟机opcode位于
0x405340处。地址409040是虚拟机用于数据处理的区域,我们可以将之定义为dword型数组。该虚拟机值得主要的是这个地方,调试的过程中需要注意一下,这里是通过异常处理完成了一个Handler:
异常处理在这个signal回调函数中:
对于虚拟机的逆向,往往都是通过首先逆出来各个Handler,然后结合opcode
搞清楚程序算法,在目前这个阶段,这个算法一般不会很难。如果算法很难,opcode很长,这种情况题目就算题出的很难的。接下来给出opcode的解释:
“””
con[1] = cmp
con[2] = input
AB 03 00 mov con[3],0
AB 04 1A mov con[4],0x1A
AB 00 66 mov con[0],0x66
AA 05 02 mov con[5],con[2] #input
A9 53 con[5] += con[3]
A0 05 mov con[5],*(byte)con[5]
AB 06 CC mov con[6],0xCC
A9 56 con[5] += con[6]
AB 06 FF mov con[6],0xFF
AC 56 con[5] &= con[6]
AE 50 con[5] ^= con[0]
AD 00 not con[0]
AA 06 05 mov con[6],con[5]
AA 05 01 mov con[5],con[1]
A9 53 con[5] += con[3]
A0 05 mov con[5],*(byte)con[5]
AF 56 00 if con[5]==con[6]
算法总结:
key = 0x66
((input[i] + 0xCC ) &0xFF) ^key == cmp[i]
~key
“””
直接爆破就可以得到需要输入值:
“””
comp ="89C1EC50973A5759E4E6E442CBD90822AE9D7C07808F1B4504E8"
comp = [ord(i) for i incomp.decode("hex")]
print comp
'''
key = 0x66
((input[i] + 0xCC ) &0xFF) ^key == cmp[i]
~key
'''
key = 0x66
f = []
for i in range(len(comp)):
for j in range(256):
if ((j + 0xCC) &0xFF ) ^ key == comp[i]:
f.append(j)
key = (~key) & 0xFF
print f
“””
值得注意的是,这个只是vm_run的输入值,需要利用程序中的解密算法解
密后得到输入值,这个可以在vm_run成功后,在地址0x040249D下断点,在内存将要解密的值进行替换,最后解密后如下所示:
这个只是其中一部分的flag,将之输入后会得到一个图案,图案为rctf{h:
拼接之后即为flag。
(注:本文属于合天原创投稿奖励,未经允许,禁止转载!)