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 |
|