-
-
[原创] KCTF 2023 第六题 wp - 98k
-
2023-9-14 00:35 2725
-
main 之前有两个 TlsCallback 用于反调试,直接 patch 掉;
进入 main ,新开一个线程,执行函数 sub_140001630
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | int __cdecl main( int argc, const char **argv, const char **envp) { _QWORD *v3; // rax _Thrd_t ThrdAddr; // [rsp+30h] [rbp-28h] BYREF _Thrd_t v6; // [rsp+40h] [rbp-18h] BYREF v3 = operator new (8ui64); *v3 = sub_140001630; ThrdAddr._Hnd = ( void *)beginthreadex(0i64, 0, (_beginthreadex_proc_type)StartAddress, v3, 0, &ThrdAddr._Id); if ( !ThrdAddr._Hnd ) { ThrdAddr._Id = 0; std::_Throw_Cpp_error(6); } if ( !ThrdAddr._Id ) std::_Throw_Cpp_error(1); if ( ThrdAddr._Id == Thrd_id() ) std::_Throw_Cpp_error(5); v6 = ThrdAddr; if ( Thrd_join(&v6, 0i64) ) std::_Throw_Cpp_error(2); return 0; } |
sub_140001630 就是主逻辑,字符串都加密了,异或解密之后就能看出来逻辑:
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 | __int64 sub_140001630() { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] std::string::ctor(&input_string, empty); std::string::ctor(&Format, s); // 'Please enter your key:' std::string::ctor(&str_kernel32_dll, aJzlOmJnms); // kernel32.dll std::string::ctor(&str_RtlFillMemory, aM); std::string::ctor(&str_kctf, aAJv); // kctf std::string::append(&str_RtlFillMemory, aD_0); // RtlFillMemory std::string::ctor(&Block, "7PK:" ); v0 = ( char *)&Format; if ( Format.cap >= 0x10 ) v0 = Format.data.lstr; vigenere_decrypt(v0, Format.size, "&x+^x" , 5); v1 = ( char *)&Format; if ( Format.cap >= 0x10 ) v1 = Format.data.lstr; printf (v1); v2 = input_string.size; // set input length to 500 if ( input_string.size < 500 ) { // ... } else { // ... } v7 = ( char *)&input_string; if ( input_string.cap >= 0x10 ) v7 = input_string.data.lstr; scanf ( "%s" , v7); v8 = ( char *)&input_string; if ( input_string.cap >= 0x10 ) v8 = input_string.data.lstr; v9 = ( char *)&str1; std::string::append(&str1, v8); v10 = ( char *)&str2; if ( str2.cap >= 0x10 ) v10 = str2.data.lstr; v11 = str2.size; v12 = str1.size; str1 += str2; // ... v15 = ( char *)&str_kctf; if ( str_kctf.cap >= 0x10 ) v15 = str_kctf.data.lstr; vigenere_decrypt(v15, str_kctf.size, "*s>0?" , 5); str1 = str_kctf + str1; // ... ProcessHandle = GetCurrentProcess(); RegionSize = 5500i64; NtAllocateVirtualMemory( ProcessHandle, ( PVOID *)&alloced, 0i64, &RegionSize, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE); if ( str1.cap >= 0x10 ) v9 = str1.data.lstr; NtWriteVirtualMemory(ProcessHandle, alloced, v9, str1.size, NumberOfBytesWritten); v20 = ( char *)&str_kernel32_dll; if ( str_kernel32_dll.cap >= 0x10 ) v20 = str_kernel32_dll.data.lstr; vigenere_decrypt(v20, str_kernel32_dll.size, "!?>d*" , 5); v22 = ( char *)&str_RtlFillMemory; if ( str_RtlFillMemory.cap >= 0x10 ) v22 = str_RtlFillMemory.data.lstr; vigenere_decrypt(v22, str_RtlFillMemory.size, "?x)da" , v21); v23 = ( char *)&str_RtlFillMemory; if ( str_RtlFillMemory.cap >= 0x10 ) v23 = str_RtlFillMemory.data.lstr; v24 = ( char *)&str_kernel32_dll; if ( str_kernel32_dll.cap >= 0x10 ) v24 = str_kernel32_dll.data.lstr; kernel32_dll = GetModuleHandleA(v24); RtlFillMemory = ( void (__fastcall *)( void *, size_t , int ))GetProcAddress(kernel32_dll, v23); if ( !RtlFillMemory || write_memory(ProcessHandle, alloced + 500) ) { // string dtor return 1i64; } if ( NtCreateThreadEx( &hThread, 0x1FFFFFu, 0i64, ProcessHandle, (LPTHREAD_START_ROUTINE)ExitThread, 0i64, 1u, 0i64, 0i64, 0i64, 0i64) ) { // string dtor return 1; } NtQueueApcThread(hThread, (PIO_APC_ROUTINE)(alloced + 500), 0i64, 0i64, 0i64); ResumeThread(hThread); NtWaitForSingleObject(hThread, 0, 0i64); CloseHandle(hThread); *(_DWORD *)Str1 = 0; *(_DWORD *)Destination = 0; strncpy (Destination, ( const char *)alloced, 3ui64); strncpy (Str1, ( const char *)alloced + 67, 3ui64); if ( ! strcmp (Str1, "110" ) ) { v40 = ( char *)&unk_140006590; } else { if ( strcmp (Str1, "120" ) ) ExitProcess(0); v40 = ( char *)&byte_140006598; } vigenere_decrypt(Destination, 3, v40, 3); printf ( "\n%s" , Destination); // string dtor return 0i64; } int __fastcall write_memory( HANDLE ProcessHandle, unsigned __int8 *ptr) { int v4; // edi unsigned __int8 *i; // rsi GetCurrentProcess(); if ( !NtCreateThreadEx( &Handle, 0x1FFFFFu, 0i64, ProcessHandle, (LPTHREAD_START_ROUTINE)ExitThread, 0i64, 1u, 0i64, 0i64, 0i64, 0i64) ) { v4 = 0; for ( i = unk_140008050; !NtQueueApcThread(Handle, (PIO_APC_ROUTINE)RtlFillMemory, &ptr[v4], 1i64, *i); ++i ) { if ( (unsigned int )++v4 >= 0x92B ) { ResumeThread(Handle); NtWaitForSingleObject(Handle, 0, 0i64); CloseHandle(Handle); return 0; } } TerminateThread(Handle, 0); CloseHandle(Handle); } return 1; } |
常规流程,提示后输入,之后和 "kctf" 以及两个 main 之前初始化的全局变量字符串拼接 "kctf" + str1 + input + str2
,之后写入到分配的一段内存上,再调用 write_memory 函数向 +500 的位置写入 shellcode ,再用 NtQueueApcThread 执行这段代码。 write_memory 中还有个反调试,去掉就行(上面是已经去掉的)。
write_memory 中可以看到, shellcode 的位置在 0x140008050 ,直接跳转过去,第一条 call 是花指令,跳转到当前地址 +4 的位置,处理后如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .data: 0000000140008054 inc eax .data: 0000000140008056 pop rdi .data: 0000000140008057 mov ecx, 1010806h .data: 000000014000805C xor ecx, 1010101h ; 0x907 .data: 0000000140008062 add rdi, 1Eh ; 0x140008073 .data: 0000000140008066 xor esi, esi .data: 0000000140008068 cld .data: 0000000140008069 .data: 0000000140008069 loc_140008069: ; CODE XREF: .data: 0000000140008071 ↓j .data: 0000000140008069 mov al, [rdi] .data: 000000014000806B cmp al, 17h .data: 000000014000806D cmovz eax, esi .data: 0000000140008070 stosb .data: 0000000140008071 loop loc_140008069 |
这一小段循环会将后面指令中的所有 0x17 全部改成 0 ,写个脚本去掉就行,在 0x140008073 处生成函数 F5 :
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 | void __fastcall check() { HANDLE (__fastcall *CreateToolhelp32Snapshot)( DWORD , DWORD ); // rbx BOOL (__stdcall *Process32First)( HANDLE , LPPROCESSENTRY32); // r13 BOOL (__stdcall *CloseHandle)( HANDLE ); // r15 DWORD (__stdcall *GetCurrentProcessId)(); // r12 HANDLE v4; // rdi bool v5; // si HANDLE v6; // rax void *v7; // r14 BOOL i; // eax char *v11; // rbx DWORD v12; // eax _BYTE *v13; // rdx _BYTE *v14; // rbx struct _MEMORY_BASIC_INFORMATION Buffer; // [rsp+48h] [rbp-2B0h] BYREF PROCESSENTRY32 v16[2]; // [rsp+80h] [rbp-278h] BYREF BOOL (__stdcall *Process32Next)( HANDLE , LPPROCESSENTRY32); // [rsp+300h] [rbp+8h] MAPDST HANDLE (__stdcall *OpenProcess)( DWORD , BOOL , DWORD ); // [rsp+308h] [rbp+10h] MAPDST SIZE_T (__stdcall *VirtualQueryEx)( HANDLE , LPCVOID , PMEMORY_BASIC_INFORMATION, SIZE_T ); // [rsp+310h] [rbp+18h] CreateToolhelp32Snapshot = ( HANDLE (__fastcall *)( DWORD , DWORD ))find_proc(0xF88DDF46); OpenProcess = ( HANDLE (__stdcall *)( DWORD , BOOL , DWORD ))find_proc(0xFD0B55A7); VirtualQueryEx = ( SIZE_T (__stdcall *)( HANDLE , LPCVOID , PMEMORY_BASIC_INFORMATION, SIZE_T ))find_proc(0x242E6FF); Process32First = ( BOOL (__stdcall *)( HANDLE , LPPROCESSENTRY32))find_proc(0x3F347695); Process32Next = ( BOOL (__stdcall *)( HANDLE , LPPROCESSENTRY32))find_proc(0x93E12339); CloseHandle = ( BOOL (__stdcall *)( HANDLE ))find_proc(0x1CA655F1); GetCurrentProcessId = ( DWORD (__stdcall *)())find_proc(0x35634E1); v4 = 0i64; v16[0].dwSize = 568; v5 = 0; v6 = CreateToolhelp32Snapshot(2i64, 0i64); v7 = v6; if ( v6 != ( HANDLE )-1i64 ) { for ( i = Process32First(v6, v16); i; i = Process32Next(v7, v16) ) { if ( v16[0].th32ProcessID == GetCurrentProcessId() ) { v4 = OpenProcess(0x2000000u, 0, v16[0].th32ProcessID); if ( v4 ) { v11 = 0i64; while ( VirtualQueryEx(v4, v11, &Buffer, 0x30ui64) ) { v11 = ( char *)Buffer.BaseAddress + Buffer.RegionSize; if ( Buffer.State == 4096 && Buffer.AllocationProtect == 0x40 ) { v12 = GetCurrentProcessId(); v13 = Buffer.BaseAddress; if ( v16[0].th32ProcessID == v12 ) v5 = check_header(*(_DWORD *)Buffer.BaseAddress); if ( v5 ) { v14 = v13 + 4; if ( check_sudoku(v13 + 4) ) { *(v14 - 4) = 0x69; *(v14 - 3) = 0x6F; *(v14 - 2) = 0x20; *(v14 - 1) = 0; v14[63] = 0x31; v14[64] = 0x31; } else { *(v14 - 4) = 0x6D; *(v14 - 3) = 0x6A; *(v14 - 2) = 0x29; *(v14 - 1) = 0; v14[63] = 49; v14[64] = 50; } v14[65] = 48; goto LABEL_19; } *v13 = 109; v13[1] = 106; v13[2] = 41; v13[3] = 0; v13[67] = 49; v13[68] = 50; v13[69] = 48; } } } } } LABEL_19: CloseHandle(v7); CloseHandle(v4); } } |
check_header 函数就检测头部的 "kctf" ,通过这种方式找到分配的这段内存,之后进入 flag 的判断(数独的判断)
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 | char __fastcall check_sudoku( char *flag) { unsigned int v2; // ebx int v3; // ebx int v4; // edi v2 = 0; while ( check_line(flag, v2) && check_column(flag, v2) ) { if ( ( int )++v2 >= 9 ) { v3 = 0; LABEL_6: v4 = 0; while ( check_grid(flag, v3, v4) ) { v4 += 3; if ( v4 >= 9 ) { v3 += 3; if ( v3 < 9 ) goto LABEL_6; return 1; } } return 0; } } return 0; } char __fastcall check_line( char *flag, unsigned int y) { int x; // ebx unsigned int v5; // eax int v7[9]; // [rsp+20h] [rbp-38h] *(_OWORD *)v7 = 0i64; v7[8] = 0; x = 0; *(_OWORD *)&v7[4] = 0i64; while ( 1 ) { v5 = get_value(flag, y, x) - 1; if ( v5 > 8 || v7[v5] ) break ; ++x; v7[v5] = 1; if ( x >= 9 ) return 1; } return 0; } char __fastcall check_column( char *flag, unsigned int x) { int y; // ebx unsigned int v5; // eax int v7[9]; // [rsp+20h] [rbp-38h] *(_OWORD *)v7 = 0i64; v7[8] = 0; y = 0; *(_OWORD *)&v7[4] = 0i64; while ( 1 ) { v5 = get_value(flag, y, x) - 1; if ( v5 > 8 || v7[v5] ) break ; ++y; v7[v5] = 1; if ( y >= 9 ) return 1; } return 0; } char __fastcall check_grid( char *flag, int y0, int x0) { int y_offset; // edi int x_offset; // ebx unsigned int v8; // eax int v10[9]; // [rsp+20h] [rbp-38h] *(_OWORD *)v10 = 0i64; v10[8] = 0; y_offset = 0; *(_OWORD *)&v10[4] = 0i64; while ( 2 ) { for ( x_offset = 0; x_offset < 3; ++x_offset ) { v8 = get_value(flag, y_offset + y0, x_offset + x0) - 1; if ( v8 > 8 || v10[v8] ) return 0; v10[v8] = 1; } if ( ++y_offset < 3 ) continue ; break ; } return 1; } |
进入 check_sudoku 就很明显的能看到,完全就是数独的判断逻辑。最后就只剩下一个函数 get_value ,这里会得到输入的格式的判断。 get_value 里的逻辑比较乱,简单整理一下函数行为:每次读取输入的 3 个字节,必须是数字或者大写的 A-F ,之后这三个字符计算得到 16 进制的数值,10进制表示的个位、十位、百位分别是数独中的 x, y, sudoku[y][x]
。当取到的 y 和 x 与传入的参数相符合时,就会结束循环。如果当前取到的输入的下标不超过 63 (第 21 组内,即原 str1 的范围内),则直接返回;否则会取后 120 字节进行判断,其中找到与 9 * y + x
相等的 2 字节对应的 10 进制数,并且这个下标和要和前一部分的每 3 字节的下标相差 21 (即跳过前面的 21 组),最后还会判断总长度是 243 + 120 保证没有多余字节。
所以前面的 63 字节是给出数独中已有的值,后 120 字节指定了输入的数独格子的顺序。直接套用 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 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 | #!/usr/bin/env python3 ''' def decrypt(a, b): return bytes(a[i] ^ b[i % len(b)] for i in range(len(a))) print(decrypt(bytes.fromhex('76144E3F0B43584E300C430A0B2717530A0B351D5F42'), b'&x+^x')) print(decrypt(bytes.fromhex('41104A56'), b'*s>0?')) print(decrypt(bytes.fromhex('4A5A4C0A4F4D0C0C4A4E4D53'), b'!?>d*')) print(decrypt(bytes.fromhex('6D0C4522085314' + '64010C500A50'), b'?x)da')) ''' sudoku = [] for i in range ( 9 ): sudoku.append([]) for j in range ( 9 ): sudoku[ - 1 ].append( 0 ) known1 = '3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E6' known2 = '677116575313142309154604431859253431473963507533496829080645035455771774602058076430276921790210013736267644383505517280' for i in range ( 0 , len (known1), 3 ): v = int (known1[i: i + 3 ], 16 ) x, y, v = v % 10 , (v / / 10 ) % 10 , v / / 100 # print('(%d, %d) -> %d' % (y, x, v)) sudoku[y][x] = v from z3 import * # 9x9 matrix of integer variables X = [ [ Int ( "x_%s_%s" % (i + 1 , j + 1 )) for j in range ( 9 ) ] for i in range ( 9 ) ] # each cell contains a value in {1, ..., 9} cells_c = [ And( 1 < = X[i][j], X[i][j] < = 9 ) for i in range ( 9 ) for j in range ( 9 ) ] # each row contains a digit at most once rows_c = [ Distinct(X[i]) for i in range ( 9 ) ] # each column contains a digit at most once cols_c = [ Distinct([ X[i][j] for i in range ( 9 ) ]) for j in range ( 9 ) ] # each 3x3 square contains a digit at most once sq_c = [ Distinct([ X[ 3 * i0 + i][ 3 * j0 + j] for i in range ( 3 ) for j in range ( 3 ) ]) for i0 in range ( 3 ) for j0 in range ( 3 ) ] sudoku_c = cells_c + rows_c + cols_c + sq_c ''' # sudoku instance, we use '0' for empty cells instance = ((0,0,0,0,9,4,0,3,0), (0,0,0,5,1,0,0,0,7), (0,8,9,0,0,0,0,4,0), (0,0,0,0,0,0,2,0,8), (0,6,0,2,0,1,0,5,0), (1,0,2,0,0,0,0,0,0), (0,7,0,0,0,0,5,2,0), (9,0,0,0,6,5,0,0,0), (0,4,0,9,7,0,0,0,0)) ''' instance = sudoku instance_c = [ If(instance[i][j] = = 0 , True , X[i][j] = = instance[i][j]) for i in range ( 9 ) for j in range ( 9 ) ] s = Solver() s.add(sudoku_c + instance_c) # print('checking') if s.check() = = sat: # print('OK') m = s.model() r = [ [ m.evaluate(X[i][j]).as_long() for j in range ( 9 ) ] for i in range ( 9 ) ] else : print ( "failed to solve" ) flag = '' for i in range ( 0 , len (known2), 2 ): v = int (known2[i: i + 2 ]) y, x = v / / 9 , v % 9 # print(y, x) flag + = hex (r[y][x] * 100 + y * 10 + x)[ 2 : ].rjust( 3 , '0' ) print (flag.upper()) # 11230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120 |
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界