0%

C/C++反逆向技巧鉴赏

记录一下比赛中遇到的各种C++的反逆向技巧(主要是Windows API)

反调试技巧

NtQueryInformationProcess() / ZwQueryInformationProcess()

原理

两个都是Windows中检查并获取进程信息的函数 区别在于调用方式和调用权限 Zw需要在内核态调用 Nt在用户态调用 主要起到反调试作用的是第二个参数ProcessInformationClass

当该值为ProcessDebugPort(0x07)时返回缓冲区为-1(调试状态), 0(非调试状态)

当该值为ProcessDebugObjectHandle(0x1E)时返回缓冲区为一个调试对象句柄!=NULL(调试状态), NULL(非调试状态)

当该值为ProcessDebugFlags(0x1F)时返回缓冲区为0(调试状态), 1(非调试状态)

例子

LineCTF/rev/BrownFlagChecker

其中有一个函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool anti_debug()
{
void (__fastcall *SystemRoutineAddress)(__int64, __int64, __int64 *); // rax
struct _UNICODE_STRING SystemRoutineName; // [rsp+30h] [rbp-18h] BYREF
__int64 v3; // [rsp+50h] [rbp+8h] BYREF

*&SystemRoutineName.Length = 0x340032;
SystemRoutineName.Buffer = L"ZwQueryInformationProcess";
SystemRoutineAddress = MmGetSystemRoutineAddress(&SystemRoutineName);
if ( !SystemRoutineAddress )
return 1;
SystemRoutineAddress(-1i64, 7i64, &v3);
return v3 != 0;
}

反制措施

直接patch掉整个函数的调用或者修改控制流使得用于检测调试的信息失效

NtSetInformationThread() / ZwSetInformationThread()

两者的分别与上述差不多 功能是设置信息而不是检查信息 当第二个参数为ThreadHideFromDebugger(0x11)时将附加的调试器取消

例子

XCTF/rev/Destination

其中一个预处理函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void *__thiscall sub_413750(void *this)
{
HANDLE CurrentThread; // eax
FARPROC ProcAddress; // [esp+D0h] [ebp-2Ch]
HMODULE hModule; // [esp+DCh] [ebp-20h]
int i; // [esp+E8h] [ebp-14h]
int j; // [esp+E8h] [ebp-14h]
int k; // [esp+E8h] [ebp-14h]

__CheckForDebuggerJustMyCode(&unk_4250E0);
for ( i = 0; i < 5; ++i )
ModuleName[i] = (ModuleName[i] - 100) ^ 0x55;
for ( j = 0; j < 22; ++j )
aS[j] = (aS[j] - 100) ^ 0x55;
for ( k = 0; k < 18; ++k )
byte_423020[k] = (byte_423020[k] - 100) ^ 0x55;
hModule = GetModuleHandleA(ModuleName);
ProcAddress = GetProcAddress(hModule, aS);
CurrentThread = GetCurrentThread();
(ProcAddress)(CurrentThread, 0x11, 0, 0);
return this;
}

在创建线程处下断点调试到此处 可以看到aS数组储存的实际上是ZwSetInformationThread 而且第二个参数是0x11 这时在线程启动时调试器会被立刻取消

反制措施

将第二个参数patch为0

虚表 hook

C++类中各种成员在内存中的分布

以以下代码编译出的二进制文件为例:

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
#include <iostream>

using namespace std;

class vmt_test{
public:
int var;
vmt_test(){
var = 10;
cout << "Test constructor" << endl;
}
virtual void function1_To_hook(){
cout << "Function1 in VMT, Havnt been hooked" << endl;
}
virtual void function2(){
cout << "Function2 in VMT" << endl;
}
void function3(){
cout << "Function3 not in VMT" << endl;
}
};

class vmt_drivation : public vmt_test{
public:
void function1_To_hook(){
cout << "Function1 in Drivation, Havnt been hooked" << endl;
}
};

void hook(){
cout << "Hooked" << endl;
}

int main(){
vmt_test* test = new vmt_test();
test->function1_To_hook();
test->function2();
vmt_test* test2 = new vmt_test();
test2->function3();
vmt_drivation* drv = new vmt_drivation();
drv->function1_To_hook();
return 0;
}

非虚成员函数

和普通函数一样没有区别 在.text段实现 不被除了调用处以外的地方引用:

image-20240915093322380

虚成员函数

.text段实现 不会被调用处引用外且排列在VMT 即虚函数表中 在调用该函数时通过查表调用:

image-20240915094620479

image-20240915094223312

虚函数表相当于一个特殊的类成员变量 初始化时被赋值给实例的类对象偏移为0的位置:

image-20240915094410949

继承类的虚函数

新建一张虚函数表 对已经实现的虚函数进行替换 没有实现的虚函数则引用原虚函数表中的对应函数 如果继承类没有自己的构造函数则调用基类的构造函数中将基类的VMT赋给偏移为0的内存 再在构造函数后用新的VMT覆盖:

image-20240915095120334

成员变量

就像作为成员变量的VMT一样在构造函数中被依次赋值给偏移为n * sizeof(void *)的内存:

image-20240915095435528

Hook虚函数

由于虚函数的调用都是间接的 而且VMT的地址和类的地址是绑定的 只需要将VMT中要hook的虚函数的地址替换为用户函数就能轻松实现hook 用以下代码编译出的二进制文件为例(编译时启用-fpermissive选项):

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
#include <windows.h>
#include <iostream>

using namespace std;

class vmt_test{
public:
int var;
vmt_test(){
var = 10;
cout << "Test constructor" << endl;
}
virtual void function1_To_hook(){
cout << "Function1 in VMT, Havnt been hooked" << endl;
}
virtual void function2(){
cout << "Function2 in VMT" << endl;
}
void function3(){
cout << "Function3 not in VMT" << endl;
}
};

class vmt_drivation : public vmt_test{
public:
void function1_To_hook(){
cout << "Function1 in Drivation, Havnt been hooked" << endl;
}
};

void hook(){
cout << "Hooked" << endl;
}

int main(){
vmt_test* test = new vmt_test();
test->function1_To_hook();
test->function2();
int * Start_address = *(int **)test;
DWORD old_protect;
if(VirtualProtect((LPVOID)Start_address, sizeof(void *), PAGE_EXECUTE_READWRITE, &old_protect)){
// cout << "Hooking" << endl;
*(int *)Start_address = (int)hook;
VirtualProtect((LPVOID)Start_address, sizeof(void *), old_protect, &old_protect);
}
else{
cout << "Failed to hook" << endl;
}
vmt_test* test2 = new vmt_test();
test2->function1_To_hook();
vmt_drivation* drv = new vmt_drivation();
drv->function1_To_hook();
return 0;
}

执行结果:

1
2
3
4
5
6
7
8
.\test.exe
Test constructor
Function1 in VMT, Havnt been hooked
Function2 in VMT
Test constructor
Hooked
Test constructor
Function1 in Drivation, Havnt been hooked

特征

要使用VMT hook绕不开的一点就是改变某段内存的权限 因为VMT所在的数据段没有写入的权限 这时候在Windows系统上要使用VirtualProtect()进行提权 Linux上使用mprotect()进行提权

[2024 DASCTF八月开学季] ezcpp

以这题为例 它的VMT hook就非常的明显:

image-20240915101509799

image-20240915101527913

两次hook了v7的VMT中的第一个虚函数

C++异常处理

try{…}catch(…){…}

最简单的一种异常处理类型 IDA可以轻松识别出try{}块和对应的catch(){}块 以以下代码编译出的二进制文件为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdexcept>
#include <iostream>

using namespace std;

int main(){
try{
int num;
cin >> num;
if(num){
cout << "10000 / input = " << (10000 / num) << endl;
}
else{
throw 0x9961;
}
}
catch(...){
cout << "Divided by zero!!!" << endl;
}
return 0;
}

image-20240915224827971

但是IDA的伪代码构造过程并不会将异常处理的部分进行反汇编并构造try-catch块 只会构造try的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
__int64 v4; // rax
_DWORD *exception; // rax
_DWORD v7[5]; // [rsp+2Ch] [rbp-4h] BYREF

_main();
std::istream::operator>>(refptr__ZSt3cin, v7);
if ( !v7[0] )
{
exception = _cxa_allocate_exception(4uLL);
*exception = 0x9961;
_cxa_throw(exception, refptr__ZTIi, 0LL);
}
v3 = std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "10000 / input = ");
v4 = std::ostream::operator<<(v3, (unsigned int)(10000 / v7[0]));
std::ostream::operator<<(v4, refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);
return 0;
}

应对的方法很简单 将throw关键字对应的汇编片段直接patch为jmp catch_block即可:

image-20240915225312007

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
__int64 v4; // rax
_DWORD *exception; // rax
__int64 v7; // rax
_DWORD v8[5]; // [rsp+2Ch] [rbp-4h] BYREF

_main();
std::istream::operator>>(refptr__ZSt3cin, v8);
if ( v8[0] )
{
v3 = std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "10000 / input = ");
v4 = std::ostream::operator<<(v3, (unsigned int)(10000 / v8[0]));
std::ostream::operator<<(v4, refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);
}
else
{
exception = _cxa_allocate_exception(4uLL);
*exception = 0x9961;
_cxa_begin_catch(exception);
v7 = std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "Divided by zero!!!");
std::ostream::operator<<(v7, refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);
_cxa_end_catch();
}
return 0;
}

但是这种通过异常处理隐藏控制流的方法局限性是很大的 第一点就是用throw关键字抛出异常这个特征太明显 而try-catch结构只能捕捉到throw关键字抛出的异常 其他的诸如内存错误和除零错误等是不会进行异常处理的 这就要提到下面这种异常处理方式

__try{…}__except(…){…} SEH

这是一种基于TEB(线程结构)的异常处理方式 具体可以参考结构化异常SEH处理机制详细介绍 可以捕获到出现的所有异常 以以下代码编译出的二进制文件为例:

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

LONG WINAPI Handlers(EXCEPTION_POINTERS* pExInfo){
switch(pExInfo->ExceptionRecord->ExceptionCode){
case EXCEPTION_ACCESS_VIOLATION:
printf("Access Violation\n");
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
printf("Divide by Zero\n");
break;
default:
printf("Unknown Exception\n");
break;
}
return EXCEPTION_EXECUTE_HANDLER;
}

int main(){
// SetUnhandledExceptionFilter(Handlers);
__try{
int zero = 0;
int num = 1 / zero;
}
__except (Handlers(GetExceptionInformation())){
printf("Exception Handled\n");
}
return 0;
}

安装Microsoft C++扩展工具后 用__except关键字可以指定发生异常时的异常处理函数 当__try块中发生异常时就会进入这个异常处理函数并将发生的异常号传入pExInfo->ExceptionRecord->ExceptionCode 和上面的异常处理一样 这种方法可以隐藏程序的控制流:

1
2
3
4
5
__int64 __fastcall main()
{
j___CheckForDebuggerJustMyCode(&_D589061D_excption_cpp);
return 0LL;
}

image-20240916140054838

而且也和上面的异常处理一样在发生异常时即使是步入调试也会直接运行到下一个断点或程序结束 这时候要恢复控制流就需要识别哪部分是一定会发生异常的 并将那部分patch为调用异常处理函数 和跳转到__except块的jmp指令 但是这样做有明显的弊端:

  1. 不一定能确定那一部分会发生异常
  2. 会发生异常的部分不一定每次都发生异常
  3. 原来会触发异常的部分不一定有足够的空间用来patch成两条指令

所以目前我能想到比较好的处理方式是在异常处理函数的起始地址下断点来动态调试分析 而IDA是可以轻松识别出__except(handler())中的handler()

最后附上所有Windows异常状态码

Debug-Blocker

实际上Debugblocker的思想非常简单 同时特征也十分明显 就是检测让程序启动并调试一个和自己一样的线程 这时候虽然两个线程运行的是同一个程序 但是主进程和子进程因为调试器附加判断(IsDebuggerPresent())执行的是完全不同的程序 而通过获取上下文 主进程对子进程的控制流是完全控制的 而子进程又可以通过触发异常来交由主进程处理的方式形成主进程的控制流 最后达成隐藏控制流的目的 同时因为子进程已经被附加了主进程这个调试器 正常来说是不可能再附加调试器了 从而进一步增加了逆向难度

用以下代码编译出的二进制文件为例:

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
#include <iostream>
#include <windows.h>
#include <stdlib.h>
using namespace std;

int zero = 0x100;
CHAR Buffer[0x1000];
HANDLE hThread;

void child(){
int z = zero - 0x100;
int get_input = 10 / z;
int check = 100 / z;
int res = 1000 / z;
int exit = 100000 / z;
ExitProcess(0);
}

bool handler(DEBUG_EVENT *DebugEvent){
if(DebugEvent->dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT){
cout << "[+]Successfully attached to child thread" << endl;
}
CONTEXT ctx;
if(DebugEvent->u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO){
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(hThread, &ctx);
switch(ctx.Rax){
case 10:{
cout << "[+]Get input:";
cin >> Buffer;
ctx.Rip += 3;
break;
}
case 100:{
cout << "[+]Checking..." << endl;
if(memcmp(Buffer, "test", 4) == 0){
ctx.Rax = 1000;
}else{
ctx.Rax = 10000;
}
ctx.Rip += 11;
break;
}
case 1000:{
cout << "[+]Correct!" << endl;
ctx.Rip += 3;
break;
}
case 10000:{
cout << "[+]Incorrect!" << endl;
ctx.Rip += 3;
break;
}
case 100000:{
cout << "[-]Terminal" << endl;
ctx.Rip += 2;
return true;
}
default:{
cout << "[!]Unknown op" << endl;
break;
}
}
SetThreadContext(hThread, &ctx);
}
return false;
}

int main(){
if(IsDebuggerPresent()){
child();
}
STARTUPINFOA si;
si.cb = sizeof(si);
PROCESS_INFORMATION pi;
CHAR SelfPath[0x100];
GetModuleFileNameA(NULL, SelfPath, sizeof(SelfPath));
DEBUG_EVENT DebugEvent;
if(CreateProcessA(SelfPath, NULL, NULL, NULL, FALSE, DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi)){
hThread = pi.hThread;
while(WaitForDebugEvent(&DebugEvent, INFINITE)){
if(handler(&DebugEvent)){
break;
}
ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_CONTINUE);
}
ExitProcess(0);
}
puts("[!]Failed to create process");
ExitProcess(-1);
}

启动线程时会直接触发一个CREATE_PROCESS_DEBUG_EVENT(0x3)的事件 当子进程触发除零异常时handler()就会获取当时的上下文并根据RAX(除数)来进行对应的处理 并在处理完后更改上下文中的RAX, RIP并设置:

image-20240928120734385

运行结果:

image-20240928121133432

动态调试子线程的方法也很简单 Cheat Engine有一个DBVM功能 简单来说就是一个极简化的虚拟机 在里面运行Windows程序可以让CE的调试器附加到任意线程上 且不影响原程序的执行

[2024 LineCTF] BrownFlagChecker

直接看伪代码:

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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
void __noreturn child()
{
HANDLE FileA; // rax
int v1; // edx
SOCKET v2; // rcx
bool v3; // zf
SOCKET v4; // rcx
int v5; // eax
int v6; // edx
__int64 out; // [rsp+40h] [rbp-28h] BYREF
__int64 OutBuffer; // [rsp+48h] [rbp-20h] BYREF

FileA = CreateFileA("\\\\.\\BrownProtectorDeviceLink", 0xC0000000, 3u, 0LL, 3u, 4u, 0LL);
hDevice = FileA;
if ( FileA != (HANDLE)-1LL )
{
if ( DeviceIoControl(FileA, 0x224004u, 0LL, 0, &OutBuffer, 8u, 0LL, 0LL) )
{
if ( OutBuffer == 0x1337 )
{
zero = 1 / zero;
out = 0LL;
if ( DeviceIoControl(hDevice, 0x22400Cu, 0LL, 0, &out, 8u, 0LL, 0LL) )
{
if ( out != 0xDEAD )
{
v3 = !vm();
v5 = 2;
if ( v3 )
v5 = 3;
v6 = v5 % zero;
zero = v5 / zero;
shutdown(v4, v6);
}
}
}
}
}
shutdown(v2, v1);
}

char __fastcall handler(_DEBUG_EVENT *debug_event)
{
__int128 v1; // xmm1
__int128 v2; // xmm0
__int128 v3; // xmm1
__int128 v4; // xmm0
__int128 v5; // xmm1
__int128 v6; // xmm0
__int128 v7; // xmm1
__int128 v8; // xmm0
__int128 v9; // xmm1
int v10; // ebx
HANDLE FileA; // rax
__int64 v12; // rdi
_BYTE *v13; // rax
_OWORD *v14; // rax
_OWORD *v15; // rcx
__int64 lenth; // rax
__int64 OutBuffer; // [rsp+40h] [rbp-C0h] BYREF
CONTEXT Context; // [rsp+50h] [rbp-B0h] BYREF
_OWORD v20[4]; // [rsp+520h] [rbp+420h] BYREF
_OWORD v21[4]; // [rsp+560h] [rbp+460h] BYREF
_OWORD v22[4]; // [rsp+5A0h] [rbp+4A0h] BYREF
_OWORD v23[4]; // [rsp+5E0h] [rbp+4E0h] BYREF
_OWORD v24[4]; // [rsp+620h] [rbp+520h] BYREF
_OWORD v25[4]; // [rsp+660h] [rbp+560h] BYREF
_OWORD v26[4]; // [rsp+6A0h] [rbp+5A0h] BYREF
_OWORD v27[4]; // [rsp+6E0h] [rbp+5E0h] BYREF
_OWORD v28[4]; // [rsp+720h] [rbp+620h] BYREF
_OWORD v29[4]; // [rsp+760h] [rbp+660h] BYREF
_OWORD v30[4]; // [rsp+7A0h] [rbp+6A0h] BYREF
_OWORD v31[4]; // [rsp+7E0h] [rbp+6E0h] BYREF
_OWORD v32[4]; // [rsp+820h] [rbp+720h] BYREF
_OWORD v33[4]; // [rsp+860h] [rbp+760h] BYREF
_OWORD v34[4]; // [rsp+8A0h] [rbp+7A0h] BYREF
_OWORD v35[4]; // [rsp+8E0h] [rbp+7E0h] BYREF
_OWORD v36[4]; // [rsp+920h] [rbp+820h] BYREF
_OWORD v37[4]; // [rsp+960h] [rbp+860h] BYREF
_OWORD v38[4]; // [rsp+9A0h] [rbp+8A0h] BYREF
_OWORD v39[4]; // [rsp+9E0h] [rbp+8E0h] BYREF

v1 = *(_OWORD *)&debug_event->u.Exception.ExceptionRecord.ExceptionCode;
*(_OWORD *)&Context.P1Home = *(_OWORD *)&debug_event->dwDebugEventCode;
v2 = *((_OWORD *)&debug_event->u.RipInfo + 1);
*(_OWORD *)&Context.P3Home = v1;
v3 = *((_OWORD *)&debug_event->u.RipInfo + 2);
*(_OWORD *)&Context.P5Home = v2;
v4 = *((_OWORD *)&debug_event->u.RipInfo + 3);
*(_OWORD *)&Context.ContextFlags = v3;
v5 = *((_OWORD *)&debug_event->u.RipInfo + 4);
*(_OWORD *)&Context.SegGs = v4;
v6 = *((_OWORD *)&debug_event->u.RipInfo + 5);
*(_OWORD *)&Context.Dr1 = v5;
v7 = *((_OWORD *)&debug_event->u.RipInfo + 7);
*(_OWORD *)&Context.Dr3 = v6;
*(_OWORD *)&Context.Dr7 = *((_OWORD *)&debug_event->u.RipInfo + 6);
v8 = *((_OWORD *)&debug_event->u.RipInfo + 8);
*(_OWORD *)&Context.Rcx = v7;
v9 = *((_OWORD *)&debug_event->u.RipInfo + 9);
*(_OWORD *)&Context.Rbx = v8;
*(_OWORD *)&Context.Rbp = v9;
if ( LODWORD(Context.P1Home) != 1 )
{
if ( LODWORD(Context.P1Home) == 3 )
{
hProcess = (HANDLE)Context.P4Home;
hThread = (HANDLE)Context.P5Home;
if ( register() )
{
v10 = 0;
FileA = CreateFileA("\\\\.\\BrownProtectorDeviceLink", 0xC0000000, 3u, 0LL, 3u, 4u, 0LL);
hObject = FileA;
if ( FileA != (HANDLE)-1LL
&& DeviceIoControl(FileA, 0x224000u, 0LL, 0, &OutBuffer, 8u, 0LL, 0LL)
&& OutBuffer == 0x1337 )
{
v20[0] = _mm_load_si128((const __m128i *)&xmmword_140005BD0);
v21[0] = _mm_load_si128((const __m128i *)&xmmword_140005B20);
v22[0] = _mm_load_si128((const __m128i *)&xmmword_140005C10);
v23[0] = _mm_load_si128((const __m128i *)&xmmword_140005BE0);
v24[0] = _mm_load_si128((const __m128i *)&xmmword_140005B00);
v25[0] = _mm_load_si128((const __m128i *)&xmmword_140005AC0);
v26[0] = _mm_load_si128((const __m128i *)&xmmword_140005C30);
v27[0] = _mm_load_si128((const __m128i *)&xmmword_140005BA0);
v28[0] = _mm_load_si128((const __m128i *)&xmmword_140005B80);
v29[0] = _mm_load_si128((const __m128i *)&xmmword_140005BC0);
v30[0] = _mm_load_si128((const __m128i *)&xmmword_140005BB0);
v31[0] = _mm_load_si128((const __m128i *)&xmmword_140005B30);
v32[0] = _mm_load_si128((const __m128i *)&xmmword_140005B60);
v33[0] = _mm_load_si128((const __m128i *)&xmmword_140005AD0);
v34[0] = _mm_load_si128((const __m128i *)&xmmword_140005C40);
v35[0] = _mm_load_si128((const __m128i *)&xmmword_140005AE0);
memset(&v20[1], 0, 48);
memset(&v21[1], 0, 48);
memset(&v22[1], 0, 48);
memset(&v23[1], 0, 48);
memset(&v24[1], 0, 48);
memset(&v25[1], 0, 48);
memset(&v26[1], 0, 48);
memset(&v27[1], 0, 48);
memset(&v28[1], 0, 48);
memset(&v29[1], 0, 48);
memset(&v30[1], 0, 48);
memset(&v31[1], 0, 48);
memset(&v32[1], 0, 48);
memset(&v33[1], 0, 48);
memset(&v34[1], 0, 48);
memset(&v35[1], 0, 48);
Context.P1Home = (DWORD64)v20;
v36[0] = _mm_load_si128((const __m128i *)&xmmword_140005B70);
v12 = 0LL;
Context.P2Home = (DWORD64)v21;
Context.P3Home = (DWORD64)v22;
Context.P4Home = (DWORD64)v23;
Context.P5Home = (DWORD64)v24;
Context.P6Home = (DWORD64)v25;
*(_QWORD *)&Context.ContextFlags = v26;
*(_QWORD *)&Context.SegCs = v27;
*(_QWORD *)&Context.SegGs = v28;
Context.Dr0 = (DWORD64)v29;
Context.Dr1 = (DWORD64)v30;
Context.Dr2 = (DWORD64)v31;
Context.Dr3 = (DWORD64)v32;
Context.Dr6 = (DWORD64)v33;
Context.Dr7 = (DWORD64)v34;
Context.Rax = (DWORD64)v35;
Context.Rcx = (DWORD64)v36;
v37[0] = _mm_load_si128((const __m128i *)&xmmword_140005B40);
Context.Rdx = (DWORD64)v37;
memset(&v36[1], 0, 48);
Context.Rbx = (DWORD64)v38;
Context.Rsp = (DWORD64)v39;
memset(&v37[1], 0, 48);
v38[0] = _mm_load_si128((const __m128i *)&xmmword_140005B10);
memset(&v38[1], 0, 48);
v39[0] = _mm_load_si128((const __m128i *)&xmmword_140005C00);
v39[1] = _mm_load_si128((const __m128i *)&xmmword_140005C20);
v39[2] = _mm_load_si128((const __m128i *)&xmmword_140005AF0);
v39[3] = _mm_load_si128((const __m128i *)&xmmword_140005BF0);
while ( 1 )
{
v13 = VirtualAlloc(0LL, 0x800uLL, 0x3000u, 4u);
(&in)[v12] = v13;
if ( !v13 )
break;
memset(v13, 0, 0x800uLL);
v14 = *(_OWORD **)((char *)&Context.P1Home + v12 * 8);
++v10;
v15 = (&in)[v12++];
*v15 = *v14;
v15[1] = v14[1];
v15[2] = v14[2];
v15[3] = v14[3];
if ( v10 >= 20 )
return 1;
}
TerminateProcess(hProcess, 1u);
}
}
}
else if ( LODWORD(Context.P1Home) != 5 )
{
return 1;
}
return 0;
}
if ( LODWORD(Context.P3Home) == 0xC0000094 )
{
Context.ContextFlags = 0x10001F;
GetThreadContext(hThread, &Context);
switch ( Context.Rax )
{
case 1uLL:
print("Welcome! Give me the key and I will give you the flag: ");
input("%128s", in);
lenth = -1LL;
do
++lenth;
while ( in[lenth] );
if ( lenth != 0x40 )
{
puts("Wrong1");
TerminateProcess(hProcess, 0);
return 0;
}
DeviceIoControl(hObject, 0x224008u, &in, 0xA0u, 0LL, 0, 0LL, 0LL);
break;
case 2uLL:
puts("Correct. Here is your flag");
decrypt((__int64)in);
Context.Rip += 6LL;
Context.Rax = 0LL;
goto LABEL_29;
case 3uLL:
puts("Wrong0");
Context.Rip += 6LL;
Context.Rax = 0LL;
LABEL_29:
SetThreadContext(hThread, &Context);
return 1;
}
Context.Rip += 6LL;
Context.Rax = 0LL;
goto LABEL_29;
}
if ( LODWORD(Context.P3Home) == 0xC0000005 )
{
Context.ContextFlags = 0x10001F;
GetThreadContext(hThread, &Context);
Context.Rip += 8LL;
goto LABEL_29;
}
return 1;
}

int __fastcall main(int argc, const char **argv, const char **envp)
{
_DEBUG_EVENT v4; // [rsp+50h] [rbp-308h] BYREF
struct _PROCESS_INFORMATION ProcessInformation; // [rsp+100h] [rbp-258h] BYREF
struct _STARTUPINFOA StartupInfo; // [rsp+120h] [rbp-238h] BYREF
_DEBUG_EVENT DebugEvent; // [rsp+190h] [rbp-1C8h] BYREF
CHAR Filename[256]; // [rsp+240h] [rbp-118h] BYREF

if ( IsDebuggerPresent() )
child();
GetModuleFileNameA(0LL, Filename, 0x100u);
StartupInfo.cb = 0x68;
memset(&StartupInfo.cb + 1, 0, 100);
memset(&ProcessInformation, 0, sizeof(ProcessInformation));
if ( CreateProcessA(Filename, 0LL, 0LL, 0LL, 0, 2u, 0LL, 0LL, &StartupInfo, &ProcessInformation) )
{
while ( WaitForDebugEvent(&DebugEvent, 0xFFFFFFFF) )
{
v4 = DebugEvent;
if ( !handler(&v4) )
break;
ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, 0x10002u);
}
CloseHandle(hObject);
nop_();
nop();
}
puts("[!] Can't create child process");
return 0;
}

这一题就是如果不调试子进程就会非常困难的例子 本题的校验和加密全部在child()handler()只起到接收数据和初始化一些数据的功能 而正常来说Windows进程有自己独立的一块虚拟内存 不同进程之间不允许访问对方内存 所以这题通过一个驱动程序(.sys)直接得到所有数据的物理地址并进过一系列处理再传给child()中的vm()(太长不放) 当作AES加密的密钥和iv 而内核对内存资源的管理和使用要经过虚拟内存转物理内存再根据四级内存页来获取内存固定的最后几位进行组合 之后才能操作用户进程的资源 而且内核调试的前置条件比较多 所以选择用CE调试

开启DBVM需要先开启CPU虚拟化 这里用CE的Lua引擎使用CE的API来Hook AES密钥扩展步骤来获取密钥和iv

image-20240928131338013

image-20240928131519855

image-20240928132928505

image-20240928133209644

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hexstr(bt)
local hexstring = {}
for _, byte in ipairs(bt) do
table.insert(hexstring, string.format("0x%02x", byte))
end
return table.concat(hexstring, ", ")
end

debug_setBreakpoint(0x7FF7BE131000, function ()
local key = hexstr(readBytes(RDX, 16, true))
local iv = hexstr(readBytes(RBX, 16, true))
print(string.format("[%s],\n[%s],\n", key, iv))
end)

debug_setBreakpoint(0x7FF7BE132741, function ()
local enc = hexstr(readBytes(RDX, 0x40, true))
print(string.format("[%s]", enc))
end)

CE脚本相较于IDA脚本更容易编写 因为Lua引擎在导入脚本时自动加入了包括寄存器的全局变量可以直接使用