Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

跟着2022年腾讯游戏安全竞赛(初赛)从零开始学DLL注入

记录一下复现2022年腾讯游戏安全初赛的踩坑经历

这一年的初赛赛题感觉是历年来比较不猜迷且简单的 附件没套壳没混淆没反调试 先尝试运行一下 开了一个窗口渲染出了ACE字样:

image-20250325214732558

但是过了几秒就空白了 而解出这题要求让程序自己画出如下例图:

image-20250325215634213

IDA静态分析

接下来先用IDA静态分析一下逻辑

WinMain中的主要逻辑在消息循环处:

image-20250325215443774

当消息队列里没有要处理的消息时会调用func1 尝试NOP掉发现就不打印图像了 那么渲染出图像的逻辑应该就在其中

image-20250325220410521

一开始就用ZwAllocateVirtualMemory分配了一块RWX的内存Target_Mem

关于该内核函数:

1
2
3
4
5
6
7
8
NTSYSAPI NTSTATUS ZwAllocateVirtualMemory(
[in] HANDLE ProcessHandle,
[in, out] PVOID *BaseAddress,
[in] ULONG_PTR ZeroBits,
[in, out] PSIZE_T RegionSize,
[in] ULONG AllocationType,
[in] ULONG Protect
);

从这里大概就能猜到下面要加载shellcode来运行了:

image-20250325220838124

果然接下来就解码了一段shellcode 然后以其中的dec_func为入口执行 还能看到会根据程序运行的时间销毁这个段 这应该就是图像几秒钟就消失的原因 patch掉这个分支以便后续调试和hook

既然程序没有任何反调试 直接调试看看里面干了什么

初步动态分析

image-20250325221346485

一进来就看到了很扎眼的一串HLSL 它是用于被D3DCompile编译为着色器的高级语言 那么下面以它为参数执行的应该就是D3DCompile 同样下面还有一段 问AI 说一个是定点着色器一个是像素着色器 不过这些都不重要 最重要的是要知道光是编译着色器是无法完成图像的渲染的 具体的坐标和颜色参数肯定会在某处传入

后续程序执行的基本上都是库函数 基本上不太可能在里面生成位置和颜色参数 直到又进入了一个唯一被调用的用户函数:

image-20250325221910107

里面是一个看起来很简单的虚拟机过程:

image-20250325222241850

其中regdraw_cube是根据逻辑自己定义的 IDA自己识别会识别出几个变量

当opcode为5或6时还会执行一个用户函数draw_cube 它的第5个参数分别是蓝色和黄色的argb值 猜测这里应该就是绘制图像的函数 里面进行了很多浮点数运算 看不懂什么意思 但是根据在绘制蓝色方块的调用处下断进行调试大概可以看出来第1, 2个参数分别是x, y坐标 因为值都不是很大 编写IDApython脚本初步hook这两个调用看看前4个参数在每次调用时是什么:

1
2
3
4
5
6
7
8
9
10
from ida_dbg import get_reg_val as regs
from idc import get_bytes

a1 = regs('ecx')
a2 = regs('edx')
a3 = regs('r8d')
a4 = regs('r9d')
a5 = int.from_bytes(get_bytes(regs('rsp') + 0x80 - 0x60, 4), 'little')
with open('log.txt', 'a+') as log:
log.write(f'args : [{hex(a1)}, {hex(a2)}, {hex(a3)}, {hex(a4)}, {hex(a5)}, ...]\n')

得到以下结果:

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
args : [0xfffffc4a, 0x32, 0xff1fa3ac, 0x130bd0, 0xffffff00, ...]
args : [0x32, 0xfffffe7a, 0x1bcd69, 0xffe8bcc5, 0xffffff00, ...]
args : [0xfffffc4a, 0xaa, 0xff8eb263, 0xd91997, 0xffffff00, ...]
args : [0x32, 0xe6, 0x1cf400, 0xe82b18, 0xffffff00, ...]
args : [0xfffffc4a, 0xffffff2e, 0x391faa, 0x6ee5d2, 0xffffff00, ...]
args : [0x32, 0x15e, 0xebe72, 0xd77de2, 0xffffff00, ...]
args : [0xfffffc86, 0xfffffef2, 0x71150, 0x74fb28, 0xffffff00, ...]
args : [0xaa, 0xfffffef2, 0xffda3e17, 0xc42e8b, 0xffffff00, ...]
args : [0xfffffcfe, 0xe6, 0x34cf2b, 0xff321ecf, 0xffffff00, ...]
args : [0x6e, 0xfffffe7a, 0xffa49c19, 0xd9e5f1, 0xffffff00, ...]
args : [0xfffffcc2, 0xaa, 0xffca5ee7, 0xc42d8b, 0xffffff00, ...]
args : [0x28a, 0x32, 0xeabf17, 0xa7e3ab, 0xff2ddbe7, ...]
args : [0x24e, 0x6e, 0xc42d8b, 0xcb0f37, 0xff2ddbe7, ...]
args : [0x212, 0xaa, 0xd717af, 0x1f5b13, 0xff2ddbe7, ...]
args : [0x1d6, 0xe6, 0xc460e9, 0xe9ad55, 0xff2ddbe7, ...]
args : [0x19a, 0x122, 0xc7a989, 0xb9fd35, 0xff2ddbe7, ...]
args : [0x1d6, 0x122, 0xab7100, 0x646cf8, 0xff2ddbe7, ...]
args : [0x212, 0x122, 0xc409a9, 0x31cd9d, 0xff2ddbe7, ...]
args : [0x2c6, 0x32, 0xd77e8b, 0x2f2773, 0xff2ddbe7, ...]
args : [0x302, 0x32, 0xd9ad01, 0x996535, 0xff2ddbe7, ...]
args : [0x33e, 0x32, 0x39e156, 0xda4a26, 0xff2ddbe7, ...]
args : [0x2c6, 0x6e, 0x13207c, 0x346848, 0xff2ddbe7, ...]
args : [0x302, 0xaa, 0xc9140b, 0xb5fa7, 0xff2ddbe7, ...]
args : [0x33e, 0xe6, 0x53071f, 0xc7ab3b, 0xff2ddbe7, ...]
args : [0x37a, 0x122, 0xd71106, 0xd6329a, 0xff2ddbe7, ...]
args : [0x3b6, 0x122, 0xeb60a1, 0xa58d79, 0xff2ddbe7, ...]
args : [0x3f2, 0x122, 0xeb67cd, 0xf5e9d9, 0xff2ddbe7, ...]
args : [0x42e, 0x122, 0xd71161, 0xd7d31, 0xff2ddbe7, ...]
args : [0x3b6, 0x32, 0x677611, 0x659df9, 0xff2ddbe7, ...]
args : [0x3f2, 0x32, 0x40173d, 0x557919, 0xff2ddbe7, ...]
args : [0x42e, 0x32, 0xd77661, 0x3d9d01, 0xff2ddbe7, ...]
args : [0x46a, 0x32, 0xefff9a, 0xca2e06, 0xff2ddbe7, ...]
args : [0x3f2, 0x6e, 0xc404eb, 0xb7178b, 0xff2ddbe7, ...]
args : [0x42e, 0xaa, 0xd7c6ef, 0x8b6337, 0xff2ddbe7, ...]
args : [0x46a, 0xaa, 0xd7701f, 0x677b0b, 0xff2ddbe7, ...]
args : [0x4a6, 0xaa, 0xd71171, 0xfd4d21, 0xff2ddbe7, ...]
args : [0x4e2, 0xaa, 0x3906ab, 0xbbf27, 0xff2ddbe7, ...]
args : [0x46a, 0xe6, 0x96663, 0xef5f33, 0xff2ddbe7, ...]
args : [0x4a6, 0x122, 0x732157, 0x835b9f, 0xff2ddbe7, ...]
args : [0x4e2, 0x122, 0x353730, 0x383434, 0xff2ddbe7, ...]
args : [0x51e, 0x122, 0x6257a9, 0x9555e9, 0xff2ddbe7, ...]
args : [0x55a, 0x122, 0xd777af, 0xdf5bd3, 0xff2ddbe7, ...]

黄色的调用了11次 蓝色的调用了31次 可以和显示出来的图像对上 并且观察绘制蓝色方块时的第2个参数 这个参数为0x32的有8次调用 对应了y坐标最小时的蓝色方块一共有8个 也就是说前两个参数确实对应x, y坐标

绘制黄色方块的函数也是有调用的 应该也会绘制出黄色方块 但是当绘制黄色方块时传入的位置参数有很多是负数的 即使是两个看起来都很正常的坐标 也没用任何黄色方块被绘制 那么就要分析它的虚拟机过程了 还好这一步也不算复杂 毕竟只有那么几个opcode

VM分析

dump出opcode后用python脚本模拟:

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
opcode = [
0x00000002, 0x00000008, 0x00000000, 0x00000002, 0x00000000, 0x00000004, 0x00000002, 0x00000004, 0x00000000, 0x00000003, ...
]
mask = ((1 << 32) - 1)

with open('result_fix.txt', 'a+') as f:
ip = 0
reg = [0] * 10
reg[-1] = reg[-2] = 50
while ip < len(opcode):
res = ''
match opcode[ip]:
case 0:
res = f"reg[0]({reg[0] :#08x}) += reg[1]({reg[1] :#08x}) -> {reg[0] + reg[1] :#08x}"
reg[0] += reg[1]
case 1:
res = f"reg[0]({reg[0] :#08x}) -= reg[1]({reg[1] :#08x}) -> {reg[0] - reg[1] :#08x}"
reg[0] -= reg[1]
case 2:
opr = opcode[ip + 1]
ip += 2
res = f"reg[{opcode[ip]}] = reg[{opr}] -> {reg[opr] :#08x}"
reg[opcode[ip]] = reg[opr]
case 3:
opr = opcode[ip + 1]
ip += 2
res = f"reg[{opcode[ip]}] = {opr} -> {opr :#08x}"
reg[opcode[ip]] = opr
case 4:
ip += 1
res = f"Decode reg[0] and reg[1] ->"
v1 = reg[0]
v2 = (reg[0] * (reg[1] + 1)) & mask
reg[0] = opcode[ip] ^ 0x414345
reg[1] = ((reg[0] ^ (reg[1] + v1)) % 256 + (((reg[0] ^ (v1 * reg[1])) % 256 + (((reg[0] ^ (reg[1] + v2)) % 256) << 8)) << 8)) & mask
res += f" {reg[0] :#08x}, {reg[1] :#08x}"
case 5:
res = f"Draw yellow cube with args: {reg[4]:#08x}, {reg[5]:#08x}, {reg[6]:#08x}, {reg[7]:#08x}"
yellow_points.append((reg[4], reg[5]))
arg3.append(reg[6])
arg4.append(reg[7])
case 6:
res = f"Draw blue cube with args: {reg[4]:#08x}, {reg[5]:#08x}, {reg[6]:#08x}, {reg[7]:#08x}"
Blue_points.append((reg[4], reg[5]))
case _:
ip += 1
continue

ip += 1
f.write(f"{res}\n")

得到以下结果:

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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
reg[0] = reg[8] -> 0x000032
reg[4] = reg[0] -> 0x000032
reg[0] = reg[4] -> 0x000032
reg[1] = 1000 -> 0x0003e8
reg[0](0x000032) -= reg[1](0x0003e8) -> -0x003b6
reg[4] = reg[0] -> -0x003b6
reg[0] = reg[9] -> 0x000032
reg[5] = reg[0] -> 0x000032
reg[0] = reg[4] -> -0x003b6
reg[1] = reg[5] -> 0x000032
Decode reg[0] and reg[1] -> 0x130bd0, 0x20a4ac
reg[3] = reg[0] -> 0x130bd0
reg[0] = reg[1] -> 0x20a4ac
reg[1] = reg[3] -> 0x130bd0
reg[6] = reg[0] -> 0x20a4ac
reg[7] = reg[1] -> 0x130bd0
Draw yellow cube with args: -0x003b6, 0x000032, 0x20a4ac, 0x130bd0
reg[0] = reg[8] -> 0x000032
reg[4] = reg[0] -> 0x000032
reg[0] = reg[9] -> 0x000032
reg[1] = 60 -> 0x00003c
reg[0](0x000032) += reg[1](0x00003c) -> 0x00006e
reg[5] = reg[0] -> 0x00006e
reg[0] = reg[5] -> 0x00006e
reg[1] = 500 -> 0x0001f4
reg[0](0x00006e) -= reg[1](0x0001f4) -> -0x00186
reg[5] = reg[0] -> -0x00186
reg[0] = reg[4] -> 0x000032
reg[1] = reg[5] -> -0x00186
Decode reg[0] and reg[1] -> 0x1bcd69, 0xe9bdc5
reg[6] = reg[0] -> 0x1bcd69
reg[7] = reg[1] -> 0xe9bdc5
Draw yellow cube with args: 0x000032, -0x00186, 0x1bcd69, 0xe9bdc5
reg[0] = reg[8] -> 0x000032
reg[4] = reg[0] -> 0x000032
reg[0] = reg[4] -> 0x000032
reg[1] = 1000 -> 0x0003e8
reg[0](0x000032) -= reg[1](0x0003e8) -> -0x003b6
reg[4] = reg[0] -> -0x003b6
reg[0] = reg[9] -> 0x000032
reg[1] = 120 -> 0x000078
reg[0](0x000032) += reg[1](0x000078) -> 0x0000aa
reg[5] = reg[0] -> 0x0000aa
reg[0] = reg[4] -> -0x003b6
reg[1] = reg[5] -> 0x0000aa
Decode reg[0] and reg[1] -> 0xd91997, 0x8fb363
reg[3] = reg[0] -> 0xd91997
reg[0] = reg[1] -> 0x8fb363
reg[1] = reg[3] -> 0xd91997
reg[6] = reg[0] -> 0x8fb363
reg[7] = reg[1] -> 0xd91997
Draw yellow cube with args: -0x003b6, 0x0000aa, 0x8fb363, 0xd91997
reg[0] = reg[8] -> 0x000032
reg[4] = reg[0] -> 0x000032
reg[0] = reg[9] -> 0x000032
reg[1] = 180 -> 0x0000b4
reg[0](0x000032) += reg[1](0x0000b4) -> 0x0000e6
reg[5] = reg[0] -> 0x0000e6
reg[0] = reg[4] -> 0x000032
reg[1] = reg[5] -> 0x0000e6
Decode reg[0] and reg[1] -> 0xe82b18, 0x1cf400
reg[3] = reg[0] -> 0xe82b18
reg[0] = reg[1] -> 0x1cf400
reg[1] = reg[3] -> 0xe82b18
reg[6] = reg[0] -> 0x1cf400
reg[7] = reg[1] -> 0xe82b18
Draw yellow cube with args: 0x000032, 0x0000e6, 0x1cf400, 0xe82b18
reg[0] = reg[8] -> 0x000032
reg[4] = reg[0] -> 0x000032
reg[0] = reg[4] -> 0x000032
reg[1] = 1000 -> 0x0003e8
reg[0](0x000032) -= reg[1](0x0003e8) -> -0x003b6
reg[4] = reg[0] -> -0x003b6
reg[0] = reg[9] -> 0x000032
reg[1] = 240 -> 0x0000f0
reg[0](0x000032) += reg[1](0x0000f0) -> 0x000122
reg[5] = reg[0] -> 0x000122
reg[0] = reg[5] -> 0x000122
reg[1] = 500 -> 0x0001f4
reg[0](0x000122) -= reg[1](0x0001f4) -> -0x000d2
reg[5] = reg[0] -> -0x000d2
reg[0] = reg[4] -> -0x003b6
reg[1] = reg[5] -> -0x000d2
Decode reg[0] and reg[1] -> 0x391faa, 0x6ee6d2
reg[6] = reg[0] -> 0x391faa
reg[7] = reg[1] -> 0x6ee6d2
Draw yellow cube with args: -0x003b6, -0x000d2, 0x391faa, 0x6ee6d2
reg[0] = reg[8] -> 0x000032
reg[4] = reg[0] -> 0x000032
reg[0] = reg[9] -> 0x000032
reg[1] = 300 -> 0x00012c
reg[0](0x000032) += reg[1](0x00012c) -> 0x00015e
reg[5] = reg[0] -> 0x00015e
reg[0] = reg[4] -> 0x000032
reg[1] = reg[5] -> 0x00015e
Decode reg[0] and reg[1] -> 0xd77de2, 0x0ebe72
reg[3] = reg[0] -> 0xd77de2
reg[0] = reg[1] -> 0x0ebe72
reg[1] = reg[3] -> 0xd77de2
reg[6] = reg[0] -> 0x0ebe72
reg[7] = reg[1] -> 0xd77de2
Draw yellow cube with args: 0x000032, 0x00015e, 0x0ebe72, 0xd77de2
reg[0] = reg[8] -> 0x000032
reg[1] = 60 -> 0x00003c
reg[0](0x000032) += reg[1](0x00003c) -> 0x00006e
reg[4] = reg[0] -> 0x00006e
reg[0] = reg[4] -> 0x00006e
reg[1] = 1000 -> 0x0003e8
reg[0](0x00006e) -= reg[1](0x0003e8) -> -0x0037a
reg[4] = reg[0] -> -0x0037a
reg[0] = reg[9] -> 0x000032
reg[1] = 180 -> 0x0000b4
reg[0](0x000032) += reg[1](0x0000b4) -> 0x0000e6
reg[5] = reg[0] -> 0x0000e6
reg[0] = reg[5] -> 0x0000e6
reg[1] = 500 -> 0x0001f4
reg[0](0x0000e6) -= reg[1](0x0001f4) -> -0x0010e
reg[5] = reg[0] -> -0x0010e
reg[0] = reg[4] -> -0x0037a
reg[1] = reg[5] -> -0x0010e
Decode reg[0] and reg[1] -> 0x071150, 0x74fc28
reg[6] = reg[0] -> 0x071150
reg[7] = reg[1] -> 0x74fc28
Draw yellow cube with args: -0x0037a, -0x0010e, 0x071150, 0x74fc28
reg[0] = reg[8] -> 0x000032
reg[1] = 120 -> 0x000078
reg[0](0x000032) += reg[1](0x000078) -> 0x0000aa
reg[4] = reg[0] -> 0x0000aa
reg[0] = reg[9] -> 0x000032
reg[1] = 180 -> 0x0000b4
reg[0](0x000032) += reg[1](0x0000b4) -> 0x0000e6
reg[5] = reg[0] -> 0x0000e6
reg[0] = reg[5] -> 0x0000e6
reg[1] = 500 -> 0x0001f4
reg[0](0x0000e6) -= reg[1](0x0001f4) -> -0x0010e
reg[5] = reg[0] -> -0x0010e
reg[0] = reg[4] -> 0x0000aa
reg[1] = reg[5] -> -0x0010e
Decode reg[0] and reg[1] -> 0xc42e8b, 0xdb3f17
reg[3] = reg[0] -> 0xc42e8b
reg[0] = reg[1] -> 0xdb3f17
reg[1] = reg[3] -> 0xc42e8b
reg[6] = reg[0] -> 0xdb3f17
reg[7] = reg[1] -> 0xc42e8b
Draw yellow cube with args: 0x0000aa, -0x0010e, 0xdb3f17, 0xc42e8b
reg[0] = reg[8] -> 0x000032
reg[1] = 180 -> 0x0000b4
reg[0](0x000032) += reg[1](0x0000b4) -> 0x0000e6
reg[4] = reg[0] -> 0x0000e6
reg[0] = reg[4] -> 0x0000e6
reg[1] = 1000 -> 0x0003e8
reg[0](0x0000e6) -= reg[1](0x0003e8) -> -0x00302
reg[4] = reg[0] -> -0x00302
reg[0] = reg[9] -> 0x000032
reg[1] = 180 -> 0x0000b4
reg[0](0x000032) += reg[1](0x0000b4) -> 0x0000e6
reg[5] = reg[0] -> 0x0000e6
reg[0] = reg[4] -> -0x00302
reg[1] = reg[5] -> 0x0000e6
Decode reg[0] and reg[1] -> 0x34cf2b, 0x331fcf
reg[6] = reg[0] -> 0x34cf2b
reg[7] = reg[1] -> 0x331fcf
Draw yellow cube with args: -0x00302, 0x0000e6, 0x34cf2b, 0x331fcf
reg[0] = reg[8] -> 0x000032
reg[1] = 60 -> 0x00003c
reg[0](0x000032) += reg[1](0x00003c) -> 0x00006e
reg[4] = reg[0] -> 0x00006e
reg[0] = reg[9] -> 0x000032
reg[1] = 60 -> 0x00003c
reg[0](0x000032) += reg[1](0x00003c) -> 0x00006e
reg[5] = reg[0] -> 0x00006e
reg[0] = reg[5] -> 0x00006e
reg[1] = 500 -> 0x0001f4
reg[0](0x00006e) -= reg[1](0x0001f4) -> -0x00186
reg[5] = reg[0] -> -0x00186
reg[0] = reg[4] -> 0x00006e
reg[1] = reg[5] -> -0x00186
Decode reg[0] and reg[1] -> 0xd9e5f1, 0xa59d19
reg[3] = reg[0] -> 0xd9e5f1
reg[0] = reg[1] -> 0xa59d19
reg[1] = reg[3] -> 0xd9e5f1
reg[6] = reg[0] -> 0xa59d19
reg[7] = reg[1] -> 0xd9e5f1
Draw yellow cube with args: 0x00006e, -0x00186, 0xa59d19, 0xd9e5f1
reg[0] = reg[8] -> 0x000032
reg[1] = 120 -> 0x000078
reg[0](0x000032) += reg[1](0x000078) -> 0x0000aa
reg[4] = reg[0] -> 0x0000aa
reg[0] = reg[4] -> 0x0000aa
reg[1] = 1000 -> 0x0003e8
reg[0](0x0000aa) -= reg[1](0x0003e8) -> -0x0033e
reg[4] = reg[0] -> -0x0033e
reg[0] = reg[9] -> 0x000032
reg[1] = 120 -> 0x000078
reg[0](0x000032) += reg[1](0x000078) -> 0x0000aa
reg[5] = reg[0] -> 0x0000aa
reg[0] = reg[4] -> -0x0033e
reg[1] = reg[5] -> 0x0000aa
Decode reg[0] and reg[1] -> 0xc42d8b, 0xcb5fe7
reg[3] = reg[0] -> 0xc42d8b
reg[0] = reg[1] -> 0xcb5fe7
reg[1] = reg[3] -> 0xc42d8b
reg[6] = reg[0] -> 0xcb5fe7
reg[7] = reg[1] -> 0xc42d8b
Draw yellow cube with args: -0x0033e, 0x0000aa, 0xcb5fe7, 0xc42d8b
...
reg[0] = reg[4] -> 0x0004e2
reg[1] = reg[5] -> 0x000122
Decode reg[0] and reg[1] -> 0x353730, 0x383434
reg[6] = reg[0] -> 0x353730
reg[7] = reg[1] -> 0x383434
Draw blue cube with args: 0x0004e2, 0x000122, 0x353730, 0x383434
reg[0] = reg[8] -> 0x0003b6
reg[1] = 360 -> 0x000168
reg[0](0x0003b6) += reg[1](0x000168) -> 0x00051e
reg[4] = reg[0] -> 0x00051e
reg[0] = reg[9] -> 0x000032
reg[1] = 240 -> 0x0000f0
reg[0](0x000032) += reg[1](0x0000f0) -> 0x000122
reg[5] = reg[0] -> 0x000122
reg[0] = reg[4] -> 0x00051e
reg[1] = reg[5] -> 0x000122
Decode reg[0] and reg[1] -> 0x6257a9, 0x9555e9
reg[6] = reg[0] -> 0x6257a9
reg[7] = reg[1] -> 0x9555e9
Draw blue cube with args: 0x00051e, 0x000122, 0x6257a9, 0x9555e9
reg[0] = reg[8] -> 0x0003b6
reg[1] = 420 -> 0x0001a4
reg[0](0x0003b6) += reg[1](0x0001a4) -> 0x00055a
reg[4] = reg[0] -> 0x00055a
reg[0] = reg[9] -> 0x000032
reg[1] = 240 -> 0x0000f0
reg[0](0x000032) += reg[1](0x0000f0) -> 0x000122
reg[5] = reg[0] -> 0x000122
reg[0] = reg[4] -> 0x00055a
reg[1] = reg[5] -> 0x000122
Decode reg[0] and reg[1] -> 0xd777af, 0xdf5bd3
reg[6] = reg[0] -> 0xd777af
reg[7] = reg[1] -> 0xdf5bd3
Draw blue cube with args: 0x00055a, 0x000122, 0xd777af, 0xdf5bd3

可以看到要绘制黄色方块计算坐标时有好几个方块的坐标都被减去了一个很大的数导致其变为了负数 并且第3, 4个参数是在每一次计算出当前要绘制的x, y坐标后通过opcode为4的一连串运算得到的 这一连串运算的参数就是x和y坐标 也就是说第3, 4个参数大概率是两个坐标的校验值

除此之外还能够观察出来在计算要绘制的黄色方块坐标时本来出现过一个比较正常像是坐标的值 但是这个值被减去了一个较大的数值导致其变为负数 而两个坐标都正常的情况下和蓝色方块校验值的计算过程相比较 这些黄色方块的校验值被交换了位置:

image-20250326101501344

这应该就是黄色方块没有被正确绘制的两个原因

接下来先验证一下想法的正确性 在python里看一下将负数修正后是否能正确绘制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
import matplotlib.pyplot as plt
import numpy as np

opcode = [
0x00000002, 0x00000008, 0x00000000, 0x00000002, 0x00000000, 0x00000004, 0x00000002, 0x00000004, 0x00000000, 0x00000003, ...
]
mask = ((1 << 32) - 1)
yellow_points = []
Blue_points = []
...
case 1:
res = f"reg[0]({reg[0] :#08x}) -= reg[1]({reg[1] :#08x}) -> {reg[0] - reg[1] :#08x}"
reg[0] -= reg[1]
if reg[0] < 0:
reg[0] += reg[1]
res += f', fix => {reg[0] :#08x}'
...
case 5:
res = f"Draw yellow cube with args: {reg[4]:#08x}, {reg[5]:#08x}, {reg[6]:#08x}, {reg[7]:#08x}"
yellow_points.append((reg[4], reg[5]))
case 6:
res = f"Draw blue cube with args: {reg[4]:#08x}, {reg[5]:#08x}, {reg[6]:#08x}, {reg[7]:#08x}"
Blue_points.append((reg[4], reg[5]))
...
yellow_points = [(x, -y) for x, y in yellow_points]
Blue_points = [(x, -y) for x, y in Blue_points]
x, y = zip(*yellow_points)
plt.scatter(x, y, label="Yellow Points", color='red')
x, y = zip(*Blue_points)
plt.scatter(x, y, label="Blue Points", color='blue')
plt.legend()
plt.title('Points')
plt.show()

运行结果:

image-20250326110820053

可以看到已经正确绘制了 那么接下来只需要记录一下正确的位置和校验值参数并调整被交换的校验值就能绘制flag了 记录得到:

1
2
3
4
5
x:50, 50, 50, 50, 50, 50, 110, 170, 230, 110, 170
y:50, 110, 170, 230, 290, 350, 230, 230, 230, 110, 170
arg3: 0xf814b4, 0x1bcd69, 0x87a34b, 0x1cf400, 0x391faa, 0xebe72, 0x71150, 0xc7371b, 0x34cf2b, 0xd1b52d, 0xb36fdf
arg4: 0x130bd0, 0x7515c9, 0xd91997, 0xe82b18, 0x520efe, 0xd77de2, 0x788404, 0xc42e8b, 0x5b8fe7, 0xd9e5f1, 0xc42d8b
swap: 0, 2, 3, 5, 7, 9, 10

下面介绍通过DetoursAPI进行DLL编写和函数hook的过程

编写DLL进行hook

安装Detours库

可以直接去官方仓库下载发行版本 也可以通过vcpkg拉取 这里讲一下vcpkg安装时踩的坑

vcpkg第一次启动时要安装一些对版本有要求的组件 包括:

image-20250326112553229

而它自带的下载功能会列出下载文件的URL并检查所下载的文件的SHA256 其中我的vcpkg在下载powershell的时候就报了校验失败的错 这种情况直接去它列出的URL那里手动下载并放在vcpkg\downloads目录下即可

初始化完成后执行vcpkg install detours即可拉取到detours库

配置项目

IDE这里选择使用Visual Studio 2022 Community 新建项目时选择C++编写的动态链接库:

image-20250326113306838

正式开始编写dll前需要先配置好项目的包含目录和库目录 项目->xxx和属性->VC++目录->包含目录, 库目录

因为hook基本上必须要用到WindowsAPI 所以要把Windows Kits\10\Include\%VERSION%\um也加入到包含目录中(版本替换成自己的) :

image-20250326113845140

同理在库目录中添加detours.lib的路径:

image-20250326113928334

这里再讲一下踩到的第二个坑

如果包含目录中添加了Windows Kits在编写代码时仍然不能包含windows.h的话可以检查一下是否除了通过Visual Studio Installer安装Windows Kits还手动安装了(用Everything搜索windows.h看看是否除了VS2022目录下还存在别的Windows Kits) 如果是的话通过设置->应用卸载所有Windows Kits

接下来我选择的是手动下载一个Kits 这时候应该也能再通过VSInstaller安装

完成后再编辑包含目录将新安装的Kits下的um给添加进去应该就能包含windows.h

编写DLL

使用detours进行目标进程函数的hook基本上是这个模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
void Start_Hook(){
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach((PVOID*)&pto_hook, my_hook);
DetourTransactionCommit();
}
void Exit_Hook(){
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)bridge, (PVOID)pto_hook);
DetourTransactionCommit();
}

其中pto_hook是目标进程中要hook的函数的虚拟地址 my_hook为要将pto_hook替换掉的函数指针

在本题中 要hook的函数是绘制方块的函数 目标是当检测到它绘制的是黄色方块时将前4个参数替换成正确的参数

问题是绘制函数是通过shellcode解码并映射到一块随机的内存中的 我们还需要获取绘制函数的地址 现在我们知道作为shellcode入口点的函数会储存到data段中:

image-20250326131412136

那么只需要获取进程中的这块内存并加上一个偏移就能获取绘制函数的虚拟地址了 经过调试可以获得这个偏移为-0x650

接下来就像pwn中ret2libc的方法来计算绘制函数的虚拟地址:

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
__int64 dec_func = 0x8318, pto_hook;
FILE* log_file = fopen("log_.txt", "a+");

BOOL Start_Hook() {
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
__int64 to_hook = 0;

HMODULE ProcessBase = GetModuleHandle(NULL);
BYTE buffer[100];
memset(buffer, 0, 100);
sprintf((char*)buffer, "ProcessBase: 0x%llx\n", (__int64)ProcessBase);
fwrite((char*)buffer, 1, strlen((char*)buffer), log_file);
dec_func += (__int64)ProcessBase;

if (!ReadProcessMemory(GetCurrentProcess(), (LPCVOID*)dec_func, &to_hook, sizeof(__int64), NULL)) {
__int64 err = GetLastError();
BYTE buffer[100];
memset(buffer, 0, 100);
sprintf((char*)buffer, "ReadProcessMemory failed, error code: %llx\n", err);
fwrite((char*)buffer, 1, strlen((char*)buffer), log_file);
fclose(log_file);
return FALSE;
}

pto_hook = (to_hook - 0x650);
memset(buffer, 0, 100);
sprintf((char*)buffer, "to_hook: 0x%llx\n", pto_hook);
fwrite((char*)buffer, 1, strlen((char*)buffer), log_file);
return TRUE;
}

使用GetModuleHandle(NULL)可以获取程序映射到虚拟内存中的基址 使用ReadProcessMemory()可以获取一个地址中的数据

这里再讲一个踩到的坑 在跟着网上的wp复现时看到读取的内存地址直接就是0x140008318 应该是因为机器没开ASLR 正常情况下需要先获取真正的虚拟地址才能读到数据

PS | 2025/3/30

实际上并不是机器的问题 Windows对于程序本身映射到虚拟内存中的基址和Linux中的PIE一样只需要修改二进制文件本身就行了 对于PE文件 使用010的EXE.bt模板修改以下程序头的字段即可使其以固定的基址被载入虚拟内存:

NtHeader->OptionalHeader->DllCharacteristics->IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE : 1

将字段设置为0即可关闭随机映射

至此我们获取到了要hook的函数地址:

image-20250326132625777

接下来就是编写自己的函数来hook绘制函数 我们需要先定义一个目标函数的函数指针类型来方便后续调用原本的绘制函数:

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
typedef __int64 (* pfunc)(__int64, __int64, __int64, __int64, int, __int64, __int64, __int64, __int64, __int64);
pfunc pto_hook;

__int32 Correct_y_pos[] = { 50, 110, 170, 230, 290, 350, 230, 230, 230, 110, 170 },
Correct_x_pos[] = { 50, 50, 50, 50, 50, 50, 110, 170, 230, 110, 170 },
Correct_a3[] = { 0xf814b4, 0x1bcd69, 0x87a34b, 0x1cf400, 0x391faa, 0xebe72, 0x71150, 0xc7371b, 0x34cf2b, 0xd1b52d, 0xb36fdf },
Correct_a4[] = { 0x130bd0, 0x7515c9, 0xd91997, 0xe82b18, 0x520efe, 0xd77de2, 0x788404, 0xc42e8b, 0x5b8fe7, 0xd9e5f1, 0xc42d8b },
Switch[] = { 0, 2, 3, 5, 7, 9, 10 },
count = 0;

__int64 __fastcall my_hook(__int64 a1, __int64 a2, __int64 a3, __int64 a4, int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10) {
if (a5 == 0xFFFFFF00) {
if (count % 11 == 0) {
count = 0;
}
a1 = Correct_x_pos[count];
a2 = Correct_y_pos[count];
a3 = Correct_a3[count];
a4 = Correct_a4[count];
for (int idx = 0; idx < 7; idx++) {
if (count == Switch[idx]) {
a3 ^= a4;
a4 ^= a3;
a3 ^= a4;
break;
}
}
count++;
}
return pto_hook(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
}

然后调用detours中的hook函数来将绘制函数替换为自己的函数:

1
2
DetourAttach((PVOID*)&pto_hook, my_hook);
DetourTransactionCommit();

简单通过研究这个hook的原理来解释一下如何调试编译出的dll

调试DLL

先在dll中下好断点:

image-20250326133448888

接下来启动宿主程序并附加调试到宿主程序:

image-20250326133753016

用注入器(这里选择Xenos)将dll注入到宿主程序中即可开始调试

执行完DetourTransactionCommit()提交了修改后查看绘制函数:

image-20250326134748667

可以看到函数头被修改为了跳转到我们编写的用户函数的字节码

image-20250326134904783

执行完后会跳转到一块注入器创建的内存执行被覆盖的函数头然后跳转回绘制函数:

image-20250326135024510

注入DLL

用上面提到的Xenos进行dll注入 在Advanced中设置(其实是默认的)注入的方法为Native inject方可进行上述的调试 使用Manual map的方法无法进行上述方法的调试

最后附上完整dll代码以及成果:

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
#include "pch.h"
#include <Windows.h>
#include <detours.h>
#include <string.h>
#include <stdio.h>

#pragma comment(lib, "detours.lib")
#define _CRT_SECURE_NO_WARNINGS 1

typedef __int64 (* pfunc)(__int64, __int64, __int64, __int64, int, __int64, __int64, __int64, __int64, __int64);
pfunc pto_hook;
__int64 dec_func = 0x8318;
FILE* log_file = fopen("log_.txt", "a+");
__int32 Correct_y_pos[] = { 50, 110, 170, 230, 290, 350, 230, 230, 230, 110, 170 },
Correct_x_pos[] = { 50, 50, 50, 50, 50, 50, 110, 170, 230, 110, 170 },
Correct_a3[] = { 0xf814b4, 0x1bcd69, 0x87a34b, 0x1cf400, 0x391faa, 0xebe72, 0x71150, 0xc7371b, 0x34cf2b, 0xd1b52d, 0xb36fdf },
Correct_a4[] = { 0x130bd0, 0x7515c9, 0xd91997, 0xe82b18, 0x520efe, 0xd77de2, 0x788404, 0xc42e8b, 0x5b8fe7, 0xd9e5f1, 0xc42d8b },
Switch[] = { 0, 2, 3, 5, 7, 9, 10 },
count = 0;

__int64 __fastcall my_hook(__int64 a1, __int64 a2, __int64 a3, __int64 a4, int a5, __int64 a6, __int64 a7, __int64 a8, __int64 a9, __int64 a10) {
if (a5 == 0xFFFFFF00) {
if (count % 11 == 0) {
count = 0;
}
a1 = Correct_x_pos[count];
a2 = Correct_y_pos[count];
a3 = Correct_a3[count];
a4 = Correct_a4[count];
for (int idx = 0; idx < 7; idx++) {
if (count == Switch[idx]) {
a3 ^= a4;
a4 ^= a3;
a3 ^= a4;
break;
}
}
count++;
}
return pto_hook(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
}

BOOL Start_Hook() {
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
__int64 to_hook = 0;

HMODULE ProcessBase = GetModuleHandle(NULL);
BYTE buffer[100];
memset(buffer, 0, 100);
sprintf((char*)buffer, "ProcessBase: 0x%llx\n", (__int64)ProcessBase);
fwrite((char*)buffer, 1, strlen((char*)buffer), log_file);
dec_func += (__int64)ProcessBase;

if (!ReadProcessMemory(GetCurrentProcess(), (LPCVOID*)dec_func, &to_hook, sizeof(__int64), NULL)) {
__int64 err = GetLastError();
BYTE buffer[100];
memset(buffer, 0, 100);
sprintf((char*)buffer, "ReadProcessMemory failed, error code: %llx\n", err);
fwrite((char*)buffer, 1, strlen((char*)buffer), log_file);
fclose(log_file);
return FALSE;
}

pto_hook = (pfunc)(to_hook - 0x650);
memset(buffer, 0, 100);
sprintf((char*)buffer, "to_hook: 0x%llx\n", (__int64)pto_hook);
fwrite((char*)buffer, 1, strlen((char*)buffer), log_file);
DetourAttach((PVOID*)&pto_hook, my_hook);
DetourTransactionCommit();
return TRUE;
}

BOOL Exit_Hook() {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)dec_func, (PVOID)pto_hook);
DetourTransactionCommit();
fclose(log_file);
return TRUE;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Start_Hook();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
Exit_Hook();
break;
}
return TRUE;
}

QQ_1742968537073