Ikoct的饮冰室

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

0%

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

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

反调试技巧

NtQueryInformationProcess() / ZwQueryInformationProcess()

原理

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

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

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

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

例子

[LineCTF 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 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引擎在导入脚本时自动加入了包括寄存器的全局变量可以直接使用

[2024 0xl4ughCTF] dance

对应Windows的WaitForDebugEventAPI Linux有ptrace系统调用 同样可以配合fork()来创建子程序以实现Debug-Blocker 而且相比Windows版本的 使用ptrace将使得父子程序之间的数据交流更加方便且更无懈可击

在着手解决题目前先简单了解一下ptrace相关的知识 简单来说

1
2
long ptrace(enum __ptrace_request op, pid_t pid,
void *addr, void *data);

会根据第1个参数request进行不同的操作 常用的有:

request function addr data
PTRACE_ATTACH 附加到pid所指的子程序上 / /
PTRACE_CONT 恢复子程序的运行 / 若传入非零值则为向子程序传递的信号(SIGNAL)
PTRACE_GETREGS 获取当前子程序各个寄存器的值 / 一个指向struct user_regs_struct类型的指针用于存放获取到的寄存器值
PTRACE_SETREGS 设置当前子程序各个寄存器的值 / 一个指向struct user_regs_struct类型的指针用于存放要设置的寄存器值
PTRACE_PEEKDATA 从子程序addr指向的地址读取1个字(WORD)的数据到data指向的地址中 要读取的地址 要写入的地址
PTRACE_POKEDATA 从data指向的地址读取1个字(WORD)的数据到子程序addr指向的地址中 要写入的地址 要读取的地址

了解完ptrace后简单说明一下fork 相较于Windows上创建新进程从头执行再使用IsDebuggerPresent来判断子进程 fork在执行后直接就在另一块内存创建了一块大多数属性都与父进程相同的子进程 而在父进程中fork返回了子进程的pid 在子进程中返回了0

现在再看题目 主函数(ptrace原本是花指令实现的 修改函数名(成_ptrace)后IDA可以直接把它当成系统调用 第一个参数用宏表示了 IDA伟大 IDA门🙏):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall main(int a1, char **args, char **a3)
{
int stat_loc; // [rsp+18h] [rbp-8h] BYREF
__pid_t pid; // [rsp+1Ch] [rbp-4h]

if ( a1 != 2 )
{
printf("usage: %s <flag>\n", *args);
exit(1);
}
pid = fork();
if ( !pid )
child(args[1]);
ptrace(PTRACE_ATTACH, pid, 0LL, 0LL);
while ( 1 )
{
waitpid(pid, &stat_loc, 0);
if ( (stat_loc & 0x7F) == 0 )
break;
if ( stat_loc != 0xFFFF )
ptrace(PTRACE_CONT, pid, 0LL, 0LL);
}
return 0LL;
}

第一层blocker父进程没有根据子进程的行为执行代码 单纯为了反调试 子进程:

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
__int64 __fastcall child(_BYTE *input)
{
__int64 result; // rax
_QWORD v2[4]; // [rsp+10h] [rbp-310h] BYREF
_QWORD v3[24]; // [rsp+30h] [rbp-2F0h] BYREF
_BYTE v4[12]; // [rsp+F0h] [rbp-230h] BYREF
int x; // [rsp+FCh] [rbp-224h] BYREF
regs regs; // [rsp+100h] [rbp-220h] BYREF
int stat_loc; // [rsp+1DCh] [rbp-144h] BYREF
char s[256]; // [rsp+1E0h] [rbp-140h] BYREF
ssize_t v9; // [rsp+2E0h] [rbp-40h]
unsigned __int8 (__fastcall *func)(_BYTE *); // [rsp+2E8h] [rbp-38h]
void *handle; // [rsp+2F0h] [rbp-30h]
int fd; // [rsp+2F8h] [rbp-28h]
unsigned __int32 crc32sum; // [rsp+2FCh] [rbp-24h]
__pid_t pid; // [rsp+300h] [rbp-20h]
int count; // [rsp+304h] [rbp-1Ch]
size_t n; // [rsp+308h] [rbp-18h]
__int64 addr; // [rsp+310h] [rbp-10h]
unsigned __int64 ins_num; // [rsp+318h] [rbp-8h]

addr = 0LL;
pid = fork();
if ( !pid )
{
ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);
fd = memfd_create(&unk_3004, 0LL);
sprintf(s, "/proc/self/fd/%d", fd);
qmemcpy(v2, "Hello, that is one key for you..", sizeof(v2));
qmemcpy(v4, "nice_move_:)", sizeof(v4));
sub_23FA(v3, (__int64)v2, (__int64)v4, 0LL);// chacha20
sub_246D((__int64)v3, (__int64)fin, len);
for ( n = len; (__int64)n > 0; n -= v9 )
v9 = write(fd, &fin[len - n], n);
handle = dlopen(s, 2);
func = (unsigned __int8 (__fastcall *)(_BYTE *))dlsym(handle, "dance_with_me");
if ( func(input) )
puts("nop");
else
puts("ok");
dlclose(handle);
exit(0);
}
ptrace(PTRACE_ATTACH, pid, 0LL, 0LL);
while ( 1 )
{
waitpid(pid, &stat_loc, 0);
result = stat_loc & 0x7F;
if ( (stat_loc & 0x7F) == 0 )
break;
if ( stat_loc != 0xFFFF )
{
if ( (unsigned __int8)stat_loc == 127 && (stat_loc & 0xFF00) == 0x500 )
{
if ( ins_num && addr )
write_code(pid, addr, int_3, ins_num);
ptrace(PTRACE_GETREGS, pid, 0LL, &regs);
x = (LOWORD(regs.rip) - 1) & 0xFFF;
crc32sum = ~crc32(&x, 4LL);
count = -1;
do
++count;
while ( crc32sum != inss[count].crc32s && inss[count].crc32s );
write_code(pid, regs.rip - 1, inss[count].code, (unsigned __int8)inss[count].ins_num);
ins_num = (unsigned __int8)inss[count].ins_num;
addr = --regs.rip;
ptrace(PTRACE_SETREGS, pid, 0LL, &regs);
}
ptrace(PTRACE_CONT, pid, 0LL, 0LL);
}
}
return result;
}

第二层blocker 可以看到子进程的流程比较清晰 创建了一个新的文件描述符并写入了一个库以供动态加载其中的check函数 稍后再解释父进程行为

这里尝试调试修改fork返回值来获取写入的库 但是其中都是INT 3中断指令:

image-20250126214231954

那么肯定是父进程动态patch了这个库 接下来我尝试将puts('nop');的失败判断patch成无限循环 然后再手动kill掉父进程再附加调试子进程来查看check函数 但是还是失败了 程序输出了i'm dead后直接退出

接下来只能硬分析父进程行为了 只看其中最核心的片段:

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
...
if ( ins_num && addr )
write_code(pid, addr, int_3, ins_num);
ptrace(PTRACE_GETREGS, pid, 0LL, &regs);
x = (LOWORD(regs.rip) - 1) & 0xFFF;
crc32sum = ~crc32(&x, 4LL);
count = -1;
do
++count;
while ( crc32sum != inss[count].crc32s && inss[count].crc32s );
write_code(pid, regs.rip - 1, inss[count].code, (unsigned __int8)inss[count].ins_num);
ins_num = (unsigned __int8)inss[count].ins_num;
addr = --regs.rip;
ptrace(PTRACE_SETREGS, pid, 0LL, &regs);
...
__int64 __fastcall write_code(int pid, __int64 addr, _BYTE *ins, unsigned __int64 ins_num)
{
__int64 v4; // r8
__int64 v5; // r9
unsigned __int64 v9; // [rsp+0h] [rbp-50h]
_BYTE *v10; // [rsp+8h] [rbp-48h]
__int64 v11; // [rsp+10h] [rbp-40h]
int v12; // [rsp+1Ch] [rbp-34h]
__int64 v13; // [rsp+20h] [rbp-30h] BYREF
_BYTE *v14; // [rsp+28h] [rbp-28h]
__int64 i; // [rsp+30h] [rbp-20h]
_BYTE *v16; // [rsp+38h] [rbp-18h]
unsigned __int64 v17; // [rsp+40h] [rbp-10h]
unsigned __int64 v18; // [rsp+48h] [rbp-8h]

v12 = pid;
v11 = addr;
v10 = ins;
v9 = ins_num;
v18 = ins_num & 7;
v17 = ins_num >> 3;
v16 = ins;
for ( i = addr; v17--; i += 8LL )
{
if ( ptrace(PTRACE_POKEDATA, v12, i, *(_QWORD *)v16) == -1 )
return 0xFFFFFFFFLL;
v16 += 8;
}
if ( !v18 )
return 0LL;
v14 = &v13;
ptrace(PTRACE_PEEKDATA, v12, i, &v13);
if ( v13 == -1 && *__errno_location() )
{
i += v18 - 8;
ptrace(PTRACE_PEEKDATA, v12, i, &v13);
if ( v13 == -1 && *__errno_location() )
return 0xFFFFFFFFLL;
v14 += 8 - v18;
}
while ( v18-- )
v14[v18] = v16[v18];
if ( ptrace(PTRACE_POKEDATA, v12, i, v13) != -1 )
return 0LL;
return 0xFFFFFFFFLL;
}

其中根据内存特点定义了一个结构体 也就是inss的类型:

image-20250126214815048

结合write_code()的内容大致推断父进程的行为:

当子进程发生异常时用一个类似CRC32的哈希函数计算异常发生点的内存地址低3位十六进制的哈希值 然后再在inss中查找这个hash 如果找到的话向异常发生点写入指令 在下一轮开头再向异常发生点写入INT 3指令达到无痕执行的目的

那么接下来的工作就是还原库:

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
CRC32_table = [0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D]
def crc32(addr):
addr32 = addr.to_bytes(4, 'little')
sum = 0xFFFFFFFF
mask = sum
for x in addr32:
sum = (sum >> 8) ^ CRC32_table[(sum ^ x) & 0xFF]
sum &= mask
return ~sum & mask

# print(hex(crc32(0x483)))

_inss = [
...
]

_inss = [_inss[0x18 * i:0x18 * (i + 1)] for i in range(len(_inss) // 0x18)]

# print(_inss[1])

inss = {}
for ins in _inss:
crc32sum = int.from_bytes(bytes(ins[:4]), 'little')
len = ins[4]
ins_code = bytearray(ins[5:5 + len])
inss[crc32sum] = (len, ins_code)

old_so = open('.\lib.so', 'rb').read()
old_so = bytearray(old_so)

addr = 0xA0
bias = 0xFFF
while addr < 0xB06:
crc32sum = crc32(addr - 1)
try:
len, ins_code = inss[crc32sum]
except:
addr += 1
continue
old_so[bias + addr:addr + len + bias] = ins_code[:len]
addr += len

open('.\lib_recovered.so', 'wb').write(old_so)

得到修复库:

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
_BOOL8 __fastcall dance_with_me(char *a1)
{
int v2[49]; // [rsp+10h] [rbp-130h] BYREF
unsigned __int8 v3[12]; // [rsp+D4h] [rbp-6Ch] BYREF
__m128i v4; // [rsp+E0h] [rbp-60h] BYREF
__int64 v5; // [rsp+F0h] [rbp-50h]
unsigned __int64 v6; // [rsp+F8h] [rbp-48h]
_QWORD enc[6]; // [rsp+100h] [rbp-40h] BYREF
char v8; // [rsp+130h] [rbp-10h]
char v9; // [rsp+137h] [rbp-9h]
size_t v10; // [rsp+138h] [rbp-8h]

enc[0] = 0x6AAEAE66E85DE0B7LL;
enc[1] = 0x7BB7CB57573A0FD9LL;
enc[2] = 0x63F82FF80A9A46FELL;
enc[3] = 0x66F574F68BBC5E5CLL;
enc[4] = 0xD56C69C511F19761LL;
enc[5] = 0x2F6C8A362420DDDBLL;
v8 = -2;
v4.m128i_i64[0] = 0x9D474C0AEEB17B6CLL;
v4.m128i_i64[1] = 0x40DD43D2BC5BD7EDLL;
v5 = 0x4B596E35B877B21DLL;
v6 = 0xDBED50E2D72663F8LL;
*(_QWORD *)v3 = '\xBC\xFB|\x8E\xCA\xEB\xBF\x96';
*(_DWORD *)&v3[8] = 'S\xA8r\xD9';
v10 = strlen(a1);
sub_160A((__int64)v2, &v4, (__int64)v3, 0LL);
v9 = anti_patch();
if ( v9 == 1 )
{
puts("i'm dead");
return 1LL;
}
else
{
encrypt(v2, a1, v10);
return v10 <= 0x30 || (unsigned int)cmp(a1, enc, 49) != 0;
}
}

还是一个chacha20加密 高级版RC4 自己写一个程序来动态加载它并用密文当输入就能得到明文了:

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

char so_path[] = "./lib_recovered.so\x00";

void main(){
int continue_flag;
scanf("%d", &continue_flag);
void *handle = dlopen(so_path, 2);
if(handle == 0){
printf("Failed to load so\n");
return;
}
void (*func)(char *) = dlsym(handle, "dance_with_me");
if(func == 0){
printf("Failed to load function\n");
return;
}
char data[0x31] = {};
memset(data, 0x30, 0x30);
data[0x30] = 0;
scanf("%d", &continue_flag);
func(data);
dlclose(handle);
}

同时能发现为什么之前不能直接patch程序来获得库了 加载库时执行的全局构造函数以及check函数中都对原程序的完整性进行了验证:

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
__int64 anti_patch()
{
char v1; // [rsp+Ch] [rbp-2464h] BYREF
char v2; // [rsp+10h] [rbp-2460h] BYREF
char v3; // [rsp+101Ch] [rbp-1454h] BYREF
char v4; // [rsp+101Eh] [rbp-1452h] BYREF
char v5; // [rsp+1020h] [rbp-1450h] BYREF
char v6; // [rsp+102Bh] [rbp-1445h] BYREF
char v7; // [rsp+102Dh] [rbp-1443h]
__int64 v8; // [rsp+1030h] [rbp-1440h] BYREF
__int64 v9; // [rsp+1038h] [rbp-1438h] BYREF
char s[5132]; // [rsp+1040h] [rbp-1430h] BYREF
int v11; // [rsp+244Ch] [rbp-24h]
_BYTE *v12; // [rsp+2450h] [rbp-20h]
char *v13; // [rsp+2458h] [rbp-18h]
__int64 v14; // [rsp+2460h] [rbp-10h]
FILE *stream; // [rsp+2468h] [rbp-8h]

stream = fopen("/proc/self/maps", "r");
if ( stream )
{
while ( fgets(s, 5120, stream) )
{
v2 = 0;
v14 = (int)__isoc99_sscanf(s, "%lx-%lx %s %lx %x:%x %u %s\n", &v9, &v8, &v6, &v5, &v3, &v4, &v1, &v2);
if ( v6 == 114 && v7 == 120 )
break;
}
fclose(stream);
v13 = sub_118C((_BYTE *)(v9 + 256), v8);
if ( !v13 )
exit();
v12 = sub_118C(v13 + 16, v8);
if ( !v12 )
exit();
v11 = crc32(v13, v12 - v13);
if ( v11 == 0x5285F228 )
{
return 0LL;
}
else
{
puts("i'm dead");
exit();
return 1LL;
}
}
else
{
puts("i'm dead");
exit();
return 1LL;
}
}

调试时修改一下RIP直接绕过即可 最后在调用encrypt时将RSI改为密文地址就能在加密后得到flag了

C-style float

众所周知C中的浮点数按照IEEE754规则存储在内存中 这里不介绍各种神必的数学上的加密方式 只介绍一些C浮点数的性质

-0.0

当一个浮点数在内存中存放的全部字节都是b'\x00'时 按照IEEE754规则这个浮点数就是2^-126 但是因为显示精度有限所以通常就认为是0.0 同理当符号位是1时这个值就是-0.0 这些都是显而易见的

但是当0.0和-0.0之间进行运算时结果就超出常理了

用以下代码编译的程序验证除了除法运算之外的结果:

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

void main(){
float fs[2] = {0.0, -0.0};
for(int i = 0; i < 4; i++){
float A = fs[i & 1];
float B = fs[i >> 1];
printf("%.1f\t+\t%.1f\t=\t%.1f\n", A, B, A+B);
}
for(int i = 0; i < 4; i++){
float A = fs[i & 1];
float B = fs[i >> 1];
printf("%.1f\t-\t%.1f\t=\t%.1f\n", A, B, A-B);
}
for(int i = 0; i < 4; i++){
float A = fs[i & 1];
float B = fs[i >> 1];
printf("%.1f\t*\t%.1f\t=\t%.1f\n", A, B, A*B);
}
}

得到结果:

1
2
3
4
5
6
7
8
9
10
11
12
0.0     +       0.0     =       0.0
-0.0 + 0.0 = 0.0
0.0 + -0.0 = 0.0
-0.0 + -0.0 = -0.0
0.0 - 0.0 = 0.0
-0.0 - 0.0 = -0.0
0.0 - -0.0 = 0.0
-0.0 - -0.0 = 0.0
0.0 * 0.0 = 0.0
-0.0 * 0.0 = -0.0
0.0 * -0.0 = -0.0
-0.0 * -0.0 = 0.0

将其中的-0.00.0分别替换为1和0 就能将浮点数的加减乘运算转为布尔运算:

1
2
3
A + B ==> A &  B
A - B ==> A & ~B
A * B ==> A ^ B

利用这一点以及目前的反汇编器难以表现出这类浮点数之间的运算就能实现对某些加密算法的隐藏

[WWCTF floats]

题目要求从命令行传入长度为0x20的flag 其中每0x10 bytes的flag会被转为__int128然后进行一系列处理 首先是根据每一位来创建一个浮点数 如果这一位是0那么这个浮点数就是-0.0否则为0.0:

1
2
3
4
5
6
7
8
9
for ( i = 0; i <= 127; ++i )
{
if ( (flag1 & 1) != 0 )
v1 = 0.0;
else
v1 = -0.0;
res[i] = v1;
flag1 >>= 1;
}

然后对这些1位进行了16轮某种处理:

image-20241202170556687

最后对结果中的位相互运算得到结果:

image-20241202170643132

显然直接进行逆运算得到正确flag十分困难 先看看汇编:

image-20241202170732989

可以看到除了移动指令之外只用到了加, 减和异或运算 按照上面的结论直接模拟程序的运行来用z3解

先得到两个check函数的汇编:

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

start = 0x55555555528C
end = 0x555555557EE9

with open("disasm_result.txt", "w") as f:
f.write("Disasm of Cheak1() :\n")
while start < end:
f.write(GetDisasm(start) + "\n")
start = next_head(start, end)
f.write("\n")
start = 0x555555557F0D
end = 0x555555558CE1
while start < end:
f.write(GetDisasm(start) + "\n")
start = next_head(start, end)
f.write("\n")
f.write("\n")

f.write("Disasm of Cheak2() :\n")
start = 0x555555558DEE
end = 0x55555555BA4B
while start < end:
f.write(GetDisasm(start) + "\n")
start = next_head(start, end)
f.write("\n")
start = 0x55555555BA5F
end = 0x55555555C843
while start < end:
f.write(GetDisasm(start) + "\n")
start = next_head(start, end)
f.write("\n")

进行初步处理得到方便执行的指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
with open("DisAsm_cheak2().txt", "r") as f:
with open("NewDisasm2.txt", "w") as f1:
ins = f.readline()
while ins:
ins = findall(r'[^\s,]+', ins)
new_ins = []
for part in ins:
if part.startswith("res["):
num = findall(r'0x[0-9a-fA-F]+', part)[0]
hex_num = int(num, 16)
part = f"res[{hex_num // 4}]"
new_ins.append(part)
f1.write(" ".join(new_ins) + "\n")
ins = f.readline()

模拟执行后用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
def NewPlus(A, B):
return A & B

def NewMinus(A, B):
return A & ~B

s = Solver()
res = [BitVec('res' + str(i), 1) for i in range(656)]
ans = res[:0x80]
xmm0 = BitVec('xmm0', 1)
xmm1 = BitVec('xmm1', 1)
count = 0
with open("NewDisasm1.txt", "r") as f:
ins = f.readline()
while ins:
if ins == '\n' and count < 15:
count += 1
# print(f"Round {count} is done.")
f.seek(0)
else:
ins = ins.split(" ")
op = ins[0]
if op == 'MOV':
exec(f"{ins[1]} = {ins[2]}")
elif op == 'ADD':
exec(f"{ins[1]} = NewPlus({ins[1]}, {ins[2]})")
elif op == 'SUB':
exec(f"{ins[1]} = NewMinus({ins[1]}, {ins[2]})")
elif op == 'XOR':
exec(f"{ins[1]} = {ins[1]} ^ {ins[2]}")

ins = f.readline()
s.add(res[655] == 1)
while s.check() == sat:
result = 0
m = s.model()
for i in range(0x80):
# print(f"{ans[i]}: {m[ans[i]]}")
result |= (~(m[ans[i]].as_long()) & 1) << i
print(result.to_bytes(0x10, 'little').decode(), end="")
s.add(Or([ans[i] != m[ans[i]] for i in range(0x80)]))
# 第二部分同理

最后才发现既然都要模拟执行了为什么不用Angr呢(