-
-
[原创] 看雪 2023 KCTF 年度赛 第六题 至暗时刻
-
2023-9-15 03:50 3061
-
main函数通过beginthreadex启动的sub_140001630(重命名为mainfunction)是真正的主逻辑所在。
IDA 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 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 | struct string { char *s; __declspec (align(16)) __int64 size; __int64 capacity; }; // sub_140001630 __int64 mainfunction() { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] std_string_assign(&userinput, byte_140006428); std_string_assign(&Format, aV); std_string_assign(&lpModuleName, aJzlOm); std_string_assign(&lpProcName, aM); std_string_assign(&str_kctf, aA); std_string_append(&lpProcName, aD_0); std_string_assign(&Block, "7PK:" ); p_Format = ( char *)&Format; if ( Format.capacity >= 0x10ui64 ) p_Format = Format.s; str_xor(p_Format, Format.size, "&x+^x" , 5); // Please enter your key: s = ( char *)&Format; if ( Format.capacity >= 0x10ui64 ) s = Format.s; printf (s); v3 = userinput.size; if ( userinput.size < 0x1F4ui64 ) { v5 = 500 - userinput.size; if ( (unsigned __int64 )(500 - userinput.size) > userinput.capacity - userinput.size ) { sub_140002398(&userinput, 500 - userinput.size, v2, 500 - userinput.size); } else { userinput.size = 500i64; p_userinput = ( char *)&userinput; if ( userinput.capacity >= 0x10ui64 ) p_userinput = userinput.s; v7 = &p_userinput[v3]; memset (v7, 0, 500 - v3); v7[v5] = 0; } } else { userinput.size = 500i64; v4 = ( char *)&userinput; if ( userinput.capacity >= 0x10ui64 ) v4 = userinput.s; v4[500] = 0; } v8 = ( char *)&userinput; if ( userinput.capacity >= 0x10ui64 ) v8 = userinput.s; scanf ( "%s" , v8); v9 = ( char *)&userinput; if ( userinput.capacity >= 0x10ui64 ) v9 = userinput.s; v10 = &global_string1; std_string_append(&global_string1, v9); v12 = &global_string2; if ( global_string2.capacity >= 0x10ui64 ) v12 = global_string2.s; v13 = global_string2.size; v14 = global_string1.size; if ( global_string2.size > (unsigned __int64 )(global_string1.capacity - global_string1.size) ) { std_string_realloc_append(( void **)&global_string1.s, global_string2.size, v11, v12, global_string2.size); } else { global_string1.size += global_string2.size; v15 = ( char *)&global_string1; if ( global_string1.capacity >= 0x10ui64 ) v15 = global_string1.s; v16 = &v15[v14]; memmove (v16, v12, global_string2.size); v16[v13] = 0; } p_strkctf = &str_kctf; if ( str_kctf.capacity >= 0x10ui64 ) p_strkctf = str_kctf.s; str_xor(p_strkctf, str_kctf.size, "*s>0?" , 5); // kctf if ( (unsigned __int64 )(0x7FFFFFFFFFFFFFFFi64 - str_kctf.size) < global_string1.size ) sub_140001284(); v19 = &str_kctf; if ( str_kctf.capacity >= 0x10ui64 ) v19 = str_kctf.s; v20 = &global_string1; if ( global_string1.capacity >= 0x10ui64 ) v20 = global_string1.s; std_string_concat(( struct string *)Src, str_kctf.size, v18, v19, str_kctf.size, v20, global_string1.size); if ( global_string1.capacity >= 0x10ui64 ) { v21 = global_string1.s; if ( (unsigned __int64 )(global_string1.capacity + 1) >= 0x1000 ) { if ( (unsigned __int64 )&global_string1.s[-*((_QWORD *)global_string1.s - 1) - 8] > 0x1F ) invalid_parameter_noinfo_noreturn(); v21 = ( char *)*((_QWORD *)global_string1.s - 1); } j_j_free(v21); } global_string1.size = 0i64; global_string1.capacity = 15i64; LOBYTE(global_string1.s) = 0; memcpy (&global_string1, Src, sizeof (global_string1)); CurrentProcess = GetCurrentProcess(); v59 = 5500i64; LODWORD(Size) = 0x3000; maybe_virtualallocex(( __int64 )CurrentProcess, ( __int64 )&Source, 0i64, ( __int64 )&v59, Size); if ( global_string1.capacity >= 0x10ui64 ) v10 = global_string1.s; maybe_writeprocessmemory(( __int64 )CurrentProcess, ( __int64 )Source, ( __int64 )v10, global_string1.size, ( __int64 )v60); p_lpModuleName = ( char *)&lpModuleName; if ( lpModuleName.capacity >= 0x10ui64 ) p_lpModuleName = lpModuleName.s; str_xor(p_lpModuleName, lpModuleName.size, "!?>d*" , 5); // kernel32.dll p_lpProcName = ( char *)&lpProcName; if ( lpProcName.capacity >= 0x10ui64 ) p_lpProcName = lpProcName.s; str_xor(p_lpProcName, lpProcName.size, "?x)da" , v24); // RtlFillMemory v26 = ( char *)&lpProcName; if ( lpProcName.capacity >= 0x10ui64 ) v26 = lpProcName.s; v27 = ( char *)&lpModuleName; if ( lpModuleName.capacity >= 0x10ui64 ) v27 = lpModuleName.s; ModuleHandleA = GetModuleHandleA(v27); RtlFillMemory_ = ( __int64 )GetProcAddress(ModuleHandleA, v26); if ( !RtlFillMemory_ || (v36 = Source, (unsigned int )has_antidebug2_(( __int64 )CurrentProcess, ( __int64 )(Source + 0x1F4))) ) { // COLLAPSED destructors // ... } sub_140002E4E(( __int64 )hThread, ( __int64 )(v36 + 0x1F4), 0i64, 0i64, 0i64, 0i64, 1, 0i64, 0i64, 0i64, 0i64); ResumeThread(hThread); sub_140002A8E(( __int64 )hThread, 0i64, 0i64, v43, Sizea); CloseHandle(hThread); *(_DWORD *)Str1 = 0; *(_DWORD *)Destination = 0; strncpy (Destination, Source, 3ui64); strncpy (Str1, Source + 67, 3ui64); if ( ! strcmp (Str1, "110" ) ) { v44 = &unk_140006590; } else { if ( strcmp (Str1, "120" ) ) ExitProcess(0); v44 = &unk_140006598; } str_xor(Destination, 3, v44, 3); printf ( "\n%s" , Destination); // COLLAPSED destructors // ... return 0i64; } |
定义出std::string的结构体和相关函数,然后排除掉析构函数的干扰,主体逻辑并不复杂(常量字符串大多用sub_1400013B0(str_xor)函数异或加密了,聊胜于无):获取输入(userinput),依次拼接"kctf"、global_string1、userinput、global_string2四部分为一个大字符串(global_string1和global_string2是两个全局string,分别在sub_140001000和sub_140001028初始化。其中global_string1长63字节,包含0-9和A-F,global_string2长120字节,只包含0-9)。
对于拼接后的大字符串,分别经过了sub_140002BBA(maybe_virtualallocex)和sub_140002DA9(maybe_writeprocessmemory)两个函数处理。这两个函数内部都是直接调用syscall指令执行系统调用,且调用号(eax)也是经过复杂的运算得出的。
(题目不支持win11的原因来源如此:windows的系统调用在用户态的接口封装在ntdll.dll中,而内核的系统调用号码和参数结构等并不属于公开的稳定api,随着系统版本更新也会有变化,这一点与linux完全不同)
此时最简单的方法是动态调试看这两个函数的具体行为是什么。在此之前,先留意程序是否存在反调试。sub_140001320和sub140001338是两个tlscallback,里面做了很明显的IsDebuggerPresent和NtQueryInformationProcess反调试检测,且如果检测到调试器,会调用sub_140001298对以"kctf"开头的内存页做修改。此外,在sub_140001450(has_antidebug2_,被sub_140001630(mainfunction)调用)函数也有反调试(CheckRemoteDebuggerPresent)。将这些地方全部patch掉。
开一台win10虚拟机调试(在win11上运行则程序不会有最后ok!或no!的输出),对于和sub_140002DA9(maybe_writeprocessmemory)函数,观察到第二个参数Source的值通常为0x1F0000,是一块分配出来的内存区域,且调用这个函数之后后会把拼接了常量和用户输入的大字符串写入开头(此时,偏移量为0的地方是"kctf",偏移量为4的地方是global_string1,然后紧跟的是userinput和global_string2)。
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 | __int64 __fastcall has_antidebug2_( __int64 a1, __int64 a2) { HANDLE CurrentProcess; // rax __int64 (**v4)(); // rax unsigned int v5; // edi unsigned __int8 *i; // rsi __int64 v7; // r9 __int64 v9; // [rsp+20h] [rbp-60h] unsigned int *ThrdAddr; // [rsp+28h] [rbp-58h] int v11; // [rsp+30h] [rbp-50h] __int64 v12; // [rsp+38h] [rbp-48h] __int64 v13; // [rsp+40h] [rbp-40h] __int64 v14; // [rsp+48h] [rbp-38h] __int64 v15; // [rsp+50h] [rbp-30h] unsigned int v16[4]; // [rsp+60h] [rbp-20h] BYREF _Thrd_t v17; // [rsp+70h] [rbp-10h] BYREF BOOL pbDebuggerPresent; // [rsp+B8h] [rbp+38h] BYREF pbDebuggerPresent = 0; CurrentProcess = GetCurrentProcess(); CheckRemoteDebuggerPresent(CurrentProcess, &pbDebuggerPresent); if ( pbDebuggerPresent ) { v4 = ( __int64 (**)())operator new (8ui64); *v4 = antidebug1_; *(_QWORD *)v16 = beginthreadex(0i64, 0, (_beginthreadex_proc_type)StartAddress, v4, 0, &v16[2]); if ( !*(_QWORD *)v16 ) { v16[2] = 0; std::_Throw_Cpp_error(6); LABEL_15: TerminateThread(hObject, 0); CloseHandle(hObject); return 1i64; } if ( !v16[2] ) { std::_Throw_Cpp_error(1); __debugbreak(); } if ( v16[2] == Thrd_id() ) { std::_Throw_Cpp_error(5); __debugbreak(); } v17 = *(_Thrd_t *)v16; if ( Thrd_join(&v17, 0i64) ) std::_Throw_Cpp_error(2); } v15 = 0i64; v14 = 0i64; v13 = 0i64; v12 = 0i64; v11 = 1; ThrdAddr = 0i64; if ( !(unsigned int )sub_140003529() ) { v5 = 0; for ( i = (unsigned __int8 *)&unk_140008050; !(unsigned int )sub_140002E4E( ( __int64 )hObject, RtlFillMemory_, a2 + v5, 1i64, *i, ( __int64 )ThrdAddr, v11, v12, v13, v14, v15); ++i ) { if ( ++v5 >= 0x92B ) { ResumeThread(hObject); sub_140002A8E(( __int64 )hObject, 0i64, 0i64, v7, v9); CloseHandle(hObject); return 0i64; } } goto LABEL_15; } return 1i64; } |
接下来调用了sub_140001450(has_antidebug2_)函数,这里除了反调试,还有一处调用i = (unsigned __int8 *)&unk_140008050; !(unsigned int)sub_140002E4E((__int64)hObject, RtlFillMemory_, a2 + v5, 1i64, *i, (__int64)ThrdAddr, v11, v12, v13, v14, v15);
。其中a2是Source+0x1F4(Source是0x1F0000,即写入了大字符串的内存段)。unk_140008050是.data段的地址,在调用sub_140001450之后观察0x1F0000段的内存,发现unk_140008050处的0x92B个字节被写入了0x1F01F4的位置。
观察发现unk_140008050开始的字节很像x64指令,IDA里按c发现确实如此。随后,mainfunction以0x1F01F4为第二个参数调用了sub_140002E4E,可以猜测sub_140002E4E是启动新的进程,从0x1F01F4开始执行指令。在此之后mainfunction只剩下判断和输出。
直接分析,发现这些指令开头是对后面的自修改,所以继续动态调试到这部分执行完毕(到达0x1F0217)再dump出来。
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 | __int64 sub_1F0217() { __int64 (__fastcall *v0)( __int64 , _QWORD); // rbx __int64 (__fastcall *v1)( __int64 , int *); // r13 void (__fastcall *v2)( __int64 ); // r15 __int64 (*v3)( void ); // r12 __int64 v4; // rdi bool v5; // si __int64 v6; // rax __int64 v7; // r14 int v9; // eax __int64 (__fastcall *v10)( __int64 , int *); // rbx __int64 (__fastcall *v11)( __int64 , _QWORD); // r13 char *v12; // rbx int v13; // eax char *v14; // rdx char *v15; // rbx int *v16; // [rsp+48h] [rbp-2B0h] BYREF int v17; // [rsp+58h] [rbp-2A0h] __int64 v18; // [rsp+60h] [rbp-298h] int v19; // [rsp+68h] [rbp-290h] int v20; // [rsp+80h] [rbp-278h] BYREF int v21; // [rsp+88h] [rbp-270h] __int64 v22; // [rsp+300h] [rbp+8h] __int64 v23; // [rsp+308h] [rbp+10h] __int64 (__fastcall *v24)( __int64 , char *, int **, _QWORD); // [rsp+310h] [rbp+18h] v0 = ( __int64 (__fastcall *)( __int64 , _QWORD))sub_1F0563(-124919994); v23 = sub_1F0563(-49588825); v24 = ( __int64 (__fastcall *)( __int64 , char *, int **, _QWORD))sub_1F0563(37938943); v1 = ( __int64 (__fastcall *)( __int64 , int *))sub_1F0563(1060402837); v22 = sub_1F0563(-1813961927); v2 = ( void (__fastcall *)( __int64 ))sub_1F0563(480663025); v3 = ( __int64 (*)( void ))sub_1F0563(55981281); v4 = 0i64; v20 = 568; v5 = 0; v6 = v0(2i64, 0i64); v7 = v6; if ( v6 == -1 ) return 0xFFFFFFFFi64; v9 = v1(v6, &v20); v10 = ( __int64 (__fastcall *)( __int64 , int *))v22; v11 = ( __int64 (__fastcall *)( __int64 , _QWORD))v23; while ( v9 ) { if ( v21 == (unsigned int )v3() ) // getcurrentprocessid { v4 = v11(0x2000000i64, 0i64); // openprocess if ( v4 ) { v12 = 0i64; while ( 1 ) { do { if ( !v24(v4, v12, &v16, '0' ) ) { v10 = ( __int64 (__fastcall *)( __int64 , int *))v22; v11 = ( __int64 (__fastcall *)( __int64 , _QWORD))v23; goto LABEL_20; } v12 = ( char *)v16 + v18; } while ( v19 != 4096 || v17 != 64 ); v13 = v3(); v14 = ( char *)v16; if ( v21 == v13 ) v5 = sub_1F062F(*v16); if ( v5 ) break ; strcpy (v14, "mj)" ); qmemcpy(v14 + 67, "120" , 3); } v15 = v14 + 4; if ( sub_1F0AA3(v14 + 4) ) { strcpy (v15 - 4, "io " ); memset (v15 + 63, '1' , 2); } else { strcpy (v15 - 4, "mj)" ); qmemcpy(v15 + 63, "12" , 2); } v15[65] = '0' ; break ; } } LABEL_20: v9 = v10(v7, &v20); } v2(v7); return (( __int64 (__fastcall *)( __int64 ))v2)(v4); } |
看到了mainfunction中检测的"120"常量字符串和分支。动态调试发现v14为0x1F0000,所以这个函数前半部分只是找到此块内存区域,sub_1F0AA3是真正的输入校验逻辑。(其参数v14+4,恰好是global_string1+userinput+global_string2三部分的拼接)
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 | char __fastcall sub_1F0AA3( char *a1) { int v2; // ebx int v3; // ebx int v4; // edi v2 = 0; while ( sub_1F093B(a1, v2) && sub_1F09A7(a1, v2) ) { if ( ++v2 >= 9 ) { v3 = 0; LABEL_6: v4 = 0; while ( sub_1F0A13(( __int64 )a1, v3, v4) ) { v4 += 3; if ( v4 >= 9 ) { v3 += 3; if ( v3 < 9 ) goto LABEL_6; return 1; } } return 0; } } return 0; } |
sub_1F0AA3里,先不管调用的三个函数,这里外层的while循环是0-9的遍历,内层的while循环还有3*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 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 | char __fastcall sub_1F093B( char *a1, int a2) { int v2; // ebx unsigned int v5; // eax int v7[8]; // [rsp+20h] [rbp-38h] BYREF int v8; // [rsp+40h] [rbp-18h] memset (v7, 0, sizeof (v7)); v8 = 0; v2 = 0; while ( 1 ) { v5 = sub_1F063B(a1, a2, v2) - 1; if ( v5 > 8 || v7[v5] ) break ; ++v2; v7[v5] = 1; if ( v2 >= 9 ) return 1; } return 0; } char __fastcall sub_1F09A7( char *a1, int a2) { int v2; // ebx unsigned int v5; // eax int v7[8]; // [rsp+20h] [rbp-38h] BYREF int v8; // [rsp+40h] [rbp-18h] memset (v7, 0, sizeof (v7)); v8 = 0; v2 = 0; while ( 1 ) { v5 = sub_1F063B(a1, v2, a2) - 1; if ( v5 > 8 || v7[v5] ) break ; ++v2; v7[v5] = 1; if ( v2 >= 9 ) return 1; } return 0; } char __fastcall sub_1F0A13( char *a1, int a2, int a3) { int v3; // edi int i; // ebx signed int v8; // eax __int128 v10[2]; // [rsp+20h] [rbp-38h] BYREF int v11; // [rsp+40h] [rbp-18h] memset (v10, 0, sizeof (v10)); v11 = 0; v3 = 0; while ( 2 ) { for ( i = 0; i < 3; ++i ) { v8 = sub_1F063B(a1, v3 + a2, i + a3) - 1; if ( (unsigned int )v8 > 8 || *((_DWORD *)v10 + v8) ) return 0; *((_DWORD *)v10 + v8) = 1; } if ( ++v3 < 3 ) continue ; break ; } return 1; } |
调用的三个函数,逻辑高度类似,都是循环检查sub_1F063B的返回值范围在1-9且无重复,所以它们依次是数独的行检测、列检测、九宫检测。
共同调用到的函数是sub_1F063B,从调用方式看,它的后两个参数应该是坐标,返回值是坐标位置的值。所以下一步需要搞清楚拼接后的大字符串的编码逻辑,以及如何映射到数独的格子。
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 | __int64 __fastcall sub_1F063B( char *a1, int a2, int a3) { char *a1_; // rbx int a3_; // r15d __int64 (__fastcall *v5)( char *); // rax int should_be_363; // eax int v7; // r11d int should_be_243; // ebp __int64 v9; // rsi int should_be_81; // r10d __int64 v11; // rdi char *p; // r14 char *tmpbuf; // rdx char v14; // cl int n; // r9d int v16; // r12d int base; // r8d int c; // ecx char v19; // cl int nn; // r12d int v21; // r13d int col; // r12d unsigned int value; // r8d int row; // r13d int v25; // esi char *v26; // r15 int v27; // edi char *v28; // r11 char v29; // dl char *v30; // rcx char v31; // al int another_n; // ebx int v33; // edx char v34; // dl char v36[4]; // [rsp+20h] [rbp-78h] BYREF int v37; // [rsp+24h] [rbp-74h] int should_be_81_; // [rsp+2Ch] [rbp-6Ch] __int64 should_be_243_; // [rsp+30h] [rbp-68h] __int64 v40; // [rsp+38h] [rbp-60h] __int64 v41; // [rsp+40h] [rbp-58h] char v45[4]; // [rsp+B8h] [rbp+20h] BYREF a1_ = a1; a3_ = a3; v5 = ( __int64 (__fastcall *)( char *))sub_1F0563(0xD3A22F6A); should_be_363 = v5(a1_); // ntdll.strlen v7 = 0; v37 = 0; should_be_243 = should_be_363 - 120; v9 = should_be_363 - 120; should_be_243_ = (unsigned int )(should_be_363 - 120); should_be_81 = (should_be_363 - 120) / 3; v41 = v9; should_be_81_ = should_be_81; if ( should_be_363 - 120 <= 0 ) return 0i64; v36[3] = 0; v11 = -2i64 - (_QWORD)a1_; p = a1_ + 2; v40 = -2i64 - (_QWORD)a1_; while ( 1 ) { tmpbuf = v36; v14 = *(p - 2); v36[1] = *(p - 1); n = 0; v36[2] = *p; v16 = 1; v36[0] = v14; base = 16; while ( v14 == ' ' || (unsigned __int8 )(v14 - 9) <= 4u ) v14 = *++tmpbuf; if ( ((v14 - '+' ) & 0xFD) == 0 ) { if ( v14 == '-' ) v16 = -1; goto LABEL_19; } while ( 1 ) { v19 = *tmpbuf; if ( !*tmpbuf ) break ; if ( (unsigned __int8 )(v19 - '0' ) > 9u ) { if ( (unsigned __int8 )(v19 - 'A' ) > 25u ) { if ( (unsigned __int8 )(v19 - 'a' ) > 25u ) break ; c = v19 - 0x57; base = 0; } else { c = v19 - '7' ; } } else { c = v19 - '0' ; } if ( c >= base ) break ; n = c + base * n; LABEL_19: ++tmpbuf; } nn = n * v16; v21 = nn / 10; col = nn % 10; value = v21 / 10; row = v21 % 10; if ( v7 > 20 ) { v25 = 0; v26 = a1_ + 0xF3; v27 = 1; v45[2] = 0; v28 = a1_ + 0xF3; do { v29 = *v28; v30 = v45; v31 = v28[1]; another_n = 0; v45[0] = *v28; v45[1] = v31; while ( v29 == ' ' || (unsigned __int8 )(v29 - 9) <= 4u ) v29 = *++v30; if ( ((v29 - '+' ) & 0xFD) == 0 ) { v27 = 1; if ( v29 == '-' ) v27 = -1; ++v30; } while ( 1 ) { v34 = *v30; if ( !*v30 ) break ; if ( (unsigned __int8 )(v34 - 48) > 9u ) { if ( (unsigned __int8 )(v34 - 65) > 0x19u ) { if ( (unsigned __int8 )(v34 - 97) > 0x19u ) break ; v33 = v34 - 87; } else { v33 = v34 - 55; } } else { v33 = v34 - 48; } if ( v33 >= 10 ) break ; another_n = v33 + 10 * another_n; ++v30; } if ( row + col + 8 * row == another_n * v27 ) break ; v28 += 2; ++v25; } while ( v28 - v26 < 120 ); v7 = v37; should_be_81 = should_be_81_; should_be_243 = should_be_243_; if ( v37 - 21 != v25 ) return value + 9; a1_ = a1; v11 = v40; v9 = v41; a3_ = a3; } if ( should_be_81 > 81 ) return value + 10; if ( should_be_81 < 81 ) return value + 12; if ( should_be_243 != 3 * should_be_81 ) return value + 11; if ( a2 == row && a3_ == col ) return value; p += 3; v37 = ++v7; if ( ( __int64 )&p[v11] >= v9 ) return 0i64; } } |
从调用情况看,sub_1F063B不应返回大于9的值。而sub_1F063B最开始就先获取了长度,但长度的检测却是在循环中完成的(should_be_81和should_be_243)。
这里的参数a1是0x1F0004,指向的内容是global_string1+userinput+global_string2三部分的拼接。
回顾一下,global_string1的长度是63,包含数字和字母,疑似hexencoded,global_string2的长度是120,只包含数字。此处检查strlen(a1)等于363,减去两个常量字符串的63和120,合计用户输入的userinput长度应为180。此时global_string2位于a1的63+180=243=0xF3的偏移处。
大循环的上半部分从a1偏移量0处开始遍历,每3个字符按十六进制解析为3位十进制整数,从百位到个位分别是数值和横纵坐标。另外,当循环次数达到21后,会开启一个新的循环,从a1偏移量0xF3的位置开始,每2个字符按十进制解析为整数,检查是否为横纵坐标所指示的位置。
结合global_string1长度63=21*3,userinput长度180=60*3,且21+60=81=9*9,所以global_string1是数独的21个初始值,userinput是60个待填值。a1的0xF3偏移开始是120个字符的global_string2,而120=60*2,所以global_string2指示的是60个待填值的排列顺序。
提取数独,找一个求解器解出答案,反向编码,即可得到正确的输入:
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 | buf = [[ 0 ] * 9 for _ in range ( 9 )] def parse1(s): r = [] assert len (s) % 3 = = 0 for i in range ( 0 , len (s), 3 ): tmp = int (s[i:i + 3 ], 16 ) col = tmp % 10 row = tmp / / 10 % 10 value = tmp / / 100 r.append((row, col, value)) return r def parse2(s): r = [] assert len (s) % 2 = = 0 for i in range ( 0 , len (s), 2 ): tmp = int (s[i:i + 2 ], 10 ) # col = tmp % 10 # row = tmp // 10 # 最开始这里写错导致多花了两个小时单步调试找bug。。。 col = tmp % 9 row = tmp / / 9 r.append((row, col)) return r seq1 = parse1( "3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E6" ) seq2 = parse2( "677116575313142309154604431859253431473963507533496829080645035455771774602058076430276921790210013736267644383505517280" ) print ( len (seq1), len (seq2)) # 21 60 for c in seq1: print (c) row, col, value = c buf[row][col] = value s = "" for line in buf: print (line) s + = "".join( chr (c + 48 ) for c in line) s + = "\n" print (s) ''' 800000000 003600000 070090200 050007000 000045700 000100030 001000068 008500010 090000400 ''' solution = ''' 812753649 943682175 675491283 154237896 369845721 287169534 521974368 438526917 796318452 ''' ss = solution.replace( "\n" , "") assert len (ss) = = 81 ans = "" for row, col in seq2: vv = ss[row * 9 + col] v = ord (vv) - 48 tmp = v * 100 + row * 10 + col t = f "{tmp:03X}" print (tmp, t) ans + = t print ( len (ans)) # 180 print (ans) # 11230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120 |
最终的正确答案:
1 | 11230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120 |
(p.s. 看到有人发现了多解,按照上面的分析似乎只有大小写可能导致多解(毕竟,限制了输入长度,其实就已经排除了大部分意料之外的多解)?但是尝试把答案全部小写却不能通过程序检查。所以可能还有其他的问题(数独多解?)?不深究了)
(p.s. 赛程过半,今年的KCTF可比往年卷多了。。。)
[培训]科锐逆向工程师培训 48期预科班将于 2023年10月13日 正式开班