vm题算是逆向中比较难的一种题型了,就在这里详细的记录一下。
原理
程序运行时通过解释操作码(opcode)选择对应的函数(handle)执行。
vm_init
进行初始化工作。在这个函数里,规定了有几个寄存器,以及有几种不同的操作。
这样看不明显,创建结构体修复一下,结构体长这个样子
1 | typedef struct |
1 | typedef struct |
修复完的初始化函数
vm_start
1 | void vm_start(vm_cpu *cpu) |
如果rip指向的操作码为F4就返回。
vm_dispatcher
调度器,任务是根据opcode选择函数执行
1 | void vm_dispatcher(vm_cpu *cpu) |
循环,找到opcode对应的函数。
参考链接:
实战1
【GWCTF2019babyvm】
分析函数
知道了原理之后,就可以分析题目加深理解了。
主要分析vm_ini函数,搞清楚opcode对应的操作。
0xF1
可以把这个当作mov指令
1 | v2 = (int *)(a1->_rip + 2); |
v2指向当前指令往后偏移两个字节的位置。
1 | a1->_rip += 6LL; |
说明这条指令的大小为6字节。
可以看出这条指令可以将复制一些值到寄存器,也可以将寄存器的值复制过去。
拿第一组数据来看,a1->rip+1是0xE1,那么将input[*v2]的值存入寄存器R0,也就是R0=input[0]
1 | 0xF1, 0xE1, 0x00, 0x00, 0x00, 0x00, |
0xF2
R0=R0^R1,指令长度1字节。
0xF5
读取输入并判断长度。
0xF4
空操作,nop,作用是将rip+1。
0xF7
R0*=R3,指令长度为1。
0xF8
交换R0和R1的数值。
0xF6
R0=R2+2*R1+3*R0
翻译
1 |
|
得到了两段程序,第一个是简单的异或
1 | please input: R1=18 |
1 | for(int i=0;i<21;i++) |
第二段
1 | please input: |
第二段的比较数据就在第一个比较数据的附近,在得到假flag之后,查看该处的数据
发现上面有一个可疑数据,查看它的交叉引用,可以跟进一个比较函数
但是比较奇怪的是,这个函数没有被调用过,之前做过actf的一道题,函数通过栈溢出覆盖了返回值,从而被调用,但是这一题就算输入了真正的flag,也不会调用这一处函数,所以感觉有点……,虽然有提示,虽然opcode有两个0xf5调用输入,但还是有些生硬了。
1 |
|
实战2
hgame2023 vm
分析结构体
直接去分析sub_1400010B0,不难看出这是vm_start函数
byte_140005360自然就是opcode,sub_140001940是调度器。这一题创建结构体比上面那道困难一些,因为我没找到vm_init函数,只能根据dispatcher调度的函数来判断。从*(unsigned int *)(a1 + 24)也不难看出,a1+24指向的是eip,大小为四字节。
从这里看以看出handle是八字节的,从第一个函数分析
不难猜测出这是个mov之类的指令,从v2那里可以看到opcode[a1[6]+1]应该就是sp+1 ,也就是a[6]是上面分析的eip,所以可以判断通用寄存器有6个,大小是dword。上面的case0~7,说明有八个函数。接着往下看,分析第二个函数
这个很容易想到push指令,存数据之前指针先增加,再结合第三个函数
取数据之后,指针自减,再加上和上面猜的的push是成对出现的,那么可以推测a[7]是栈指针esp。再往下只有一个比较陌生的东西:
在vm_cpu结构体的第32字节处,有一个一字节大小的变量,相等赋值0,不等赋值1,有点像标志位zf,接着根据下面的函数分析
这个函数会判断我们刚才提到的那个一字节的变量从而选择不同的指令,所以可以推测其是一个一字节大小的标志寄存器。
综上:
vm_cpu有6个通用寄存器,一个eip,一个esp,还有一个zf
创建结构体
1 | typedef struct |
1 | typedef struct |
分析函数
逐个分析函数
fun0
都在注释上了
fun1
fun2
fun3
这个函数就是实现的+、-、*、^、<<等操作
fun4
更改标志寄存器
fun5
更改eip的值,而且没有任何条件,可以推测出这是条jmp指令。
fun6
标志寄存器为0则发生跳转,所以是条jnz指令。
fun7
和fun6对应,是jz指令
翻译
1 |
|
然后分析打印出来的伪指令,不要被一千多行的指令吓得头皮发麻,仔细看下来发现大量的重复,也就是循环
1 | //part1 |
可以发现,以jmp to fun0来分割,可以分割成40个差别不大的块,拿第一块来分析。
1 | int R[6]={0}; |
转为c语言就是
1 | R[2]=(input[0]+input[50])^input[100]; |
可以看到,input经过一番操作被存储在了R1,随后push R1将数据存入了栈中,通过搜索pop发现,只有在最后一个块出现了pop
定位到这条指令,他比较了R0和R1,而此时的R1存储的是input[39]经过加密的值,而R0存放的input[150]那里就是比较数据了
可以看到从input[150]那里,有连续的四十个有意义的数据,不难判断这就是比较数据了。那么为什么只进行了一次比较呢?因为这一次比较和cpdata不相等,input[0]~input[39]是随便输入的,如果我们把这个zf给置为0,那么接下来就又有一个pop R1,对比的数据是input[150+1]
解密脚本
1 |
|