分析程序
IDA 打开,main 函数中有很多 switch-case 语句,可以判断可能是 VM 程序
VM 程序首先需要先找到 opcode,(执行指令的顺序)


先查看 switch 的参数 opcode 来源于 EIP


EIP 就一个值为 0,也就是 EIP[0] = 0
opcode 取值为 EIP 的 0+109(0x6D)
计算一下位置为 49F08D

查看 49F08D 什么值都没有,但是注意到数据全是 db(byte)字节,
一般 VM 题目 opcode 都是 dd(Dword)双字

所以将他按照 dword 格式跳转一下(db x 4 = dd, 109 x 4)

查看 49F1D4,看到刚好是一些数据开始的位置

这串数据就是 VM 程序需要的 opcode 了
将这些数据导出,等下写脚本要用,一直到 FFFFFFFF 就是结束了

导出的时候将 Byte 改成 Dword

识别 VM 指令
然后我们开始分析 VM case 的每个指令
case0 - case3

赋值,对应 mov 指令

自增,对应 inc 指令

自减,对饮 dec 指令

异或,对应 xor 指令
case4 - case6



在 case 4 - case 6 全部用到了这个 EIP[ESP + 9]
EIP 在前面取 opcede 的时候用到了,地址 0049F020
这里又多了一个 ESP + 9
查看 ESP 值为 FFFFFFFF

这里用的是补码,FFFFFFFF 在有符号数值里为 -1
所以就是 - 1 + 9,然后 byte 还要 x 4

查看 49F044,这里在内存中分配了一些空字节

在这几个 VM 指令中都是指针自增后,向里面填寄存器的数据
所以这里应该是模拟栈,往栈里填数据就是
入栈:对应 push 指令
case8 - case11


这里和上面一样也是关于 ESP 的栈操作,多了一个判断栈指针是否合法(>= 0)
栈指针自减,然后将栈中数据给寄存器
出栈:对应 pop 指令
case12 - case15

如果 ZF 为 0,EIP(opcode 指针) 加上 EDX 的值(指针偏移)
这里 ZF 的值由 case15 的 cmp 指令赋值的,结合 case15 就能明白
相等跳转:对应 jz [ 指针+偏移 ]

同上
不相等跳转:对应 jnz [ 指针+偏移 ]

绝对跳转:对应 jmp [ 指针+偏移 ]

这里对 ZF 进行赋值,如果 EAX 等于 ECX,ZF=0,如果小于,ZF=-1,如果大于 ZF=1
比较:对应 cmp 指令
case16-case17

调用 getchar 获取输入

调用 putchar 输出 EAX
case18


idx 初始值为 0
调用了 EIP[idx + 209],(byte x 4)

跳转到 49F364,看到都是一些数据

VM 指令中每次将一个数据压入栈中
取数据入栈:用 push data[idx++] 表示
case19 - case20

将栈中指定数据取出到 EAX
mov EAX, [ 栈指针 + 偏移 ]

将 EAX 数据存到指定栈中
mov [ 栈指针 + 偏移 ], EAX
case21

这里可以理解为 << 1 或是 + EAX
左移:对应 shl 指令
最后 EIP 自增,准备读取下一个 opcode
还原伪汇编
分析的时候 dump opcode,然后搓脚本
opcode = [0x12, 0x8, 0x12, 0x9, 0x10, 0x4, 0x1, 0xf, 0xd, 0x2, 0x12, 0x8, 0x12, 0x9, 0x0, 0x4, 0xf, 0xd, 0x12, 0x9, 0x12, 0xa, 0x13, 0x12, 0xb, 0x15, 0x3, 0x14, 0x1, 0x0, 0xf, 0xd, 0x12, 0xa, 0x12, 0x12, 0x12, 0x8, 0x13, 0xf, 0x7, 0x4, 0x9, 0xd, 0x9, 0x8, 0x5, 0x6, 0x4, 0x1, 0x0, 0xf, 0xd, 0x12, 0x9, 0x12, 0x8, 0x12, 0xa, 0x12, 0x7, 0xf, 0xc, 0x11, 0xe, 0xffffffff]
data = [0xa, 0xfffffffb, 0x20, 0x2f, 0xfffffff6, 0x0,
0x5e, 0x46, 0x61, 0x43, 0xe, 0x53, 0x49, 0x1f, 0x51, 0x5e, 0x36, 0x37, 0x29, 0x41, 0x63, 0x3b, 0x64, 0x3b, 0x15, 0x18, 0x5b, 0x3e, 0x22, 0x50, 0x46, 0x5e, 0x35, 0x4e, 0x43, 0x23, 0x60, 0x3b,
0x0, 0xffffffef, 0x15,
0x8e, 0x88, 0xa3, 0x99, 0xc4, 0xa5, 0xc3, 0xdd, 0x19, 0xec, 0x6c, 0x9b, 0xf3, 0x1b, 0x8b, 0x5b, 0x3e, 0x9b, 0xf1, 0x86, 0xf3, 0xf4, 0xa4, 0xf8, 0xf8, 0x98, 0xab, 0x86, 0x89, 0x61, 0x22, 0xc1,
0x2, 0x0, 0xfffffffa, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0]
ip = 0
while ip < len(opcode):
match opcode[ip]:
case 0:
print(ip,"mov eax, ebx")
case 1:
print(ip,"inc ebx")
case 2:
print(ip,"dec ebx")
case 3:
print(ip,"xor eax, esi")
case 4:
print(ip,"push eax")
case 5:
print(ip,"push ecx")
case 6:
print(ip,"push edx")
case 7:
print(ip,"pop eax")
case 8:
print(ip,"pop ecx")
case 9:
print(ip,"pop edx")
case 10:
print(ip,"pop ebx")
case 11:
print(ip,"pop esi")
case 12:
print(ip,"jz [ip + edx]")
case 13:
print(ip,"jnz [ip + edx]")
case 14:
print(ip,"jmp [ip + ebx]")
case 15:
print(ip,"cmp eax, ecx")
case 16:
print(ip,"getchar(eax)")
case 17:
print(ip,"putchar(eax)")
case 18:
print(ip,"push data[idx++]")
case 19:
print(ip,"mov eax, [esp + ebx]")
case 20:
print(ip,"mov [esp + ebx], eax")
case 21:
print(ip,"shl eax, 1")
ip += 1
运行得到下面(伪)汇编代码
0 push data[idx++] ;0xa
1 pop ecx
2 push data[idx++] ;0xfffffffb(-5)
3 pop edx
4 getchar(eax)
5 push eax
6 inc ebx
7 cmp eax, ecx
8 jnz [ip + edx]
9 dec ebx
10 push data[idx++] ;0x20
11 pop ecx
12 push data[idx++] ;0x2f
13 pop edx
14 mov eax, ebx
15 push eax
16 cmp eax, ecx
17 jnz [ip + edx]
18 push data[idx++] ;0xfffffff6(-10)
19 pop edx
20 push data[idx++] ;0x0
21 pop ebx
22 mov eax, [esp + ebx]
23 push data[idx++] ;共取32个异或值
24 pop esi
25 shl eax, 1
26 xor eax, esi
27 mov [esp + ebx], eax
28 inc ebx
29 mov eax, ebx
30 cmp eax, ecx
31 jnz [ip + edx]
32 push data[idx++] ;0x0
33 pop ebx
34 push data[idx++] ;0xffffffef(-17)
35 push data[idx++] ;0x15
36 push data[idx++] ;共取32个密文
37 pop ecx
38 mov eax, [esp + ebx]
39 cmp eax, ecx
40 pop eax
41 push eax
42 pop edx
43 jnz [ip + edx]
44 pop edx
45 pop ecx
46 push ecx
47 push edx
48 push eax
49 inc ebx
50 mov eax, ebx
51 cmp eax, ecx
52 jnz [ip + edx]
53 push data[idx++] ;0x2
54 pop edx
55 push data[idx++] ;0x0
56 pop ecx
57 push data[idx++] ;0xfffffffa(-6)
58 pop ebx
59 push data[idx++] ;'success'
60 pop eax
61 cmp eax, ecx
62 jz [ip + edx]
63 putchar(eax)
64 jmp [ip + ebx]
进行正常的汇编代码分析
分析汇编
0-8
获取输入,直到获取到"\n"结束
0 push data[idx++] ;0xa ;入栈0xa("\n")
1 pop ecx ;出栈0xa到ecx="\n"
2 push data[idx++] ;0xfffffffb(-5) ;入栈-5
3 pop edx ;入栈-5到edx=-5
4 getchar(eax) ;获取字符输入到eax
5 push eax ;入栈eax=输入
6 inc ebx ;ebx自增, ebx++ (ebx作为计数器)
7 cmp eax, ecx ;比较eax和ecx, 输入比较"\n"
8 jnz [ip + edx] ;不相等跳转ip+(-5)=3, 执行下一条4
9-17
判断输入长度是否为 32,否则结束程序
9 dec ebx ;ebx自减, ebx--
10 push data[idx++] ;0x20 ;入栈0x20(32)
11 pop ecx ;出栈ecx=32
12 push data[idx++] ;0x2f ;入栈0x2f(47)
13 pop edx ;出栈edx=47
14 mov eax, ebx ;eax=ebx
15 push eax ;入栈eax
16 cmp eax, ecx ;比较eax和ecx, 输入长度比较32
17 jnz [ip + edx] ;不相等跳转ip+47=64, 执行下一条65
18-31
进行主要运算 (flag << 1) ^ data,循环 32 次
18 push data[idx++] ;0xfffffff6(-10) ;入栈-10
19 pop edx ;出栈edx=-10
20 push data[idx++] ;0x0 ;入栈0
21 pop ebx ;出栈0到ebx=0
22 mov eax, [esp + ebx] ;eax=栈指针+0, 循环获取输入
23 push data[idx++] ;共取32个异或值 ;获取data中的数据段(循环)
24 pop esi ;入栈数据到esi=数据
25 shl eax, 1 ;将输入左移1位
26 xor eax, esi ;异或数据
27 mov [esp + ebx], eax ;栈指针+0=eax, 将运算后的输入返回栈中
28 inc ebx ;ebx自增 ebx++
29 mov eax, ebx ;eax=ebx
30 cmp eax, ecx ;比较eax和ecx, 循环次数对比32
31 jnz [ip + edx] ;不相等跳转ip+(-10)= 21, 执行下一条22
32-43
验证加密后的输入和 data 中的数据是否相等,否则结束程序
32 push data[idx++] ;0x0 ;入栈0
33 pop ebx ;出栈ebx=0
34 push data[idx++] ;0xffffffef(-17) ;入栈-17
35 push data[idx++] ;0x15 ;入栈0x15(21)
36 push data[idx++] ;共取32个密文 ;入栈data数据段(循环)
37 pop ecx ;出栈数据
38 mov eax, [esp + ebx] ;eax=栈指针+0位置的数据(输入)
39 cmp eax, ecx ;比较eax和ecx, 输入值和后取的数据进行比较(循环)
40 pop eax
41 push eax
42 pop edx ;出栈edx=21
43 jnz [ip + edx] ;不相等跳转ip+21=64, 执行下一条65
44-52
循环验证 32 次
44 pop edx ;出栈edx=-17
45 pop ecx
46 push ecx
47 push edx ;入栈-17
48 push eax ;入栈数据
49 inc ebx ;ebx自增, ebx++
50 mov eax, ebx ;eax=ebx
51 cmp eax, ecx ;比较32和计数器
52 jnz [ip + edx] ;跳转到ip+(-17)=36, 执行下一条37
53-64
输出 'success'
53 push data[idx++] ;0x2 ;入栈2
54 pop edx ;出栈edx=2
55 push data[idx++] ;0x0 ;入栈0
56 pop ecx ;出栈ecx=0
57 push data[idx++] ;0xfffffffa(-6) ;入栈-6
58 pop ebx ;出栈ebx=-6
59 push data[idx++] ;'success' ;入栈's'
60 pop eax ;出栈eax='s'
61 cmp eax, ecx ;比较eax和0
62 jz [ip + edx] ;相等跳转到ip+2=64,执行下一条65
63 putchar(eax) ;输出eax中的字符
64 jmp [ip + ebx] ;绝对跳转到ip+(-6)=59, 执行下一条60
最后总结程序的逻辑是将输入的字符进行右移和异或指定字符后,与密文进行对比,成功输出 success
还原伪 C 代码
转成 C 语言代码
#include <stdio.h>
int main() {
unsigned char key[32] = {0x5e, 0x46, 0x61, 0x43, 0xe, 0x53, 0x49, 0x1f, 0x51, 0x5e, 0x36, 0x37, 0x29, 0x41, 0x63, 0x3b, 0x64, 0x3b, 0x15, 0x18, 0x5b, 0x3e, 0x22, 0x50, 0x46, 0x5e, 0x35, 0x4e, 0x43, 0x23, 0x60, 0x3b};
unsigned char str1[32] = {0x8e, 0x88, 0xa3, 0x99, 0xc4, 0xa5, 0xc3, 0xdd, 0x19, 0xec, 0x6c, 0x9b, 0xf3, 0x1b, 0x8b, 0x5b, 0x3e, 0x9b, 0xf1, 0x86, 0xf3, 0xf4, 0xa4, 0xf8, 0xf8, 0x98, 0xab, 0x86, 0x89, 0x61, 0x22, 0xc1};
unsigned char input[33];
int i = 0;
while((input[i] = getchar()) != '\n')
i++;
if(i != 32)
return 0;
for(i = 0; i < 32; i++)
input[i] = (input[i] << 1) ^ key[i];
for(i = 0; i < 32; i++)
if(input[i] != str1[i])
return 0;
printf("success");
return 0;
}
主要逻辑为input = (input << 1) ^ key
EXP
enc = [0x8e, 0x88, 0xa3, 0x99, 0xc4, 0xa5, 0xc3, 0xdd, 0x19, 0xec, 0x6c, 0x9b, 0xf3, 0x1b, 0x8b, 0x5b, 0x3e, 0x9b, 0xf1, 0x86, 0xf3, 0xf4, 0xa4, 0xf8, 0xf8, 0x98, 0xab, 0x86, 0x89, 0x61, 0x22, 0xc1]
key = [0x5e, 0x46, 0x61, 0x43, 0xe, 0x53, 0x49, 0x1f, 0x51, 0x5e, 0x36, 0x37, 0x29, 0x41, 0x63, 0x3b, 0x64, 0x3b, 0x15, 0x18, 0x5b, 0x3e, 0x22, 0x50, 0x46, 0x5e, 0x35, 0x4e, 0x43, 0x23, 0x60, 0x3b]
for i in range(32):
print(chr((enc[i] ^ key[i]) >> 1),end="")
hgame{Ea$Y-Vm-t0-PrOTeCT_cOde!!}
总结
这是一个 VM 虚拟机程序,和普通 VM 程序不同的地方在于,他使用了指针来指向 opcode、数据、虚拟栈,在分析过程中需要了解软件运行原理,对汇编足够了解,模拟了一个较为完整的软件运行环境,对 VM 虚拟机的进阶学习比较有价值
1158

被折叠的 条评论
为什么被折叠?



