VM类型题目整理

逆向中的VM类型题目是一种基于虚拟机的代码保护技术,它将可执行代码转换为字节码指令系统的代码,一种是自定义的虚拟机,另一种是模拟tea等常见算法,来达到不被轻易逆向的目的,实现原理是将程序的代码转换自定义的操作码(opcode),然后在程序执行时再通过解释这些操作码,选择对应的函数执行,从而实现程序原有的功能

下面用&和~两种运算表示其他基础运算

直接还原opcode

给了opcode和算法,直接打印出具体代码之后再分析

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
opcode = [0x01, 0x01, 0x21, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0x53,
0x57, 0x44, 0x61, 0x44, 0x64, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4,
0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2,
0xE5, 0xD4, 0xA1, 0x53, 0x57, 0x44, 0x61, 0x44, 0x64, 0xF1,
0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4,
0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2,
0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0x53,
0x57, 0x44, 0x61, 0x44, 0x64, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4,
0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2,
0xE5, 0xD4, 0xA1, 0x53, 0x57, 0x44, 0x61, 0x44, 0x64, 0xF1,
0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4,
0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0x66, 0x6C, 0x61,
0x67, 0x7B, 0x59, 0x75, 0x69, 0x73, 0x61, 0x62, 0x65, 0x61,
0x75, 0x74, 0x69, 0x66, 0x75, 0x6C, 0x67, 0x69, 0x72, 0x6C,
0x7D, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2,
0xE5, 0xD4, 0xA1, 0x66, 0x6C, 0x61, 0x67, 0x7B, 0x59, 0x75,
0x69, 0x73, 0x61, 0x62, 0x65, 0x61, 0x75, 0x74, 0x69, 0x66,
0x75, 0x6C, 0x67, 0x69, 0x72, 0x6C, 0x7D, 0xF1, 0xC3, 0xB2,
0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0x66,
0x6C, 0x61, 0x67, 0x7B, 0x59, 0x75, 0x69, 0x73, 0x61, 0x62,
0x65, 0x61, 0x75, 0x74, 0x69, 0x66, 0x75, 0x6C, 0x67, 0x69,
0x72, 0x6C, 0x7D, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1,
0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0x53, 0x57, 0x44, 0x61, 0x44,
0x64, 0x66, 0x6C, 0x61, 0x67, 0x7B, 0x59, 0x75, 0x69, 0x73,
0x61, 0x62, 0x65, 0x61, 0x75, 0x74, 0x69, 0x66, 0x75, 0x6C,
0x67, 0x69, 0x72, 0x6C, 0x7D, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4,
0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2,
0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0x53,
0x57, 0x44, 0x61, 0x44, 0x64, 0x66, 0x6C, 0x61, 0x67, 0x7B,
0x59, 0x75, 0x69, 0x73, 0x61, 0x62, 0x65, 0x61, 0x75, 0x74,
0x69, 0x66, 0x75, 0x6C, 0x67, 0x69, 0x72, 0x6C, 0x7D, 0xF1,
0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0x66, 0x6C, 0x61, 0x67, 0x7B,
0x59, 0x75, 0x69, 0x73, 0x61, 0x62, 0x65, 0x61, 0x75, 0x74,
0x69, 0x66, 0x75, 0x6C, 0x67, 0x69, 0x72, 0x6C, 0x7D, 0xF1,
0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4,
0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2,
0xE5, 0xD4, 0xA1, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4, 0xA1, 0x53,
0x57, 0x44, 0x61, 0x44, 0x64, 0xF1, 0xC3, 0xB2, 0xE5, 0xD4,
0xA1, 0x99, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB,
0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB,
0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB,
0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB,
0xBF, 0xBB, 0x66, 0x6C, 0x61, 0x67, 0x7B, 0x59, 0x75, 0x69,
0x73, 0x61, 0x62, 0x65, 0x61, 0x75, 0x74, 0x69, 0x66, 0x75,
0x6C, 0x67, 0x69, 0x72, 0x6C, 0x7D, 0xBF, 0xBB, 0xBF, 0xBB,
0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB,
0xBF, 0xBB, 0xBF, 0xBB, 0xBF, 0xBB, 0x53, 0x57, 0x44, 0x44,
0xBF, 0xBB, 0xBF, 0xBB, 0x99, 0x53, 0x57, 0x44, 0x44, 0x53,
0x57, 0x44, 0x44, 0x88, 0xFF, 0x53, 0x57, 0x44, 0x44]

for i in opcode:
match i:
case 0xA1:
print("enc[i - 1] = v25 & v22")
case 0xC3:
print("v22 = v17 | v18")
case 0xB2:
print("v23 = ~v17")
case 0xE5:
print("v24 = ~v18")
case 0xF1:
print("v17 = input[i]")
print("v18 = input[++i]")
case 0xD4:
print("v25 = v24 | v23")
case 0xBF:
print("v6 = i++")
print("v21[0] = enc[v6]")
case 0x99:
print("i = 0")
case 0xBB:
print("enc[i - 1] = i + v21[0] - 1")

'''
i = 0
v17 = input[i]
v18 = input[++i]
v22 = v17 | v18
v23 = ~v17
v24 = ~v18
v25 = v24 | v23
enc[i - 1] = v25 & v22

v6 = i++
v21[0] = enc[v6]
enc[i - 1] = i + v21[0] - 1
'''

2023浙江省省赛决赛vm

去除花指令后得到main

发现是类似矩阵运算的加密,用z3得到fake flag

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
from z3 import *

v12 = [Int(f'v12_{i}') for i in range(24)]

v11 = [
[18, 52, 86, 120],
[154, 171, 205, 239],
[17, 69, 20, 25],
[25, 129, 24, 145],
[145, 65, 84, 17],
[151, 152, 153, 154]
]
v13 = [
24234, 24902, 25618, 23938, 27572, 31178,
69522, 68864, 73677, 68410, 77977, 83181,
12585, 9667, 13524, 12011, 11843, 14508,
31301, 26091, 30728, 30431, 29995, 37484,
28625, 28559, 31400, 28135, 33319, 30264,
55785, 53999, 59437, 54723, 61902, 64867
]

solver = Solver()

for j in range(6):
for k in range(6):
equation = v13[6 * j + k]
for m in range(4):
equation -= v12[6 * m + k] * v11[j][m]
solver.add(equation == 0)

for i in range(24):
solver.add(v12[i] >= 0, v12[i] <= 255)

if solver.check() == sat:
model = solver.model()

v12_solution = [model[v12[i]].as_long() for i in range(24)]
print("v12:", v12_solution)
flag = ''.join(chr(v) for v in v12_solution)
print(flag)
else:
print("No solution found")
#flag{Th1s_IsA_f@ke_fLag}

main前面加密得到encrypt

交叉引用找a1

Str 字符串指示了这个文件是 DLL 文件,解密算法是简单循环异或

由于 DLL 文件头固定,可以先推出key,这也是文件加密类题目的常见解法

1
2
3
4
5
6
7
8
9
data = open('encrypt', 'rb').read()[:100]
data = bytearray(data)
head = open('ezRE.exe', 'rb').read()[:100] # 任意PE文件
a = []
for i in range(99):
t = head[i] ^ data[i + 1] ^ head[i + 1] ^ 0x66
a.append(t)
print(bytes(a))
#gaLf_ek@f_AsI_s1hT{galf}gaLf_ek@f_AsI_s1hT{galf}gaLf_ek@f_AcY_s1hT{galf}gaLf_ek@f_AsI_s1hT{galf}gaL

得到gaLf_ek@f_AsI_s1hT{galf},从上面加密也可以看出从倒数第二个开始往前循环

然后解密得到DllMain.dll

1
2
3
4
5
6
data = open('encrypt', 'rb').read()
data = bytearray(data)
key = list(b'gaLf_ek@f_AsI_s1hT{galf}')
for i in range(len(data) - 1):
data[i + 1] ^= data[i] ^ key[i % len(key)] ^ 0x66
open('DllMain.dll', 'wb').write(data)

用vm实现tea

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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
int __cdecl sub_100012C0(int input, int *key)
{
int v2; // ecx
int v4; // [esp+4h] [ebp-70h]
int v5; // [esp+8h] [ebp-6Ch]
char v6[72]; // [esp+Ch] [ebp-68h] BYREF
int v7; // [esp+54h] [ebp-20h]
int v8; // [esp+58h] [ebp-1Ch]
int v9; // [esp+5Ch] [ebp-18h]
int v10; // [esp+60h] [ebp-14h]
int v11; // [esp+64h] [ebp-10h]
int v12; // [esp+68h] [ebp-Ch]
int v13; // [esp+6Ch] [ebp-8h]

strcpy(v6, "\t");
v6[2] = 6;
v6[3] = 7;
v6[4] = 5;
v6[5] = 8;
v6[6] = 1;
v6[7] = 2;
v6[8] = 8;
v6[9] = 3;
v6[10] = 0;
v6[11] = 8;
v6[12] = 2;
v6[13] = 5;
v6[14] = 9;
v6[15] = 1;
v6[16] = 0;
v6[17] = 9;
v6[18] = 6;
v6[19] = 5;
v6[20] = 10;
v6[21] = 1;
v6[22] = 3;
v6[23] = 10;
v6[24] = 4;
v6[25] = 0;
v6[26] = 10;
v6[27] = 3;
v6[28] = 1;
v6[29] = 8;
v6[30] = 9;
v6[31] = 1;
v6[32] = 8;
strcpy(&v6[33], "\n");
v6[35] = 0;
v6[36] = 8;
v6[37] = 5;
v6[38] = 8;
v6[39] = 0;
v6[40] = 2;
v6[41] = 8;
v6[42] = 5;
v6[43] = 0;
v6[44] = 8;
v6[45] = 4;
v6[46] = 5;
strcpy(&v6[47], "\t");
v6[49] = 0;
v6[50] = 9;
v6[51] = 6;
v6[52] = 5;
strcpy(&v6[53], "\n");
v6[55] = 3;
v6[56] = 10;
v6[57] = 6;
v6[58] = 0;
v6[59] = 10;
v6[60] = 5;
v6[61] = 1;
v6[62] = 8;
v6[63] = 9;
v6[64] = 1;
v6[65] = 8;
strcpy(&v6[66], "\n");
v6[68] = 1;
v6[69] = 8;
v6[70] = 11;
v6[71] = 0;
v7 = 0;
v8 = 0;
v9 = 0;
v10 = 0;
v11 = 0;
v12 = 0;
v13 = 0;
v4 = 0;
while ( v4 < 10 )
{
v5 = 0;
while ( 2 )
{
switch ( v6[v5] )
{
case 0:
add(&dword_100043D0[(unsigned __int8)v6[v5 + 1]], &dword_100043D0[(unsigned __int8)v6[v5 + 2]]);
goto LABEL_19;
case 1:
xor(&dword_100043D0[(unsigned __int8)v6[v5 + 1]], &dword_100043D0[(unsigned __int8)v6[v5 + 2]]);
goto LABEL_19;
case 2:
lshift(&dword_100043D0[(unsigned __int8)v6[v5 + 1]], (unsigned __int8)v6[v5 + 2]);
goto LABEL_19;
case 3:
rshift(&dword_100043D0[(unsigned __int8)v6[v5 + 1]], (unsigned __int8)v6[v5 + 2]);
goto LABEL_19;
case 5:
copy(&dword_100043D0[(unsigned __int8)v6[v5 + 1]], &dword_100043D0[(unsigned __int8)v6[v5 + 2]]);
goto LABEL_19;
case 6:
sub(&dword_100043D0[(unsigned __int8)v6[v5 + 1]], &dword_100043D0[(unsigned __int8)v6[v5 + 2]]);
goto LABEL_19;
case 7:
mul(&dword_100043D0[(unsigned __int8)v6[v5 + 1]], &dword_100043D0[(unsigned __int8)v6[v5 + 2]]);
goto LABEL_19;
case 8:
div(&dword_100043D0[(unsigned __int8)v6[v5 + 1]], &dword_100043D0[(unsigned __int8)v6[v5 + 2]]);
goto LABEL_19;
case 9:
dword_100043D0[6] = 0;
dword_100043D0[7] = 26318864;
dword_100043D0[11] = 0;
v2 = *(_DWORD *)(input + 4 * v4 + 4);
dword_100043D0[0] = *(_DWORD *)(input + 4 * v4);
dword_100043D4 = v2;
dword_100043D8 = *key;
dword_100043DC = key[1];
dword_100043E0 = key[2];
dword_100043E4 = key[3];
goto LABEL_19;
case 11:
if ( ++dword_100043D0[11] != 36 )
goto LABEL_19;
if ( !sub_10001130(dword_100043D0, dword_10004018[v4])
|| !sub_10001130(&dword_100043D0[1], dword_1000401C[v4]) )
{
return puts(Buffer);
}
v4 += 2;
break;
default:
LABEL_19:
if ( v6[v5] == 9 )
{
++v5;
}
else if ( v6[v5] == 11 )
{
v5 = 1;
}
else
{
v5 += 3;
}
continue;
}
break;
}
}
return puts(aGood);
}

先照抄代码模拟出加密流程

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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#include<stdio.h>
int main()
{
int v2; // ecx
int v4; // [esp+4h] [ebp-70h]
int v5; // [esp+8h] [ebp-6Ch]
char v6[72]; // [esp+Ch] [ebp-68h] BYREF
int v7; // [esp+54h] [ebp-20h]
int v8; // [esp+58h] [ebp-1Ch]
int v9; // [esp+5Ch] [ebp-18h]
int v10; // [esp+60h] [ebp-14h]
int v11; // [esp+64h] [ebp-10h]
int v12; // [esp+68h] [ebp-Ch]
int v13; // [esp+6Ch] [ebp-8h]

v6[0] = 9;
v6[1] = 0;
v6[2] = 6;
v6[3] = 7;
v6[4] = 5;
v6[5] = 8;
v6[6] = 1;
v6[7] = 2;
v6[8] = 8;
v6[9] = 3;
v6[10] = 0;
v6[11] = 8;
v6[12] = 2;
v6[13] = 5;
v6[14] = 9;
v6[15] = 1;
v6[16] = 0;
v6[17] = 9;
v6[18] = 6;
v6[19] = 5;
v6[20] = 10;
v6[21] = 1;
v6[22] = 3;
v6[23] = 10;
v6[24] = 4;
v6[25] = 0;
v6[26] = 10;
v6[27] = 3;
v6[28] = 1;
v6[29] = 8;
v6[30] = 9;
v6[31] = 1;
v6[32] = 8;
v6[33] = 10;
v6[34] = 0;
v6[35] = 0;
v6[36] = 8;
v6[37] = 5;
v6[38] = 8;
v6[39] = 0;
v6[40] = 2;
v6[41] = 8;
v6[42] = 5;
v6[43] = 0;
v6[44] = 8;
v6[45] = 4;
v6[46] = 5;
v6[47] = 9;
v6[48] = 0;
v6[49] = 0;
v6[50] = 9;
v6[51] = 6;
v6[52] = 5;
v6[53] = 10;
v6[54] = 0;
v6[55] = 3;
v6[56] = 10;
v6[57] = 6;
v6[58] = 0;
v6[59] = 10;
v6[60] = 5;
v6[61] = 1;
v6[62] = 8;
v6[63] = 9;
v6[64] = 1;
v6[65] = 8;
v6[66] = 10;
v6[67] = 0;
v6[68] = 1;
v6[69] = 8;
v6[70] = 11;
v6[71] = 0;
v4 = 0;
int input0,input1;
int key0,key1,key2,key3;
int a[12];
while ( v4 < 10 )
{
v5 = 0;
while ( 2 )
{
switch ( v6[v5] )
{
case 0:
printf("a[%d]+a[%d]\n",v6[v5 + 1],v6[v5 + 2]);
goto LABEL_19;
case 1:
printf("a[%d]^a[%d]\n",v6[v5 + 1],v6[v5 + 2]);
goto LABEL_19;
case 2:
printf("a[%d]<<%d\n",v6[v5 + 1],v6[v5 + 2]);
goto LABEL_19;
case 3:
printf("a[%d]>>%d\n",v6[v5 + 1],v6[v5 + 2]);
goto LABEL_19;
case 5:
printf("a[%d]=a[%d]\n",v6[v5 + 1],v6[v5 + 2]);
goto LABEL_19;
case 6:
printf("a[%d]-a[%d]\n",v6[v5 + 1],v6[v5 + 2]);
goto LABEL_19;
case 7:
printf("a[%d]*a[%d]\n",v6[v5 + 1],v6[v5 + 2]);
goto LABEL_19;
case 8:
printf("a[%d]/a[%d]\n",v6[v5 + 1],v6[v5 + 2]);
goto LABEL_19;
case 9:
a[6] = 0;
a[7] = 26318864;
a[11] = 0;
a[0] = input0;
a[1] = input1;
a[2] = key0;
a[3] = key1;
a[4] = key2;
a[5] = key3;
goto LABEL_19;
case 11:
break;
default:
LABEL_19:
if ( v6[v5] == 9 )
{
++v5;
}
else if ( v6[v5] == 11 )
{
exit(0);
}
else
{
v5 += 3;
}
continue;
}
v4+=1
break;
}
}
return 0;
}

/*
sum+=delta
input0+=(input1<<3+key0)^(input1+sum)^(input1>>4+key1)
input1+=(input0<<5+key2)^(input0+sum)^(input0>>6+key3)
*/

后面是魔改tea

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
#include <stdio.h>
#include <stdint.h>

int main() {
uint32_t v[2] = {1, 2};
uint32_t v0 = v[0], v1 = v[1], sum,i;
uint32_t delta = 0x1919810;
uint32_t v2[2] = {0x3D121D26, 0x5E6189F9}, v3[2] = {0xC1FB278E, 0x3B494648}, v5[10] = {0x0D9D29102,0X0DC34518C,0xc3e171aa,0x665d20e0,0x926a7765,0x139332ee,0xf00faf61,0x16b72b82,0x529fe600,0x2ba3db0c};
unsigned int k[4] = {0x1234567, 0x89ABCDEF, 0xFEDCBA98, 0x76543210}, l = 0, r = 0;
uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];

for (int m = 0; m < 10; m += 2) {
sum = delta*(36);
for (i = 0; i < 36; i++) {
v5[m + 1] -= ((v5[m] << 5) + k2) ^ (v5[m] + sum) ^ ((v5[m] >> 6) + k3);
v5[m] -= ((v5[m + 1] << 3) + k0) ^ (v5[m + 1] + sum) ^ ((v5[m + 1] >> 4) + k1);
sum -= delta;
}
}

for (int i = 0; i < 10; i++) {
for (int m = 0; m <= 3; m++) {
printf("%c", (v5[i] >> (8 * m)) & 0xff);
}
}

return 0;
}
//flag{Be@ut1fuL-Ex2cpti0n-Vi3tual-F10wer}

有时候只能打印出伪汇编,需要再转化成高级语言,参考upsw1ng师傅写的文章:https://mp.weixin.qq.com/s/t1MeYCCrBgl9U-vZiF9RZQ

这个时候也可以编译出bin文件,最后IDA反编译bin文件得到伪代码

z3约束

与直接还原相比用z3可以不还原算法

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

def getfile(file_path):
opcode = []
with open(file_path, 'r') as file:
for line in file:
opcode.append(int(line.strip(), 16))
return opcode

def encode(flag, opcode):
encoded_flag = flag.copy()
i = 1
while i < len(opcode):
op_type = opcode[i - 1]
target = opcode[i]
value = opcode[i + 1]

if op_type == 1:
# 加法操作后截断为 8 位
encoded_flag[target] = (encoded_flag[target] + value) & 0xFF
print(f"flag[{target}] += {hex(value)}")
i += 3
elif op_type == 4:
# 减法操作后截断为 8 位
encoded_flag[target] = (encoded_flag[target] - value) & 0xFF
print(f"flag[{target}] -= {hex(value)}")
i += 3
elif op_type == 5:
# 异或操作后截断为 8 位
encoded_flag[target] = (encoded_flag[target] ^ value) & 0xFF
print(f"flag[{target}] ^= {hex(value)}")
i += 3
else:
break # 未知操作类型退出循环
return encoded_flag

if __name__ == '__main__':
opcode = getfile("E:\\CTF\\code\\python\\CTF\\dumpopcode.txt")
flag_size = 32
flag = [z3.BitVec(f'flag[{i}]', 8) for i in range(flag_size)] # 使用 8 位 BitVec
flag_enc = [
0x65, 0xE2, 0x57, 0x60, 0xCE, 0x1E, 0xE1, 0x5C, 0x4B, 0x4B,
0x23, 0x6D, 0x8C, 0xC2, 0xBC, 0x58, 0x84, 0x92, 0x7E, 0x8C,
0x43, 0xDB, 0x15, 0x71, 0x97, 0x4A, 0xE3, 0xC4, 0x1F, 0x7C,
0xC2, 0xFD
]

solver = z3.Solver()
encoded_flag = encode(flag, opcode)

# 添加约束:每个字节最终等于 flag_enc
for j in range(len(flag_enc)):
solver.add(encoded_flag[j] == flag_enc[j])
for i in solver.assertions():
print(i)
if solver.check() == z3.sat:
model = solver.model()
result_flag = [model.evaluate(flag[i]).as_long() for i in range(flag_size)]
flag_str = ''.join(chr(c) for c in result_flag)
print("flag:", flag_str)
else:
print("No solution found.")

2023NCTFezVM

先脱壳,发现有很多分支,照抄之后用z3解,相比上面只不过多了一些opcode和对应的操作

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
#!/usr/bin/env python3

from z3 import *

def fetch_int(pc, size):
assert pc + size <= len(codes), pc
return int.from_bytes(codes[pc: pc + size], 'little'), pc + size

def fetch_byte(pc):
return fetch_int(pc, 1)

def fetch_word(pc):
return fetch_int(pc, 2)

def fetch_dword(pc):
return fetch_int(pc, 4)

def fetch_qword(pc):
return fetch_int(pc, 8)

s = Solver()
symbols = [BitVec('x%d' % i, 64) for i in range(44)]
for i in symbols:
s.add(i > 0)
s.add(i < 0x80)

# symbols = list(b'flag{0123456789abcdef0123456789abcdef012345}')

stack = [0] * 256
rsp = 100

codes = open('./ezVM.exe', 'rb').read()[0x2840: ][: 0x1bfc0]
pc = 0
while pc < len(codes):
opcode, pc = fetch_byte(pc)
if opcode == 0x0b:
rsp += 8
elif opcode == 0x0c:
imm, pc = fetch_word(pc)
rsp -= 2
stack[rsp: rsp + 2] = list(imm.to_bytes(2, 'little'))
elif opcode == 0x0e:
rsp -= 1
stack[rsp] = stack[rsp + 1]
elif opcode == 0x19:
imm, pc = fetch_byte(pc)
print('exit(0x%x);' % imm)
break
elif opcode == 0x32:
imm, pc = fetch_byte(pc)
rsp -= 1
stack[rsp] = imm
elif opcode == 0x49:
stack[rsp + 1] = ~stack[rsp + 1] & 0xff
elif opcode == 0x71:
stack[rsp + 1] &= stack[rsp]
rsp += 1
elif opcode == 0x72:
stack[rsp] = ~stack[rsp] & 0xff
elif opcode == 0x7b:
rsp -= 7
stack[rsp: rsp + 8] = list((0xabababababababab).to_bytes(8, 'little'))
elif opcode == 0x7c:
rsp -= 2
stack[rsp: rsp + 2] = list(stack[rsp + 2: rsp + 4])
elif opcode == 0x8d:
if stack[rsp] == 0: pc = int.from_bytes(bytes(stack[rsp + 1: rsp + 5]), 'little')
rsp += 5
elif opcode == 0x8e:
rsp += 2
elif opcode == 0x91:
imm, pc = fetch_dword(pc)
rsp -= 4
stack[rsp: rsp + 4] = list(imm.to_bytes(4, 'little'))
elif opcode == 0x99:
rsp += 4
elif opcode == 0xad:
rsp -= 1
stack[rsp] = r0
elif opcode == 0xb5:
if type(stack[rsp]) == int:
stack[rsp] = stack[rsp] != 0
else:
# print(simplify(stack[rsp]))
s.add(stack[rsp] == 0)
stack[rsp] = 0
elif opcode == 0xb7:
r0 = stack[rsp]
rsp += 1
elif opcode == 0xb8:
ptr = int.from_bytes(bytes(stack[rsp + 1: rsp + 9]), 'little')
assert 0xabababababababab == ptr, hex(ptr - 0xabababababababab)
stack[rsp] = symbols[stack[rsp]]
elif opcode == 0xd3:
imm, pc = fetch_qword(pc)
rsp -= 8
stack[rsp: rsp + 8] = list(imm.to_bytes(8, 'little'))
elif opcode == 0xea:
rsp += 1
elif opcode == 0xfb:
key, pc = fetch_byte(pc)
imm, pc = fetch_byte(pc)
size = key ^ imm
_s = ''
for i in range(size):
_s += chr(stack[rsp + i] ^ key)
print('puts(%s)' % repr(_s[::-1]))
rsp += size
elif opcode == 0xff:
if type(stack[rsp]) == int:
stack[rsp] = int(stack[rsp] == 0)
else:
# print(simplify(stack[rsp]))
s.add(stack[rsp] == 0)
stack[rsp] = 1
else:
pass
assert len(stack) == 256

assert s.check() == sat
model = s.model()

flag = []
for i in symbols:
flag.append(model[i].as_long())

print(bytes(flag))

# flag{O1SC_VM_1s_h4rd_to_r3v3rs3_#a78abffaa#}

符号执行爆破

适用于结果是一位位比较密文和加密明文的题目

HSCCTF2024ezvm

跟进vm的加密函数,这个函数变成了一个完整的switch case语句

利用插件,先设置好两个断点,分别用于进行符号化和爆破flag

下面是第一个断点,用来获取输入

img

第二个断点,用来爆破flag

img

ctrl+g跳到相应位置

修改eip

img 一位位爆破,最终得到flagimg

单字节加密

一些单字节加密题目基于VM,VM 程序通常有一个指令分发器,在分发指令时需要一个类似于 EIP的累加器来确保指令正确执行在单字节验证的过程中,如果出现一个字节不匹配的情况,程序会立即退出。这里存在一个相关关系:当前字节验证正确时,EIP 会向下移动,累加器也会相应增加。因此,使用先插入代码,在程序退出时检查累加器的变化,以此判断当前位是否验证成功

frida插桩爆破

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
var number = 22;
function main() {
var base = Module.findBaseAddress("vm.exe");
if (base) {
// console.log(base);
Interceptor.attach(base.add(0x197F), {//循环的地方
//opcode
onEnter: function(args) {
number += 1;
}
});
Interceptor.attach(base.add(0x19A2), { //最好放在retn

onEnter: function(args) {

//console.log(number)
send(number);
var a = 0;
for (var i = 0; i < 10000; i++) {
a += 1;
}
var f = new NativeFunction(base.add(0x274A), 'void', ['int']); //exit函数地址,不是特别重要
f(0);
}
});
}
}

setImmediate(main);

2023NCTFezVM

hook的两个位置分别为opcode分发和putchar的位置

尝试的脚本,发现可以hook

调用exit函数在获取发送过来的number之后结束程序,可以得到更快的爆破速度

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
var number = 0
function main()
{
var base = Module.findBaseAddress("ezVM.exe")
//获取目标进程的基地址
//console.log("inject success!!!")
//console.log("base:",base)
if(base){
Interceptor.attach(base.add(0x1044), {

onEnter: function(args) {

//console.log("number",number)
number+=1
//进行插桩 每当程序运行到这里 number+=1

}

});

Interceptor.attach(base.add(0x0113f), {
onEnter: function(args) {

//console.log("end!",number)
send(number)
var a = 0;
for(var i = 0 ; i < 9999 ; i ++ ){
a+=1;
}
var f = new NativeFunction(base.add(0x21D8),'void',['int']);
f(0)
//当程序执行结束后把结果发送个消息处理函数
}

});
}
}
setImmediate(main);

自动化脚本,实际测试发现可能不止一条路,要找到正确的路需要不断往flag字符串里面添加正确的字符才能尽量使得后续字符正确

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
import subprocess
import win32api
import win32con
def start_suspended_process(proc_name):
creation_flags = 0x14
process = subprocess.Popen(proc_name, creationflags=creation_flags)
print("子进程已启动并挂起")
return process.pid
import ctypes
def resume_process(pid):
try:
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
kernel32.DebugActiveProcess(pid)
print(f"进程 {pid} 已恢复.")
except OSError as e:
print(f"恢复进程时发生错误: {str(e)}")

printable = "`!\"#$%&'()*+,-./:;<=>?@[\]^_{|}~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

import frida, sys
number = 102741
number =103833
new_number = 0
def is_right():
global new_number,number
if new_number > number:
number = new_number
return True
else:
return False

def on_message(message, data):
global new_number
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
new_number = message['payload']
# val = int(message['payload'], 16)
# script.post({'type': 'input', 'payload': str(val * 2)})
elif message['type'] == "error":
print(message["description"])
print(message["stack"])
print(message["fileName"],"line:",message["lineNumber"],"colum:",message["columnNumber"])
else:
print(message)
pass
jscode = open("h00k.js","rb").read().decode()
import subprocess
# 44 -6 = 38 5--42
flag = "flag{O"

for index in range(len(flag),44):
for i in printable:
process = subprocess.Popen("ezVm.exe",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
tmp_flag = (flag+i).ljust(43,"A")+"}"
print(tmp_flag)
print("try index:",index ,"chr :",i)


session = frida.attach("ezVM.exe")
# 在目标进程里创建脚本
script = session.create_script(jscode)
# 注册消息回调
script.on('message', on_message)
#print('[*] Start attach')
# 加载创建好的javascript脚本
script.load()

process.stdin.write(tmp_flag)

output, error = process.communicate()
if(i == '`'):
number = new_number

elif(is_right() == True):
flag +=i
print(flag)
break
process.terminate()

pintools爆破

动态二进制插桩,在不修改源代码的情况下,对二进制可执行文件进行运行时插桩,以便分析统计指令数量、分支次数等特征,与frida插桩爆破相比好处是误差较小,自动化程度较高

2025强网拟态初赛hyperjump

典型的VM,switch-case 结构

用Pintools进行侧信道爆破,遍历指令最多的字符就是该位置正确的字符

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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/usr/bin/env python3
import re
import string
import sys
import os
import subprocess
from pwn import *

# --- 1. 配置区 ---

TARGET_BINARY = os.path.abspath('./hyperjump')
FLAG_LEN = 24
START_INDEX = 0

PIN_ROOT = '/root/pin'
PIN_TOOL_PATH = os.path.abspath('./pintools/pin-external-4.0-linux/source/tools/ManualExamples/obj-intel64/inscount0.so')

PIN_CMD_LIST = [os.path.join(PIN_ROOT, 'pin'), '-t', PIN_TOOL_PATH, '--', TARGET_BINARY]

CHARSET = string.ascii_lowercase + string.digits + "_@{}"

context.log_level = 'info'


# --- 2. 使用 subprocess 可靠获取 Pin 输出并解析指令数 ---

def read_inscount_outfile(workdir=None):
candidates = ['inscount.out', 'inscount.txt', 'pin.out']
for name in candidates:
path = os.path.join(workdir if workdir else '.', name)
if os.path.exists(path):
try:
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
data = f.read()
# 尝试解析数字
m = re.search(r'(\d+)\s+instructions', data)
if m:
return int(m.group(1)), data
# 直接取第一个数字
m2 = re.search(r'(\d+)', data)
if m2:
return int(m2.group(1)), data
except Exception:
pass
return None, None


def get_inscount(payload: bytes) -> int:

IO_TIMEOUT = 5
log.info(f"Testing payload: {payload.decode('latin-1')[:START_INDEX+1]}...")

try:
# 启动 Pin 并把 payload 发到 stdin
# 注意:text=True 使 input/outputs 为 str 类型
proc = subprocess.run(
PIN_CMD_LIST,
input=payload.decode('latin-1') + '\n', # 把 payload 当作 stdin 发给程序
capture_output=True,
timeout=IO_TIMEOUT,
text=True,
cwd='.' # 确保当前目录是工作目录(inscount 输出文件通常在 cwd)
)

stdout = proc.stdout or ""
stderr = proc.stderr or ""
exitcode = proc.returncode

# 打印调试信息(PID 不好拿到,因为 subprocess.run 是阻塞调用)
sys.stderr.write(f"\n--- Pin Tool 完整输出 (Exit Code {exitcode}): ---\n")
if stdout.strip():
sys.stderr.write("[stdout]\n" + stdout.strip() + "\n")
if stderr.strip():
sys.stderr.write("[stderr]\n" + stderr.strip() + "\n")
sys.stderr.write("--------------------------------------------------\n")

combined = stdout + "\n" + stderr

# 1) 先在 combined 输出中查找像 "12345 instructions" 的形式
match_re = re.search(r'(\d+)\s+instructions', combined, re.IGNORECASE)
if match_re:
return int(match_re.group(1))

# 2) 再查找最后一行是数字的情况
lines = combined.strip().splitlines()
for line in reversed(lines):
s = line.strip()
if s.isdigit():
return int(s)

# 3) 检查常见的输出文件(inscount.out 等)
file_count, file_content = read_inscount_outfile(workdir='.')
if file_count is not None:
# 可选:删除该文件以免后续干扰(如果你不想删除可以注释掉)
try:
os.remove('inscount.out')
except Exception:
pass
return file_count

# 4) 启发式判断:若退出码非 0 且输出包含拒绝信息,则视为无效(返回 0)
low = combined.lower()
if exitcode is not None and exitcode != 0:
# 常见拒绝提示
if any(k in low for k in ['nope', 'try again', 'wrong', 'incorrect']):
return 0
# 若 stderr 有 pin 自身的错误,返回 0
if stderr.strip():
return 0

# 5) 若退出码是 0 且输出包含成功关键字,则返回非常大的计数作为 heuristic
if exitcode == 0 and any(k in low for k in ['flag', 'correct', 'congrats', 'success']):
return 10**9

# 否则无法解析到有效信息
log.warning("无法从 Pin 输出或文件中解析到指令数(stdout/stderr/file 均无数字)。")
return 0

except subprocess.TimeoutExpired as te:
sys.stderr.write(f"[TIMEOUT] Pin 运行超时: {te}\n")
return 0
except FileNotFoundError as fe:
sys.stderr.write(f"[FATAL] 找不到 pin 可执行文件或路径错误: {fe}\n")
return 0
except Exception as e:
sys.stderr.write(f"[FATAL ERROR] get_inscount 发生错误: {e}\n")
return 0


# --- 3. 爆破主循环 ---

def main():
known_flag = list("A" * FLAG_LEN)
log.info(f"开始爆破,长度 {FLAG_LEN},字符集大小 {len(CHARSET)},从索引 {START_INDEX} 开始。")

if not os.path.exists(TARGET_BINARY):
log.critical(f"目标文件不存在: {TARGET_BINARY}")
return
if not os.path.exists(PIN_TOOL_PATH):
log.critical(f"Pin Tool 不存在: {PIN_TOOL_PATH}")
return
if not os.path.exists(PIN_CMD_LIST[0]):
log.critical(f"Pin 可执行文件不存在: {PIN_CMD_LIST[0]}")
return

for i in range(START_INDEX, FLAG_LEN):
log.info(f"\n>>>> 正在爆破第 {i} 位...")

max_count = 0
best_char = None

for char in CHARSET:
current_test = known_flag[:]
current_test[i] = char
payload = "".join(current_test).encode('latin-1')

count = get_inscount(payload)

if count == 0:
continue

if count > max_count:
max_count = count
best_char = char
log.info(f"发现新的最大值: 字符 '{best_char}' -> {max_count} 条指令")

if best_char:
known_flag[i] = best_char
log.success(f"第 {i} 位确定为: '{best_char}'")
log.success(f"当前 Flag: {''.join(known_flag)}")
else:
log.warning(f"第 {i} 位爆破失败。所有字符返回 0。")
break

log.success(f"\n--- 爆破完成 ---")
log.success(f"最终 Flag: {''.join(known_flag)}")


if __name__ == '__main__':
main()

参考链接:

https://bbs.kanxue.com/thread-267670.htm

https://blog.csdn.net/Hotspurs/article/details/106039643

https://bbs.kanxue.com/thread-281796.htm

https://bbs.kanxue.com/thread-285831.htm


VM类型题目整理
https://j1ya-22.github.io/2025/12/17/VM类型题目整理/
作者
j1ya
发布于
2025年12月17日
许可协议