[MiniL CTF 2022] Reverse部分赛题复现
再不学re👶要被开了
为什么是部分复现呢?因为有个wasm题没有环境没法复现捏
twin
👶最开始以为这是个签到题。。
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;
}
}
a2 = 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);
:WriteFileXor_0x7Fu(ModuleName);
:kernel32.dllhModule = 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)
a2 != 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);
}
子文件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这些异常处理了