关闭页面特效

[MiniL CTF 2022] Reverse部分赛题复现

再不学re👶要被开了

为什么是部分复现呢?因为有个wasm题没有环境没法复现捏

1|0twin


👶最开始以为这是个签到题。。

for (int i = 0; i < 40; ++i) flag[i] = v5[i] ^ i ^ 0x7f, putchar(flag[i]); // You_are_too_young_this_is_a_fake_flag!!!

打断点调试发现main之前已经有东西在运行了,调了一会发现有个函数叫TlsCallback_0

搜了下发现是TLS回调函数

TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数,创建进程的主线程时也会自动调用回调函数,且其调用执行先于EP代码

可以理解为创建线程和终止线程的时候会各执行一次TLS回调函数

发现没法反编译(sp-analysis failed,栈帧错误),点进去看汇编

.text:00401990 TlsCallback_0 proc near ; DATA XREF: .data:TlsCallbacks↓o .text:00401990 .text:00401990 var_12C = dword ptr -12Ch .text:00401990 .text:00401990 push ebp .text:00401991 mov ebp, esp .text:00401993 sub esp, 11Ch .text:00401999 push ebx .text:0040199A push esi .text:0040199B push edi .text:0040199C call $+5 .text:004019A1 add [esp+12Ch+var_12C], 1Eh .text:004019A5 retn .text:004019A5 TlsCallback_0 endp ; sp-analysis failed .text:004019A5 .text:004019A5 ; --------------------------------------------------------------------------- .text:004019A6 aWelcomeTo2022M db 'Welcome_to_2022_miniLCTF',0 .text:004019BF ; ---------------------------------------------------------------------------

分析一下汇编,发现0x401999C处有个call $+5,$+5就是40199C + 5 = 4019A1,就是下一句话的地址。call将下一句的地址压栈,然后add给esp指针加了个0x1E(var_12C = -12Ch),retn的地址就是0xA1 + 0x1E = 0xBF,所以中间这一串屁用没有,那直接把call到字符串这里全nop了。

发现还是不能反编译(继续sp-analysis failed),最后发现是函数的结束地址被最开始0x40199C给干扰了(指看了wp才知道),最后正确的retn地址是0x401D5B,alt + p把tls函数的end address改成401D5C就行。401A03处也有这个花指令。

void __cdecl TlsCallback_0(int a1, int a2) { char *v2; // eax char Buffer[80]; // [esp+14h] [ebp-11Ch] BYREF struct _STARTUPINFOA StartupInfo; // [esp+64h] [ebp-CCh] BYREF struct _PROCESS_INFORMATION ProcessInformation; // [esp+A8h] [ebp-88h] BYREF char v7[22]; // [esp+BCh] [ebp-74h] BYREF char v8[4]; // [esp+D2h] [ebp-5Eh] BYREF char v9[39]; // [esp+D8h] [ebp-58h] BYREF char v10[4]; // [esp+FFh] [ebp-31h] BYREF char v11[6]; // [esp+104h] [ebp-2Ch] BYREF char v12[4]; // [esp+10Ah] [ebp-26h] BYREF CHAR Name[8]; // [esp+110h] [ebp-20h] BYREF CHAR ApplicationName[8]; // [esp+118h] [ebp-18h] BYREF char v15[5]; // [esp+120h] [ebp-10h] BYREF char v16[10]; // [esp+125h] [ebp-Bh] BYREF uint8_t BeingDebugged; // [esp+12Fh] [ebp-1h] if ( a2 == 1 ) { memset(Buffer, 0, sizeof(Buffer)); printf(Buffer); BeingDebugged = 0; BeingDebugged = NtCurrentPeb()->BeingDebugged; if ( !BeingDebugged ) *(&TlsCallbacks + 1) = (int (__cdecl *)(int, int))sub_401D60; strcpy(Name, "93>8"); Xor_0x7Fu(Name); hObject = CreateFileMappingA(0, 0, 4u, 0, 0x1000u, Name); *(_DWORD *)dword_404448 = MapViewOfFile(hObject, 0xF001Fu, 0, 0, 0x1000u); v7[0] = 47; v7[1] = 19; v7[2] = 26; v7[3] = 30; v7[4] = 12; v7[5] = 26; v7[6] = 95; v7[7] = 22; v7[8] = 17; v7[9] = 15; v7[10] = 10; v7[11] = 11; v7[12] = 95; v7[13] = 6; v7[14] = 16; v7[15] = 10; v7[16] = 13; v7[17] = 95; v7[18] = 25; v7[19] = 19; v7[20] = 30; v7[21] = 24; strcpy(v8, "E_"); v2 = (char *)Xor_0x7Fu(v7); printf(v2); v16[3] = 90; v16[4] = 12; v16[5] = 0; Xor_0x7Fu(&v16[3]); input(&v16[3], *(_DWORD *)dword_404448, 41); } if ( !a2 ) { qmemcpy(ApplicationName, "QP\v", 3); ApplicationName[3] = 18; ApplicationName[4] = 15; ApplicationName[5] = 0; Xor_0x7Fu(ApplicationName); sub_401410(); memset(&StartupInfo, 0, sizeof(StartupInfo)); StartupInfo.cb = 68; CreateProcessA(ApplicationName, 0, 0, 0, 0, 3u, 0, 0, &StartupInfo, &ProcessInformation); v11[0] = 28; v11[1] = 16; v11[2] = 13; v11[3] = 13; v11[4] = 26; v11[5] = 28; strcpy(v12, "\vu"); qmemcpy(v15, "\b\r", 2); v15[2] = 16; v15[3] = 17; v15[4] = 24; strcpy(v16, "u"); v9[0] = 47; v9[1] = 19; v9[2] = 26; v9[3] = 30; v9[4] = 12; v9[5] = 26; v9[6] = 95; v9[7] = 28; v9[8] = 19; v9[9] = 16; v9[10] = 12; v9[11] = 26; v9[12] = 95; v9[13] = 11; v9[14] = 23; v9[15] = 26; v9[16] = 95; v9[17] = 27; v9[18] = 26; v9[19] = 29; v9[20] = 10; v9[21] = 24; v9[22] = 24; v9[23] = 26; v9[24] = 13; v9[25] = 95; v9[26] = 30; v9[27] = 17; v9[28] = 27; v9[29] = 95; v9[30] = 11; v9[31] = 13; v9[32] = 6; v9[33] = 95; v9[34] = 30; v9[35] = 24; v9[36] = 30; v9[37] = 22; v9[38] = 17; strcpy(v10, "u"); sub_401510(ApplicationName, &ProcessInformation); if ( dword_404440 == 1 ) { XXTea((_DWORD *)(*(_DWORD *)dword_404448 + 20), 5, (int)&unk_40405C); if ( !memcmp((const void *)(*(_DWORD *)dword_404448 + 20), &unk_40402C, 0x14u) ) { Xor_0x7Fu(v11); printf(v11); LABEL_13: CloseHandle(hObject); return; } } else if ( dword_404440 == -2 ) { Xor_0x7Fu(v9); printf(v9); goto LABEL_13; } Xor_0x7Fu(v15); printf(v15); goto LABEL_13; } }

1|1a2 = 1


最开始的BeingDebugged = NtCurrentPeb()->BeingDebugged;是反调试,动调的时候jz jnz反着patch就能过掉

v7这串玩意在异或0x7F后是Please input your flag:,v16异或后是%s,可以发现后面两个函数分别是输出和输入,输入存储在dword_404448内

在反调试的下面有一句*(&TlsCallbacks + 1) = (int (__cdecl *)(int, int))sub_401D60;

这里判断若没有在动调就在 TLS函数后增加一个函数sub_401D60,这个函数就在TLS下面。

现在切过去看发现这里也爆红了,但花指令和TLS的花是一样的

void __cdecl __noreturn sub_401D60(int a1, int a2) { CHAR ModuleName[16]; // [esp+4h] [ebp-1Ch] BYREF CHAR ProcName[12]; // [esp+14h] [ebp-Ch] BYREF if ( a2 == 1 ) { qmemcpy(ProcName, "(\r", 2); ProcName[2] = 22; ProcName[3] = 11; ProcName[4] = 26; ProcName[5] = 57; ProcName[6] = 22; ProcName[7] = 19; ProcName[8] = 26; ProcName[9] = 0; ModuleName[0] = 20; ModuleName[1] = 26; ModuleName[2] = 13; ModuleName[3] = 17; ModuleName[4] = 26; ModuleName[5] = 19; ModuleName[6] = 76; ModuleName[7] = 77; ModuleName[8] = 81; ModuleName[9] = 27; ModuleName[10] = 19; ModuleName[11] = 19; ModuleName[12] = 0; Xor_0x7Fu(ProcName); Xor_0x7Fu(ModuleName); hModule = GetModuleHandleA(ModuleName); dword_4043DC = (int)GetProcAddress(hModule, ProcName); sub_4016C0(dword_4043DC, sub_401650, hModule); } ExitProcess(0xFFFFFFFF); }
  • Xor_0x7Fu(ProcName);:WriteFile
  • Xor_0x7Fu(ModuleName);:kernel32.dll
  • hModule = GetModuleHandleA(ModuleName);:获得kernel32.dll的句柄

这里是导入了WriteFile和kernel32.dll(后续实现了注入)。GetProcAddress取得WriterFile的函数指针存储在dword_4043DC中

int __cdecl sub_4016C0(int a1, int a2, HMODULE a3) { DWORD flOldProtect; // [esp+Ch] [ebp-10h] BYREF int v5; // [esp+10h] [ebp-Ch] HMODULE ModuleHandleA; // [esp+14h] [ebp-8h] LPVOID lpAddress; // [esp+18h] [ebp-4h] ModuleHandleA = GetModuleHandleA(0); v5 = (int)ModuleHandleA + *(_DWORD *)((char *)ModuleHandleA + *((_DWORD *)ModuleHandleA + 15) + 128); flOldProtect = 0; do { if ( !*(_DWORD *)(v5 + 16) || dword_4043D0 ) break; if ( a3 == GetModuleHandleA((LPCSTR)ModuleHandleA + *(_DWORD *)(v5 + 12)) ) { for ( lpAddress = (char *)ModuleHandleA + *(_DWORD *)(v5 + 16); lpAddress; lpAddress = (char *)lpAddress + 4 ) { if ( *(_DWORD *)lpAddress == a1 ) { VirtualProtect(lpAddress, 4u, 4u, &flOldProtect); *(_DWORD *)lpAddress = a2; VirtualProtect(lpAddress, 4u, flOldProtect, 0); dword_4043D0 = 1; break; } } } v5 += 20; } while ( !dword_4043D0 ); return dword_4043D0; }

该函数中遍历了kernel32.dll的导入表, 并在lpAddress为WriteFile的时候调用VituralProtect获取权限,从而修改其为a2(即sub_401650)。换言之,该函数将原本的WriteFile给hook成了sub_401650。

好几把高端,以前没见过这种玩意。。

int __stdcall sub_401650(int a1, int a2, int a3, int a4, int a5) { *(_BYTE *)(a2 + 1822) = 6; *(_BYTE *)(a2 + 1713) = 6; dword_4043DC(a1, a2, a3, a4, a5); sub_4017C0(dword_4043DC, sub_401650, hModule); return 0; }

一开始看的时候不知道上面两个6有啥用,唯一能看懂的是sub_4017C0和sub_40166C0类似,只是这次是将hook给取消了,WriteFile调用的还是自己。但一开始也能猜出来这次hook就是为了执行前面两个置6的操作。

(后面会发现是把xxtea的z的位移改成了6)

1|2a2 != 1


sub_401D60末尾有ExitProcess,这之后a2 = 0,进入下半段函数。

v11是correct,v15是wrong,v9是Please close the debugger and try again。

其中有一个加密函数内是xxtea加密,对输入的后20位进行判断。在动调拿出enflag和key后可以解出是3e90c91c02e9b40b78b},显然是后半段flag

这里面有个重要函数sub_401410

BOOL sub_401410() { CHAR Type[8]; // [esp+0h] [ebp-2Ch] BYREF CHAR FileName[8]; // [esp+8h] [ebp-24h] BYREF BOOL v3; // [esp+10h] [ebp-1Ch] DWORD NumberOfBytesWritten; // [esp+14h] [ebp-18h] BYREF HGLOBAL hResData; // [esp+18h] [ebp-14h] LPCVOID lpBuffer; // [esp+1Ch] [ebp-10h] DWORD nNumberOfBytesToWrite; // [esp+20h] [ebp-Ch] HRSRC hResInfo; // [esp+24h] [ebp-8h] HANDLE hFile; // [esp+28h] [ebp-4h] qmemcpy(FileName, "QP\v", 3); FileName[3] = 18; FileName[4] = 15; FileName[5] = 0; strcpy(Type, ":':-:,"); Xor_0x7Fu(FileName); Xor_0x7Fu(Type); hResInfo = FindResourceA(0, (LPCSTR)0x65, Type); nNumberOfBytesToWrite = SizeofResource(0, hResInfo); hResData = LoadResource(0, hResInfo); lpBuffer = LockResource(hResData); sub_401E40(lpBuffer, nNumberOfBytesToWrite); hFile = CreateFileA(FileName, 0xC0000000, 0, 0, 2u, 0x80u, 0); NumberOfBytesWritten = 0; v3 = WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0); FlushFileBuffers(hFile); return CloseHandle(hFile); }

1|3子文件tmp


sub_401410中创建了一个叫做tmp的文件,断点打在return处动调再运行几步可以拿到完整的tmp文件。然后分析tmp文件。

int __cdecl main(int argc, const char **argv, const char **envp) { void *v3; // ecx sub_401400(v3); if ( sub_4010E0() ) { delta ^= 0x90909090; key[1] = 144; } delta = sub_401210(delta); sub_401390(&unk_4043A8); xxtea(&unk_4043A8, 5, key); if ( !memcmp(&unk_4043A8, &unk_404018, 0x14u) ) return 1; else return -1; }

最开始的sub_401400中有sub_4010F0,是个检测动调的函数,如果检测到一些动调进程的名字的话就会退出程序。然后有对delta的异或操作和一步添加VEH(但好像并没有用到这个VEH)。

sub_4010E0是IsDebuggerPresent反调试

BOOL __cdecl sub_401390(void *a1) { HANDLE hFileMappingObject; // [esp+8h] [ebp-8h] LPCVOID lpBaseAddress; // [esp+Ch] [ebp-4h] hFileMappingObject = CreateFileMappingA(0, 0, 4u, 0, 0x1000u, "FLAG"); lpBaseAddress = MapViewOfFile(hFileMappingObject, 0xF001Fu, 0, 0, 0x1000u); qmemcpy(a1, lpBaseAddress, 0x28u); UnmapViewOfFile(lpBaseAddress); return CloseHandle(hFileMappingObject); }

后面才发现sub_401390和之前主体文件a2=1部分中的这一句对应

strcpy(Name, "93>8"); Xor_0x7Fu(Name); hObject = CreateFileMappingA(0, 0, 4u, 0, 0x1000u, Name); *(_DWORD *)dword_404448 = MapViewOfFile(hObject, 0xF001Fu, 0, 0, 0x1000u);

这个NAME字符串异或出来就是"FLAG",hObject这一句创建了名为FLAG的文件映像,dword_404448这一句将内存映射的文件存了下来,这样可以让子进程访问dword_404448指向的内存,实现修改等操作。

那么sub_401390里面获取了dword_404448的共享内存,然后把lpBaseAddress指向的数据复制0x28u位(40位,按位数来说应该是输入)给a1(unk_4043A8)。

随后,对unk_4043A8进行了xxtea加密。

注意,此处的xxtea加密是有变动的:其右移位数从原来的5位变成了6位(前面sub_401650写进去的两个6就在这里)。

动调发现sub_401210炸了,看汇编看到了和以前一样的花指令,去掉后发现还是爆红了

int __cdecl sub_401210(int a1) { MEMORY[0] = 0; return (a1 ^ 0x7B) + 12345; }

MEMORY[0] = 0是往地址0内写值了,引发了内存访问异常。在汇编里面你可以看见这几句话

.text:00401230 xor ebx, ebx .text:00401232 mov [ebx], ebx .text:00401234 mov eax, [ebp+var_4]

xor先把ebx置0,然后mov [ebx], ebx就向地址0里面写值了, 就异常了。

尝试动调的时候把eip改改跳到401234,发现还是死了。。

分析一下发现[ebp+var_4]的值是delta(在401220处mov [ebp+var_4], eax),显然这个地址没卵用,还是会爆异常(这就是为什么上面说好像并没有用到VEH,他完全没处理异常。。)

滚回原来的程序,从创建tmp那里往下看,发现sub_401510里面又有花,去掉

BOOL __cdecl sub_401510(int a1, int a2) { CONTEXT Context; // [esp+8h] [ebp-33Ch] BYREF int v4[23]; // [esp+2D4h] [ebp-70h] BYREF HANDLE hThread; // [esp+330h] [ebp-14h] int v6; // [esp+334h] [ebp-10h] int v7; // [esp+338h] [ebp-Ch] int v8; // [esp+33Ch] [ebp-8h] int v9; // [esp+340h] [ebp-4h] v4[22] = *(_DWORD *)a2; hThread = *(HANDLE *)(a2 + 4); v6 = *(_DWORD *)(a2 + 8); v7 = *(_DWORD *)(a2 + 12); v9 = 1; while ( v9 ) { WaitForDebugEvent(&DebugEvent, 0xFFFFFFFF); if ( DebugEvent.dwDebugEventCode == 1 ) { qmemcpy(v4, &DebugEvent.u, 0x54u); v8 = v4[0]; if ( v4[0] == -1073741819 ) { memset(&Context, 0, sizeof(Context)); Context.ContextFlags = 65543; GetThreadContext(hThread, &Context); Context.Eip += 5; Context.Eax ^= 0x1B207u; SetThreadContext(hThread, &Context); } } if ( DebugEvent.dwDebugEventCode == 5 ) { dword_404440 = DebugEvent.u.Exception.ExceptionRecord.ExceptionCode; v9 = 0; } ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, 0x10002u); } Sleep(0x64u); return DeleteFileA((LPCSTR)a1); }

WaitForDebugEvent表明父进程是通过调试的方法打开的子进程,并且通过百度发现

联合体u的值

1u.Exception

2 u.Create Thread

3 u.CreateProcessInfo

4 u.ExitThread

5 u.ExitProcess

6 u.LoadDll

7 u.UnloadDll

8 u.DebugString

9 u.RipInfo

union的值是通过DebugEvent.dwDebugEventCode决定的,也就是说,=1的时候说明触发了异常,此时父进程会处理子进程的异常,修改子进程代码;=5说明程序正常退出。

if ( v4[0] == -1073741819 )下面对eip和eax进行了修改,经过搜索发现-1073741819(就是0xC0000005)代表的错误类型是发生访问冲突,也就是内存访问异常。

联系一下tmp文件里面爆红的MEMORY[0]=0,说明这里就是检测到子进程发生了异常后,程序会转到父进程进行异常处理,并且如果是内存访问异常的话就修改eip和eax。

联系到tmp文件内对应的eax的值是处理后的delta,说明tmp里面的xxtea的delta还得再异或0x1B207u才是正确的值。这下就可以用正确的魔改版xxtea解出前一半的flag了:miniLctf{cbda59ff59e

最后得到flag:

miniLctf{cbda59ff59e3e90c91c02e9b40b78b}

这个题学到的新东西真的太多了,👶打算下一个自学坑就开seh veh这些异常处理了

2|0NotRC4


3|0lemon



__EOF__

作  者iPlayForSG
出  处https://www.cnblogs.com/Here-is-SG/p/16285687.html
关于博主:编程路上的小学生,热爱技术,喜欢专研。评论和私信会在第一时间回复。或者直接私信我。
版权声明:署名 - 非商业性使用 - 禁止演绎,协议普通文本 | 协议法律文本
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!

posted @   iPlayForSG  阅读(226)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示