-
-
[原创]看雪CTF.TSRC 2018 团队赛 第六题 追凶者也
-
2018-12-11 23:12 2023
-
看雪CTF.TSRC 2018 团队赛 第六题 追凶者也
朦胧初识
先试运行了下,果然win10运行不正常,没有退出,CPU立马上来了。
拖进ida,ida自动定位到了WinMain
函数。但是,函数只调用了一个空函数。
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { sub_401280(); return 0; } void sub_401280() { ; }
按图索骥
事出反常必有妖。因为用户函数比较少,很容易就发现程序使用的tls
回调函数,从exports
中亦能看出。查看回调函数,其中有两个调用,和一个线程函数。
HANDLE __stdcall TlsCallback_0(int a1, int a2, int a3) { HANDLE result; // eax int Parameter; // [esp+Ch] [ebp-8h] int v5; // [esp+10h] [ebp-4h] result = (HANDLE)0xCCCCCCCC; Parameter = 0xCCCCCCCC; v5 = 0xCCCCCCCC; if ( a2 == 1 ) { smc(); InitializeCriticalSection(&CriticalSection); hook_GetDlgItemTextA(); result = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, &Parameter, 0, 0); } return result; }
先看smc
函数,明显是修改sub_401280
偏移4字节处的5个字节数据,第一个字节修改为E9
,对应jmp
指令,后面4字节数据当然是跳转偏移,所以是跳到off_414014
指向的函数处。
BOOL smc() { signed int i; // [esp+0h] [ebp-18h] char v2[12]; // [esp+Ch] [ebp-Ch] *(_DWORD *)&v2[4] = 0xCCCCCCCC; *(_DWORD *)&v2[8] = 0xCCCCCCCC; v2[0] = 0xE9u; *(_DWORD *)&v2[1] = off_414014 - (off_414018 + 4) - 5; j_VirtualProtect(off_414018); for ( i = 0; i < 5; ++i ) off_414018[i + 4] = v2[i]; return j_VirtualProtect_re(off_414018); } .data:00414014 20 12 40 00 off_414014 dd offset set_diag_proc .data:00414018 80 12 40 00 off_414018 dd offset sub_401280
顺着往下走,看看主函数到底执行了什么。原来指定了窗口的消息回调函数DialogFunc
,ID为1002的控件被点击时的响应函数为sub_401040
。
oid __noreturn set_diag_proc() { HINSTANCE hInstance; // ST18_4 hInstance = GetModuleHandleW(0); DialogBoxParamW(hInstance, (LPCWSTR)0x65, 0, DialogFunc, 0); exit(0); } BOOL __stdcall DialogFunc(HWND hWnd, UINT a2, WPARAM a3, LPARAM a4) { if ( a2 == WM_CLOSE ) { DestroyWindow(hWnd); } else if ( a2 == WM_COMMAND && a3 == 1002 ) { sub_401040(hWnd); } return 0; } int __cdecl sub_401040(HWND hDlg) { void *v1; // ST08_4 signed int i; // [esp+Ch] [ebp-24h] signed int v4; // [esp+10h] [ebp-20h] char String[20]; // [esp+18h] [ebp-18h] strcpy(Text, "try again!"); strcpy(Caption, "fail"); String[0] = 0; *(_DWORD *)&String[1] = 0; *(_DWORD *)&String[5] = 0; *(_DWORD *)&String[9] = 0; *(_DWORD *)&String[13] = 0; *(_WORD *)&String[17] = 0; String[19] = 0; GetDlgItemTextA(hDlg, 1001, String, 20); v4 = 0; for ( i = 0; i < 20; ++i ) v4 += String[i]; if ( v4 > 0 && v4 < 4132 ) { v1 = malloc(v4); sub_401020(); j___free_base(v1); } return MessageBoxA(0, Text, Caption, 0); }
但sub_401040
除了获取了文本框的输入,最后出了个弹窗,似乎什么都没做。此条线就到此为止了。回到tls回调函数,还有一个函数调用的一个线程函数没有分析。go on,看hook_GetDlgItemTextA
函数。
BOOL hook_GetDlgItemTextA() { char *v0; // edx signed int j; // [esp+0h] [ebp-Ch] signed int i; // [esp+8h] [ebp-4h] get_api_GetDlgItemTextA(); g_GetDlgItemTextA = v0; j_VirtualProtect(v0); for ( i = 0; i < 5; ++i ) g_GetDlgItemTextA_bak[i] = g_GetDlgItemTextA[i + 32]; *(_DWORD *)&byte_414028[1] = (char *)sub_401A10 - (char *)(g_GetDlgItemTextA + 32) - 5; for ( j = 0; j < 5; ++j ) g_GetDlgItemTextA[j + 32] = byte_414028[j]; return j_VirtualProtect_re(g_GetDlgItemTextA); } .data:00414028 E9 byte_414028 db 0E9h
先是通过get_api_GetDlgItemTextA
函数获取GetDlgItemTextA
的地址,然后修改GetDlgItemTextA
函数偏移32字节处的5字节数据。与上次的smc类似,第一个字节修改为E9
,后4字节为偏移,是jmp
到sub_401A10
的偏移。下面列出了GetDlgItemTextA
的代码,就是将754A6B56
处本来要跳到返回的代码改成了跳到sub_401A10
,也就是hook了GetDlgItemTextA
的返回。
754A6B36 > 8BFF mov edi,edi 754A6B38 55 push ebp 754A6B39 8BEC mov ebp,esp 754A6B3B FF75 0C push dword ptr ss:[ebp+0xC] 754A6B3E FF75 08 push dword ptr ss:[ebp+0x8] 754A6B41 E8 7486FCFF call user32.GetDlgItem 754A6B46 85C0 test eax,eax 754A6B48 74 0E je short user32.754A6B58 754A6B4A FF75 14 push dword ptr ss:[ebp+0x14] 754A6B4D FF75 10 push dword ptr ss:[ebp+0x10] 754A6B50 50 push eax 754A6B51 E8 D394FAFF call user32.GetWindowTextA 754A6B56 EB 0E jmp short user32.754A6B66 754A6B58 837D 14 00 cmp dword ptr ss:[ebp+0x14],0x0 754A6B5C 74 06 je short user32.754A6B64 754A6B5E 8B45 10 mov eax,dword ptr ss:[ebp+0x10] 754A6B61 C600 00 mov byte ptr ds:[eax],0x0 754A6B64 33C0 xor eax,eax 754A6B66 5D pop ebp 754A6B67 C2 1000 retn 0x10
再顺着往下走,看sub_401A10
:
int __usercall sub_401A10@<eax>(int a1@<ecx>, int a2@<ebp>, int a3@<esi>) { unsigned int v3; // et0 int v4; // eax _BYTE *v5; // ecx unsigned int v7; // [esp-24h] [ebp-24h] int v8; // [esp-18h] [ebp-18h] v8 = a1; v3 = __readeflags(); v7 = v3; dword_414800 = a3; g_max_length = *(_DWORD *)(a2 + 20); g_input_copy = malloc(g_max_length); dword_4147F4 = a2 + 16; memmove(g_input_copy, *(const void **)(a2 + 16), g_max_length); *(_DWORD *)(a2 - 16) = g_input_copy; *(_DWORD *)(a2 - 20) = *(_DWORD *)(a2 - 16) + 1; do *(_BYTE *)(a2 - 21) = *(_BYTE *)(*(_DWORD *)(a2 - 16))++; while ( *(_BYTE *)(a2 - 21) ); *(_DWORD *)(a2 - 28) = *(_DWORD *)(a2 - 16) - *(_DWORD *)(a2 - 20);// length-1 if ( go_check((int)g_input_copy, *(_DWORD *)(a2 - 28)) ) { v4 = len((int)g_input_copy); if ( hash((char *)g_input_copy, v4) == 0x5634D252 ) { v5 = off_414030; *(_WORD *)off_414030 = 0; v5[2] = 0; for ( *(_WORD *)(a2 - 4) = 0; *(signed __int16 *)(a2 - 4) < 8; ++*(_WORD *)(a2 - 4) ) *((char *)off_414030 - *(signed __int16 *)(a2 - 4)) = (39 - *(unsigned __int16 *)(a2 - 4)) ^ *((_BYTE *)&dword_41401C + 7 - *(signed __int16 *)(a2 - 4)); *(_WORD *)off_414034 = 0; for ( *(_WORD *)(a2 - 8) = 0; *(signed __int16 *)(a2 - 8) < 3; ++*(_WORD *)(a2 - 8) ) *((char *)off_414034 - *(signed __int16 *)(a2 - 8)) = (34 - *(unsigned __int16 *)(a2 - 8)) ^ *((_BYTE *)&dword_414024 + 2 - *(signed __int16 *)(a2 - 8)); } } j___free_base(g_input_copy); dword_4147F8 = (int (__thiscall *)(_DWORD))(g_GetDlgItemTextA + 32); j_VirtualProtect(g_GetDlgItemTextA); for ( *(_DWORD *)(a2 - 12) = 0; *(_DWORD *)(a2 - 12) < 5; ++*(_DWORD *)(a2 - 12) ) g_GetDlgItemTextA[*(_DWORD *)(a2 - 12) + 32] = g_GetDlgItemTextA_bak[*(_DWORD *)(a2 - 12)]; j_VirtualProtect_re(g_GetDlgItemTextA); g_flag = 1; __writeeflags(v7); return dword_4147F8(v8); }
咋一看,有点费事,动态跟下就出来了。基本过程是:
- 将输入复制到一个全局变量中
- 计算输入长度
- 如果过了
go_check
和hash
的检查,则修改错误信息的数据为正确信息,直接影响到弹窗的内容 - 恢复对
GetDlgItemTextA
的修改,并全局标志位。 - 返回到
GetDlgItemTextA
处继续执行
所以检验点就出来了。
那线程函数是干嘛用的呢?
void __stdcall StartAddress(LPVOID lpThreadParameter) { while ( 1 ) { EnterCriticalSection(&CriticalSection); if ( g_flag == 1 ) { hook_GetDlgItemTextA(); g_flag = 0; } LeaveCriticalSection(&CriticalSection); } }
原来是为了下次按键点击时作准备,继续hookGetDlgItemTextA
。
玩乐求解
这里的校验有两个,下面的hash
函数其实在取api的时候用过,按道理讲是不可逆的,应该实际上的check只有一个,第一个满足条件了,第二个也应该就满足了。其实还有个隐含条件,就是输入应该是小于20字节的,可以从取输入的地方可以看出来,那第二个条件也可以说是防止多解。看go_check
:
bool __cdecl go_check(int a1, int a2) { g_table_4147D0[0][0] = 4; g_table_4147D0[0][1] = 1; g_table_4147D0[0][2] = 3; g_table_4147D0[1][0] = 7; g_table_4147D0[1][1] = 2; g_table_4147D0[1][2] = 5; g_table_4147D0[2][0] = 8; g_table_4147D0[2][1] = 6; g_table_4147D0[2][2] = 0; return check((char *)a1, a2); } bool __cdecl check(char *a1, int a2) { int i; // [esp+0h] [ebp-Ch] int v4; // [esp+8h] [ebp-4h] v4 = 0xCCCCCCCC; if ( a2 % 2 ) // length %2 == 0 return 0; for ( i = 0; i < a2; i += 2 ) { if ( a1[i] == 'w' ) v4 = 0; if ( a1[i] == 'd' ) v4 = 1; if ( a1[i] == 's' ) v4 = 2; if ( a1[i] == 'a' ) v4 = 3; if ( !move(v4, a1[i + 1] - 0x30) ) return 0; } return g_table_4147D0[0][0] == 1 && g_table_4147D0[0][1] == 2 && g_table_4147D0[0][2] == 3 && g_table_4147D0[1][0] == 4 && g_table_4147D0[1][1] == 5 && g_table_4147D0[1][2] == 6 && g_table_4147D0[2][0] == 7 && g_table_4147D0[2][1] == 8 && !g_table_4147D0[2][2]; } char __cdecl move(int op, int num) { char result; // al signed int i; // [esp+8h] [ebp-8h] signed int v4; // [esp+Ch] [ebp-4h] if ( !num ) return 0; v4 = 0; LABEL_4: if ( v4 >= 3 ) return 0; for ( i = 0; ; ++i ) { if ( i >= 3 ) { ++v4; goto LABEL_4; } if ( g_table_4147D0[v4][i] == num ) break; LABEL_6: ; } switch ( op ) { case 0: // w if ( v4 ) { if ( g_table_4147D0[v4 - 1][i] ) { result = 0; } else { g_table_4147D0[v4 - 1][i] = g_table_4147D0[v4][i]; g_table_4147D0[v4][i] = 0; result = 1; } } else { result = 0; } break; case 1: // d if ( i == 2 ) { result = 0; } else if ( byte_4147D1[3 * v4 + i] ) { result = 0; } else { byte_4147D1[3 * v4 + i] = g_table_4147D0[v4][i]; g_table_4147D0[v4][i] = 0; result = 1; } break; case 2: // s if ( v4 == 2 ) { result = 0; } else if ( g_table_4147D0[v4 + 1][i] ) { result = 0; } else { g_table_4147D0[v4 + 1][i] = g_table_4147D0[v4][i]; g_table_4147D0[v4][i] = 0; result = 1; } break; // a case 3: if ( i ) { if ( byte_4147CF[3 * v4 + i] ) { result = 0; } else { byte_4147CF[3 * v4 + i] = g_table_4147D0[v4][i]; g_table_4147D0[v4][i] = 0; result = 1; } } else { result = 0; } break; default: goto LABEL_6; } return result; }
go_check
函数先初始化了一个33的全局数组,再进入check
函数。check
先检查输入长度为偶数,再以2字节步长遍历输入,进入move
对33全局数组进行操作,最后检查全局数组值为1-8,最后为0。
细看move
函数,以遍历的输入2字节的第1字节为控制方向,第2字节为控制对象,进行上下左右的移动,原位置0。仔细一想,这不就是拼图游戏,0为空位,考虑到输入长度的隐含限制,应该是最少步数完成结果,具体步骤如下:
w-上 s-下 a-左 d-右 4 1 3 7 2 5 8 6 0 d6: 4 1 3 7 2 5 8 0 6 d8: 4 1 3 7 2 5 0 8 6 s7: 4 1 3 0 2 5 7 8 6 s4: 0 1 3 4 2 5 7 8 6 a1: 1 0 3 4 2 5 7 8 6 w2: 1 2 3 4 0 5 7 8 6 a5: 1 2 3 4 5 0 7 8 6 w6: 1 2 3 4 5 6 7 8 0
所以最终flag为:d6d8s7s4a1w2a5w6
。
输入试试,果然成功。
无聊之举
我十分好奇,为什么在win10上不能运行。原来秘密藏在api的获取过程中,代码如下,将就用伪代码看看吧。
int get_api_GetDlgItemTextA() { int v0; // edx get_user32_base(0x3BD696F4); return get_GetDlgItemTextA_addr(v0, 0x925DF53F); } int __stdcall get_user32_base(int a1) { int v1; // ecx int v2; // esi int v3; // ST08_4 char *v4; // ST04_4 int v5; // eax int result; // eax int v7; // edx v1 = a1; v2 = (int)NtCurrentPeb()->Ldr->InInitializationOrderModuleList.Flink; do { v2 = *(_DWORD *)v2; v3 = v1; v4 = *(char **)(v2 + 0x18); v5 = unicode_len(*(_DWORD *)(v2 + 0x18)); result = hash(v4, 2 * v5); v1 = v3; } while ( result != v3 ); v7 = *(_DWORD *)(v2 + 8); return result } int __stdcall get_GetDlgItemTextA_addr(int a1, int a2) { _DWORD *v2; // esi int i; // eax char *v4; // esi int v5; // eax int v7; // [esp-8h] [ebp-14h] int l_AddrofOrd; // [esp+0h] [ebp-Ch] int l_AddrofName; // [esp+4h] [ebp-8h] int l_AddrofFunc; // [esp+8h] [ebp-4h] v2 = (_DWORD *)(a1 + *(_DWORD *)(a1 + *(_DWORD *)(a1 + 0x3C) + 0x78));// ExportDir l_AddrofFunc = v2[7]; l_AddrofName = v2[8]; l_AddrofOrd = v2[9]; for ( i = 0; ; i = v7 + 1 ) { v4 = (char *)(a1 + *(_DWORD *)(a1 + l_AddrofName + 4 * i)); v7 = i; v5 = len(a1 + *(_DWORD *)(a1 + l_AddrofName + 4 * i)); if ( hash(v4, v5) == a2 ) break; } return a1 + *(_DWORD *)(a1 + l_AddrofFunc + 4 * *(unsigned __int16 *)(a1 + l_AddrofOrd + 2 * v7)); } int __stdcall hash(char *a1, int a2) { int i; // [esp+0h] [ebp-8h] int v4; // [esp+4h] [ebp-4h] v4 = a2; for ( i = 0; i < a2; ++i ) v4 = a1[i] ^ (v4 >> 28) ^ 16 * v4; return v4; }
先取dll基址,具体实现是:通过PEB找到PEB_LDR_DATA指针,再找到InInitializationOrderModuleList双向链表,遍历dll的全路径名称,进行hash计算与预设hash值比对。这里问题就来了,win10和win7 64位环境中user32.dll的全路径名称并不一致,就算基本路径一致,大小写也不一样。程序中预设的全路径名称为C:\Windows\syswow64\USER32.dll
,所以即使是在win7 32位系统中也是运行不正常的,会一直遍历双向链表。要想通用,只能取BaseDllName
,并且hash前统一大小写。
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界