向 VM 的步步靠近--部分 vm 赛题复现

[GWCTF 2019]babyvm

修结构体

ida 中查看 main 函数,点进 sub_CD1 里

前4个字段:写入4个32位整数(DWORD),偏移分别为0, 4, 8, 12,这是在设置寄存器

1
2
3
4
uint32_t eax;    // 偏移 0
uint32_t ebx; // 偏移 4 (初始值为18)
uint32_t ecx; // 偏移 8
uint32_t edx; // 偏移 12

后续字段:从偏移16开始,每16字节写入一个函数指针(QWORD)和一个操作码(BYTE),共8次

分析第一个条目:函数指针写入偏移16(占8字节),操作码写入偏移24(占1字节),从偏移16到24的总跨度:8字节(函数指针) + 8字节(操作码及填充) = 16字节

注:操作码仅占1字节,但为了对齐,编译器填充了7字节

后面的条目也是一样

处理程序条目结构体
函数指针(64位):占用8字节(QWORD)
操作码(8位):占用1字节(BYTE)。
填充(7字节):为保证对齐到16字节边界,需填充7字节
由此可以定义

1
2
3
4
5
struct HandlerEntry {
void (*handler)(void *); // 8字节函数指针
int8_t opcode; // 1字节操作码
uint8_t padding[7]; // 7字节填充
};

一共有八个处理条目,所以可以定义结构体 VMcpu

1
2
3
4
5
6
7
struct VMcpu {
uint32_t eax; // 偏移 0
uint32_t ebx; // 偏移 4
uint32_t ecx; // 偏移 8
uint32_t edx; // 偏移 12
HandlerEntry handlers[8];// 偏移16,共8个条目
};

在 ida 的 Local Types 里面右键 Add type,定义这两个结构体 HandlerEntryVMcpuY键修改sub_CD1 的参数 a1 的类型,改成 VMcpu*,可以看到修改完之后会好看很多

提取操作码


&unk_202060 处存的是操作码,需要提取出来

分析函数对应的功能

接下看每个操作码对应的函数,把每个函数传入的参数 a1 的类型也修成 VMcpu*,然后分析其功能
sub_B5F -> mov 寄存器赋值操作,对于不同的操作码进行不同的 mov

sub_A64 -> xor 明显的异或

sub_AC5 -> cmp strlen 可以看出这是在判断字符串长度

sub_956 -> nop 啥也没做,那就是 nop 了

sub_A08 -> mul 乘法操作

sub_8F0 -> swap 交换寄存器的值

sub_99C 这个里面是一个稍微复杂点的运算

写脚本获得汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
opcode=[

0xF5, 0xF1, 0xE1, 0x00, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4,
0x20, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x01, 0x00, 0x00, 0x00,
0xF2, 0xF1, 0xE4, 0x21, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x02,
0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x22, 0x00, 0x00, 0x00,
0xF1, 0xE1, 0x03, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x23,
0x00, 0x00, 0x00, 0xF1, 0xE1, 0x04, 0x00, 0x00, 0x00, 0xF2,
0xF1, 0xE4, 0x24, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x05, 0x00,
0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x25, 0x00, 0x00, 0x00, 0xF1,
0xE1, 0x06, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x26, 0x00,
0x00, 0x00, 0xF1, 0xE1, 0x07, 0x00, 0x00, 0x00, 0xF2, 0xF1,
0xE4, 0x27, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x08, 0x00, 0x00,
0x00, 0xF2, 0xF1, 0xE4, 0x28, 0x00, 0x00, 0x00, 0xF1, 0xE1,
0x09, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x29, 0x00, 0x00,
0x00, 0xF1, 0xE1, 0x0A, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4,
0x2A, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0B, 0x00, 0x00, 0x00,
0xF2, 0xF1, 0xE4, 0x2B, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0C,
0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2C, 0x00, 0x00, 0x00,
0xF1, 0xE1, 0x0D, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2D,
0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0E, 0x00, 0x00, 0x00, 0xF2,
0xF1, 0xE4, 0x2E, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0F, 0x00,
0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x2F, 0x00, 0x00, 0x00, 0xF1,
0xE1, 0x10, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x30, 0x00,
0x00, 0x00, 0xF1, 0xE1, 0x11, 0x00, 0x00, 0x00, 0xF2, 0xF1,
0xE4, 0x31, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x12, 0x00, 0x00,
0x00, 0xF2, 0xF1, 0xE4, 0x32, 0x00, 0x00, 0x00, 0xF1, 0xE1,
0x13, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x33, 0x00, 0x00,
0x00, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF5, 0xF1,
0xE1, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x01, 0x00, 0x00,
0x00, 0xF2, 0xF1, 0xE4, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xE1,
0x01, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x02, 0x00, 0x00, 0x00,
0xF2, 0xF1, 0xE4, 0x01, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x02,
0x00, 0x00, 0x00, 0xF1, 0xE2, 0x03, 0x00, 0x00, 0x00, 0xF2,
0xF1, 0xE4, 0x02, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x03, 0x00,
0x00, 0x00, 0xF1, 0xE2, 0x04, 0x00, 0x00, 0x00, 0xF2, 0xF1,
0xE4, 0x03, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x04, 0x00, 0x00,
0x00, 0xF1, 0xE2, 0x05, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4,
0x04, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x05, 0x00, 0x00, 0x00,
0xF1, 0xE2, 0x06, 0x00, 0x00, 0x00, 0xF2, 0xF1, 0xE4, 0x05,
0x00, 0x00, 0x00, 0xF1, 0xE1, 0x06, 0x00, 0x00, 0x00, 0xF1,
0xE2, 0x07, 0x00, 0x00, 0x00, 0xF1, 0xE3, 0x08, 0x00, 0x00,
0x00, 0xF1, 0xE5, 0x0C, 0x00, 0x00, 0x00, 0xF6, 0xF7, 0xF1,
0xE4, 0x06, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x07, 0x00, 0x00,
0x00, 0xF1, 0xE2, 0x08, 0x00, 0x00, 0x00, 0xF1, 0xE3, 0x09,
0x00, 0x00, 0x00, 0xF1, 0xE5, 0x0C, 0x00, 0x00, 0x00, 0xF6,
0xF7, 0xF1, 0xE4, 0x07, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x08,
0x00, 0x00, 0x00, 0xF1, 0xE2, 0x09, 0x00, 0x00, 0x00, 0xF1,
0xE3, 0x0A, 0x00, 0x00, 0x00, 0xF1, 0xE5, 0x0C, 0x00, 0x00,
0x00, 0xF6, 0xF7, 0xF1, 0xE4, 0x08, 0x00, 0x00, 0x00, 0xF1,
0xE1, 0x0D, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x13, 0x00, 0x00,
0x00, 0xF8, 0xF1, 0xE4, 0x0D, 0x00, 0x00, 0x00, 0xF1, 0xE7,
0x13, 0x00, 0x00, 0x00, 0xF1, 0xE1, 0x0E, 0x00, 0x00, 0x00,
0xF1, 0xE2, 0x12, 0x00, 0x00, 0x00, 0xF8, 0xF1, 0xE4, 0x0E,
0x00, 0x00, 0x00, 0xF1, 0xE7, 0x12, 0x00, 0x00, 0x00, 0xF1,
0xE1, 0x0F, 0x00, 0x00, 0x00, 0xF1, 0xE2, 0x11, 0x00, 0x00,
0x00, 0xF8, 0xF1, 0xE4, 0x0F, 0x00, 0x00, 0x00, 0xF1, 0xE7,
0x11, 0x00, 0x00, 0x00, 0xF4
]
i=0
for i in range(len(opcode)):
if(opcode[i]==0xF1):
print("mov",end=' ')
if(opcode[i+1]==0xE1):
print("eax flag["+str(opcode[i+2])+"]")
elif(opcode[i+1]==0xE2):
print("ebx flag["+str(opcode[i+2])+"]")
elif(opcode[i+1]==0xE3):
print("ecx flag["+str(opcode[i+2])+"]")
elif(opcode[i+1]==0xE4):
print("flag["+str(opcode[i+2])+"] eax")
elif(opcode[i+1]==0xE5):
print("edx flag["+str(opcode[i+2])+"]")
elif(opcode[i+1]==0xE7):
print("flag["+str(opcode[i+2])+"] ebx")
i+=6
elif(opcode[i]==0xF2):
print("xor eax ebx")
i+=1
elif(opcode[i]==0xF5):
print("cmp strlen")
i+=1
elif(opcode[i]==0xF4):
print("nop")
i+=1
elif(opcode[i]==0xF7):
print("mul eax edx")
i+=1
elif(opcode[i]==0xF8):
print("swap eax ebx")
i+=1
elif(opcode[i]==0xF6):
print("eax = ecx+2*ebx+3*eax")
i+=1
else:
i+=1

输出得到伪代码,前面部分是干扰的,后面那一段才是真正的加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
cmp strlen
mov eax flag[0]
xor eax ebx
mov flag[32] eax
mov eax flag[1]
xor eax ebx
mov flag[33] eax
mov eax flag[2]
xor eax ebx
mov flag[34] eax
mov eax flag[3]
xor eax ebx
mov flag[35] eax
mov eax flag[4]
xor eax ebx
mov flag[36] eax
mov eax flag[5]
xor eax ebx
mov flag[37] eax
mov eax flag[6]
xor eax ebx
mov flag[38] eax
mov eax flag[7]
xor eax ebx
mov flag[39] eax
mov eax flag[8]
xor eax ebx
mov flag[40] eax
mov eax flag[9]
xor eax ebx
mov flag[41] eax
mov eax flag[10]
xor eax ebx
mov flag[42] eax
mov eax flag[11]
xor eax ebx
mov flag[43] eax
mov eax flag[12]
xor eax ebx
mov flag[44] eax
mov eax flag[13]
xor eax ebx
mov flag[45] eax
mov eax flag[14]
xor eax ebx
mov flag[46] eax
mov eax flag[15]
xor eax ebx
mov flag[47] eax
mov eax flag[16]
xor eax ebx
mov flag[48] eax
mov eax flag[17]
xor eax ebx
mov flag[49] eax
mov eax flag[18]
xor eax ebx
mov flag[50] eax
mov eax flag[19]
xor eax ebx
mov flag[51] eax
nop


cmp strlen

#六个异或
mov eax flag[0]
mov ebx flag[1]
xor eax ebx
mov flag[0] eax

mov eax flag[1]
mov ebx flag[2]
xor eax ebx
mov flag[1] eax

mov eax flag[2]
mov ebx flag[3]
xor eax ebx
mov flag[2] eax

mov eax flag[3]
mov ebx flag[4]
xor eax ebx
mov flag[3] eax

mov eax flag[4]
mov ebx flag[5]
xor eax ebx
mov flag[4] eax

mov eax flag[5]
mov ebx flag[6]
xor eax ebx
mov flag[5] eax

#三个相乘
mov eax flag[6]
mov ebx flag[7]
mov ecx flag[8]
mov edx flag[12]
eax = ecx+2*ebx+3*eax
mul eax edx
mov flag[6] eax

mov eax flag[7]
mov ebx flag[8]
mov ecx flag[9]
mov edx flag[12]
eax = ecx+2*ebx+3*eax
mul eax edx
mov flag[7] eax

mov eax flag[8]
mov ebx flag[9]
mov ecx flag[10]
mov edx flag[12]
eax = ecx+2*ebx+3*eax
mul eax edx
mov flag[8] eax

#三个交换
mov eax flag[13]
mov ebx flag[19]
swap eax ebx
mov flag[13] eax
mov flag[19] ebx

mov eax flag[14]
mov ebx flag[18]
swap eax ebx
mov flag[14] eax
mov flag[18] ebx

mov eax flag[15]
mov ebx flag[17]
swap eax ebx
mov flag[15] eax
mov flag[17] ebx
nop

翻译成伪代码

大概进行了下面的这些操作

1
2
3
4
5
6
7
8
9
10
11
for i in range(6):
flag[i]^=flag[i+1]
flag[6]=(flag[8]+2*flag[7]+3*flag[6])*flag[12]
flag[7]=(flag[9]+2*flag[8]+3*flag[7])*flag[12]
flag[8]=(flag[10]+2*flag[9]+3*flag[8])*flag[12]
swap(flag[13],flag[19])
swap(flag[14],flag[18])
swap(flag[15],flag[17])
flag[13], flag[19] = flag[19], flag[13]
flag[14], flag[18] = flag[18], flag[14]
flag[15], flag[17] = flag[17], flag[15]

找密文解密

这里需要注意的是 sub_F83 里面的 aFzAmAmFmtSum 点进去得到的并不是真正的密文,真正的密文需要对 dword_2022A4 交叉引用,在 sub_F00 里面的 byte_202020 中找到


根据上面部分的加密逻辑写出解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from z3 import *
enc=[0x69, 0x45, 0x2A, 0x37, 0x09, 0x17, 0xC5, 0x0B, 0x5C, 0x72,
0x33, 0x76, 0x33, 0x21, 0x74, 0x31, 0x5F, 0x33, 0x73, 0x72,]
#假的加密数据 跑出来是 "Mt!! Mus~"
# enc=[0x46, 0x7A, 0x7B, 0x61, 0x4D, 0x7B,0x61, 0x4D, 0x7C, 0x7D,
# 0x66, 0x4D, 0x74, 0x7E, 0x73, 0x75, 0x4D, 0x20, 0x21, 0x21,]

enc[13], enc[19] = enc[19], enc[13]
enc[14], enc[18] = enc[18], enc[14]
enc[15], enc[17] = enc[17], enc[15]

s=Solver()
f=[BitVec(f"f{i}",8)for i in range(11)]
s.add(enc[6]==(f[8]+2*f[7]+3*f[6])*enc[12])
s.add(enc[7]==(f[9]+2*f[8]+3*f[7])*enc[12])
s.add(enc[8]==(f[10]+2*f[9]+3*f[8])*enc[12])

s.add(enc[0]==f[0]^f[1])
s.add(enc[1]==f[1]^f[2])
s.add(enc[2]==f[2]^f[3])
s.add(enc[3]==f[3]^f[4])
s.add(enc[4]==f[4]^f[5])
s.add(enc[5]==f[5]^f[6])

s.add(enc[9]==f[9])
s.add(enc[10]==f[10])

for var in f:
s.add(var>=32,var<=126)

flag=""
if s.check()==sat:
model=s.model()

for i in range(11):
flag += chr(model[f[i]].as_long())


for i in range(11,20):
flag+=chr(enc[i])

print(flag)

flag 即为 flag{Y0u_hav3_r3v3rs3_1t!}

[NPC²CTF 2025]babyVM

粗看逻辑

找到 main 函数,挺清晰的,输入 32 字节的 flag,然后再进 VM,之后进行比较,在 cipher 处找到密文


点进 VM 里面,很多 case ,case 里面还有继续套 case的,本来想手撕的,但是最后被劝退了,用打条件断点的方式进行 ida trace,这样就可以跳过写脚本去对应操作码的步骤

细看 case 打条件断点

看第一个 case0:

-4说明在向“栈”压入一个 4 字节的值
实际修改 qword_… 高 32 位的值 —— 更新“栈指针,模拟栈增长操作,所以这是 push 操作
对应汇编打条件断点,看寄存器,右键Edit breakpoinnt,写 idapython 脚本

case1 是看字符串长度

1
2
3
eax_val = idc.get_reg_value("eax")
output = f"strlen,{eax_val}(0x{eax_val:X})\n"
print(output.strip())

case2 是 mov 类,里面又有很多 case 分支,是不同的move,比对汇编和代码


注意打断点时不要打到 LABEL 里面去了,不然会重复输出
可以看到这是把一个参数赋值到内存地址,v41 对应的寄存器是 r8d,v42(rdx) 地址加上 v40(rcx) 偏移值就是地址值

1
2
3
4
5
6
7
rdx_val=idc.get_reg_value("rdx")
rcx_val=idc.get_reg_value("rcx")
r8d_val=idc.get_reg_value("r8d")
output = f"case0 mov [{rdx_val+rdx_val}],{r8d_val}\n"
log_path = r"D:\aaa\vm\log.txt"
with open(log_path, 'a', encoding='utf-8') as f:
f.write(output)

就这样继续对每一个 case 打条件断点,打的时候可以稍微标注一下,以便后续修改去对着找是哪一条

case3 是 sub 操作,case4 是 add 操作,case5 是 xor 操作,case6 是 add 操作,case7 是 shr 操作,case8 是 shl,case9 是比较字符串,case13 是 and 指令

找加密、密钥、轮数、delta

打印出来看到有这样的结构,一眼密钥

然后去找相似的循环结构,这里注意到

1
2
3
4
5
6
7
8
9
10
11
12
case3 mov rag2,0
case4 add reg2,1
...
case3 mov rag2,1
case4 add reg2,1
...
case3 mov rag2,2
case4 add reg2,1
...
...
case3 mov rag2,47
case4 add reg2,1

明显的计数器每经过一轮就加一,可以得出循环轮数为 48

左移右移的操作再加上 >>11&3,大致推测出这是一个魔改的 xtea 算法
接着去找 delta,delta 是一个在每轮加密中都会出现的常量,就去找到出现多的不变的常数,
其实在密钥的下面一点也出现了,delta 也是魔改了的,值为421101834
case7 mov [18446744073709551812],421101834

解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <bits/stdc++.h>
using namespace std;

void decrypt(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4], uint32_t delta) {
unsigned int i;
uint32_t v0 = v[0], v1 = v[1];

uint32_t sum = 0;
for (i = 0; i < num_rounds; i++) {
sum -= delta;
}

for (i = 0; i < num_rounds; i++) {
v1 -= (((v0 >> 7) ^ (v0 << 3)) + v0) ^ (sum + key[(sum >> 11) & 3]);
sum += delta;
v0 -= (((v1 >> 6) ^ (v1 << 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0] = v0;
v[1] = v1;
}

int main() {
const char* key_str = "Chovy_inkey_w36_";
uint32_t key[4] = {0};

// 自定义delta值
uint32_t delta = 421101834; // 0x191A06FA

// 将字符串转换为4个32位整数
// 每4个字符组成一个32位整数
for (int i = 0; i < 4; i++) {
key[i] = 0;
for (int j = 0; j < 4; j++) {
int idx = i * 4 + j;
if (idx < strlen(key_str)) {
key[i] |= ((uint32_t)key_str[idx] << (j * 8));
}
}
}

// 打印密钥
cout << "XTEA Key (hex): " << endl;
for (int i = 0; i < 4; i++) {
cout << "key[" << i << "] = 0x" << hex << key[i] << endl;
}

uint8_t encrypted_bytes[] = {
0xE5, 0xDF, 0xF0, 0xA1, 0xF4, 0xBD, 0x6A, 0xDB, 0x1B, 0xE9,
0xDD, 0x20, 0x0D, 0x9D, 0x21, 0x59, 0xD0, 0xB3, 0x59, 0x29,
0xB9, 0xEC, 0x2F, 0xC0, 0x22, 0x7E, 0xAD, 0xE1, 0xB0, 0x15,
0xB6, 0x29
};


// 将密文分成若干个块进行解密 (XTEA每次处理8字节)
int num_blocks = sizeof(encrypted_bytes) / 8;
uint8_t decrypted_bytes[sizeof(encrypted_bytes)];
memcpy(decrypted_bytes, encrypted_bytes, sizeof(encrypted_bytes));

for (int i = 0; i < num_blocks; i++) {
uint32_t* block = (uint32_t*)(decrypted_bytes + i * 8);
decrypt(48, block, key, delta);
}


cout << "Decrypted : ";
for (int i = 0; i < sizeof(decrypted_bytes); i++) {

if (decrypted_bytes[i] >= 32 && decrypted_bytes[i] <= 126) {
cout << (char)decrypted_bytes[i];
} else {
cout << ".";
}
}
cout << endl;

return 0;
}


得到 flag 为 flag{D0_yOu_l1k3_VmmmmMMMMMmmm?}


向 VM 的步步靠近--部分 vm 赛题复现
http://example.com/2025/05/24/vm/
作者
Eleven
发布于
2025年5月24日
许可协议