VM逆向
VM逆向
¶技术原理
虚拟机保护是通过开发者自定义的一套opcode,由虚拟机的dispatcher解释执行,从而起到代码混淆、增加逆向难度的技术

VM_start:是对虚拟机的初始化
VM_dispatcher:调度器,解释opcode,并选择相应的函数执行,一般为switch语句,根据地址码判断
VM_code:程序可执行代码形成的操作码
¶ida定义结构体
¶Dasctf——EasyVm
一开始有个花指令,是比较常见的永真跳转,先对call指令按u取消定义,将e8改为90,再重新弄成函数就可以了
先找到加密的函数

点进去看看

这里是base64变种,在最后加了一个异或操作,先把脚本写出来
1 |
|
接下来就是vm的部分

先看func函数的类型,是指针数组
这里需要结合动调看每条指令对应的操作

在这里下断点之后F7进入函数


这就是func数组存放的东西,因为是指针,所以要先按d转为dd才会显示

先把指令提取出来
1 | unsigned char ida_chars[] = |
F8单步调试
0xCA

0xCB

0xCC

0xCF

每执行完再进入func[2]都能知道当前位置,便于查看指令

0xc9


0xd1


这里本来赋值为0,1,2,为了保持字符相等的情况,把 this[5]全部赋值为1

0xd3


0xc2

0xd2

长度判断
0xd4

0xcc
接下来又回到0xcc,所以就能猜测是循环做了异或操作,然后判断

| func[1] | 指令集 |
|---|---|
| func[2] | 加密后的flag的字符 |
| func[3] | 0 |
| func[4] | 索引 |
| func[5] | 判断字符相等 |
| func[6] | 对比的flag |
| func[7] | 0 |
| func[8] | 加密后的字符串 |
| 0xca | 先将this[1]指令后的数据存放到this[3],然后往后跳转5,正好对应了下一条指令,一开始this[1]后一个数据为0,要先转为 dword |
| 0xcb | 先将this[1]指令后的数据存放到this[4],然后往后跳转5,这也是下一条指令 |
| 0xcc | 把this[2]先赋值为this[8]+this[4],这里this[4]是个整数,所以猜测this[4]是索引,然后继续执行下一指令 |
| 0xcf | 将this[3]和this[2]的值异或后存放在this[3],进入下一条指令 |
| 0xc9 | 先把this[1]下一个数据赋值给this[2],进入下一条指令,也就是0xee |
| 0xcf | 将this[3]和this[2]的值异或后存放在this[3],进入下一条指令 |
| 0xd1 | 根据this[4]的索引来进行字符比较,这里是调试过程,所以为了进行下一步,需要修改汇编指令 |
| 0xd3 | v1是指针,解引用是v1下一个位置,也就是0x1,整个就是0xee,这时候然后指向下三个位置,也就是c2指令 |
| 0xc2 | 索引this[4]+1 |
| 0xd2 | this[4]是索引,所以是判断是否结束,这里没结束,所以this[5]赋值为0 |
| 0xd4 | ec+2==ee |
整个过程就是这样

因为偶数次的异或等于不变,所以只需对奇数次的进行异或即可
最终脚本
1 |
|
¶hgame2022-week4-easyvm
第一次尝试写解释器,跟着别人的大致思路的
首先VM就是模仿汇编,用指令代替汇编,用数据段来模拟寄存器和数据段,所以我们关键是要找到数据段和操作数、opcode,以及一些数据存放内容的含义
先来给寄存器重新命名

根据main函数中switch可以找到类寄存器的位置,重命名

从switch的v3可以知道,前面给v3赋值的就是操作数
r0[0]开始是0,所以操作指令第一个存储的位置是r0[109],因为是int型,计算的时候×4,就可以找到地址

按g跳转,使用lazyida dump下来

接下来动调分析每条指令对应的汇编代码

先++,再赋值给它,就相当于push指令的入栈操作,然后还有一个r4后移一位
| push | data[r4++] |
|---|---|

先–,再赋值给r5,相当于pop操作
| pop | r5 |
|---|---|

又回到0x12,还是push,但是因为r4++,所以已经后移,我们根据计算也可以找到存储数据的地址并提取出数据

回到汇编,这里的指令push了-5进入到堆栈,也就是寄存器下面的位置
| push | data[r4++] |
|---|---|

| pop | r6 |
|---|---|

获取输入,并存储到a1中,这里的a1对应r3
| getchar | r3 |
|---|---|

熟悉的操作,把输入压入堆栈,记得栈顶往低处移动
| push | r3 |
|---|---|

这里的a1是r2
| add | r2,1 |
|---|---|

| cmp | r3,r5 |
|---|---|
根据前面的pop r5可以知道此时r5是0A,而我们的输入被存入了r3,也就是对我们的输入字符进行判断,那么r8应该就对应ZF标志位

验证了上面的想法,这里的a1是r6,也就是-5,指令跳转回去,相当于进行循环,输入完成后会有一个换行符,getchar会吸收,这里也就是结束我们的循环
| jnz | r0-5 |
|---|---|
直接在下一条指令下断点,F9运行

这里的a1是r2,用于记录输入长度,因为最后有一个回车符,所以要–
| r2– | |
|---|---|
根据前面就可以先对数组进行注释

r4没写上去,是内存中存储的一段数据
继续往下走

将内存段的下一个数据入栈,也就是0x20
| push | data[r4++] |
|---|---|

| pop | r5,这里懒得改了,可以点过去查看 |
|---|---|
接下来是0x12和0x9
| push | data[r4++] |
|---|---|
| pop | r6 |

把r2的值赋给r3
| mov | r3,r2 |
|---|---|

| push | r3 |
|---|---|

| cmp | r3,r5 |
|---|---|
但是这次比较的是字符串的长度,也就是字符串长度是32

| jnz | r0+r6 |
|---|---|
这里r6存放的是2f,正好对应结束,所以这段就相当于exit
下面是0x12和0x09
| push | data[r4++] |
|---|---|
| pop | r6 |
0x12和0x0A
这是0x0a的代码

| push | data[r4++] |
|---|---|
| pop | r2 |
0x13

r2此时为0,+9正好是堆栈的位置,也是存放我们输入数据的位置
| mov | r3,stack[count] |
|---|---|
从下面开始就是加密的部分了

| push | data[r4++] |
|---|---|
0x0B

| pop | r7 |
|---|---|
0x15

| add | r3,r3 |
|---|---|
0x03

| xor | r3,r7 |
|---|---|

把处理完的数据重新放入栈中
| mov | stack[count],r3 |
|---|---|

| add | r2,1 |
|---|---|

| mov | r3,r2 |
|---|---|
0xF

| cmp | r3,r5 |
|---|---|

| jnz | r0+r6 |
|---|---|
此时的r6是-10,也就是跳转回到前十条指令,这就是循环加密,现在就差最终的密文了,前面加密的部分直接运行过去
再次来到0x12

| push | data[r4++] |
|---|---|
0xA
| pop | r2 |
|---|---|

接下来是三个0x12
| push | data[r4++] |
|---|---|
| push | data[r4++] |
| push | data[r4++] |
到了0x08,此时我们的比较数据已经被压入栈中

| pop | r5 |
|---|---|
0x13,将处理完的值重新从堆栈中取出
| mov | r3,satck[count] |
|---|---|
0xF
| cmp | r3,r5 |
|---|---|
0x07

| pop | r3 |
|---|---|
0x04

| mov | stack[count],r3 |
|---|---|
0x09
| pop | r6 |
|---|---|
0x0D
r6是0x15,加上之后正好exit,和前面类似,压入栈是因为待会需要多次使用
| jnz | r0+r6 |
|---|---|
这里没有问题就会往下
0x09
| pop | r6 |
|---|---|
此时把-17给到r6
0x08
| pop | r5 |
|---|---|
0x05
| push | r5 |
|---|---|
0x06
| push | r6 |
|---|---|
0x04
| push | r3 |
|---|---|
0x01
| add | r2,1 |
|---|---|
0x00
| mov | r3,r2 |
|---|---|
0x0F//长度判断
| cmp | r3,r5 |
|---|---|
0xd,重新进入循环

¶最终的一些数据和分组和脚本
1 |
|