-
-
[原创] 看雪 2023 KCTF 年度赛 第三题 秘密计划
-
2023-9-7 07:05 4256
-
为什么下载附件会被chrome认为危险拦截掉?!(目前三道题的附件都这样……)
Windows GUI程序,一个输入框,输入长度是32位。
IDA打开,41347个函数,看一眼字符串至少用了 soui4、cryptopp、unicorn等库,而且还是C++编写的,各种间接跳转虚函数调用等。虽然没有混淆,但本身程序的复杂性使得如何排除干扰找到真正的校验逻辑变成了一件并不轻松的事,特别是如何避免花费大量时间逆向结果是一个库函数。
这个程序编译C++时保留了RTTI信息,因此IDA把所有的虚表重命名了,方便了后面看伪代码对虚表赋值的地方猜测函数的作用。这些大部分都是CrytoPP的函数。
先学习soui4框架再从WinMain函数正向深入显然不适合短期解题。
注意到随着exe还有一个code.dat文件,程序中一定会打开它,然而直接搜索"code.dat"字符串找不到结果。换一个方向,在IDA中查找CreateFileW
的交叉引用,只有十个左右的条目,顺次看下来,在sub_40DA90
的伪代码中看到了L"code.dat"
,确认了这个函数出题人写的而不是库函数。
程序中大量使用了std::string,在IDA中定义出它的结构体(大小是6个DWORD共24字节,后两个DWORD是size和capacity,第一个DWORD是指向实际char *的指针,或者长度小于16时内联到结构体的前16字节),会让F5的伪代码观感提升不少,更重要的是局部变量的交叉引用能更准确。
粗略看一遍sub_40DA90
,直观上是读入了code.dat文件,前8字节和后面的字节分别存成两个string,各自做了一些运算(跑了ClassInformer插件后,一些函数点进去后,虚表赋值的地方能看到CryptoPP的类名和函数名,至少有AES、HexDecode、CRC32等)。
合理猜测code.dat的前8字节是hexencode后的crc32,而后面的字节是加密过的不知道什么东西,中间的逻辑可能是解密然后crc校验,通过后又做了一些不知道什么的事。
虽然找到了sub_40DA90
作为突破口,但是这里并没有获取输入的地方。
向上找,它的调用者sub_40CF80
有很多SendMessageW
;再上一级的调用者是sub_40CF60
,是通过QueueUserWorkItem
在其他线程中调用的sub_40CF80
。而sub_40CF60
本身在虚表中,通过静态分析很难再找到它的调用者了。
开始动态调试,但调试器附加后程序总是闪退,基本断定程序有反调试。简单试了x32dbg ScallyHide的几个默认选项发现无效,而一时间又很难静态分析从海量函数中找到反调试所在,初次陷入僵局。
在不断尝试中发现如果在sub_40CF60
调用QueueUserWorkItem
之前下断点,程序不会闪退。在此处查看调用栈,找到了它的调用者是sub_4061D0
。这个函数有两处MessageBox
分别是L"验证成功"
和验证失败
,还有1146、1147、1148、1149、1150等几个常数的分支判断,所以这很可能是一个消息处理函数,几个常数对应几种不同的消息。
其中1149直接导向“验证失败”,到达“验证成功”则需要1147以及一个额外的判断。
回看sub_40CF80
,里面有SendMessage
1147、1149、1150等,所以接下来期望找到1147的条件。
而sub_40CF80
的逻辑十分复杂,再加上调用了大量CrytoPP的密码学函数,要进一步分析还是依赖动态调试。而且,最难受的是,到现在仍然没有找到获取输入的地方。但无论如何,反调试问题仍然要先解决掉。
注意到程序在sub_40CF60
调用QueueUserWorkItem
之后才会闪退,所以可以把这里patch为直接调用sub_40CF80
函数,不会影响逻辑,且调试器附加后程序不会闪退了。
依靠不断调试和猜,一点点梳理程序的逻辑。
从sub_4061D0
开始:(重名了一些函数)
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 | // sub_4061D0 int __userpurge handle_window_message@<eax>( int a1@<ecx>, int a2@<ebp>, int a3@<edi>, int a4, _OWORD *input_wchars, int a6) { char *p_input_sha256; // edx void *s; // ecx char *v9; // edx char *v10; // edx int v11; // eax unsigned __int8 v12; // al char v13; // al int v15; // [esp-54h] [ebp-60h] unsigned __int8 v17; // [esp-45h] [ebp-51h] struct string input_sha256; // [esp-44h] [ebp-50h] BYREF struct string v19; // [esp-2Ch] [ebp-38h] BYREF int *v20; // [esp-10h] [ebp-1Ch] struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; // [esp-Ch] [ebp-18h] void *v22; // [esp-8h] [ebp-14h] int v23; // [esp-4h] [ebp-10h] int v24[2]; // [esp+0h] [ebp-Ch] BYREF int v25; // [esp+8h] [ebp-4h] BYREF int retaddr; // [esp+Ch] [ebp+0h] v24[0] = a2; v24[1] = retaddr; v23 = -1; v22 = &loc_C0D4D5; ExceptionList = NtCurrentTeb()->NtTib.ExceptionList; v20 = &v25; if ( a4 != 1146 ) { if ( a4 == 1147 ) { if ( *(_BYTE *)(a1 + 652) ) MessageBoxW(*( HWND *)(a1 + 28), L "验证成功!" , L "提示" , 0); else (*( void (__stdcall **)( int , int , _DWORD, _DWORD))(*(_DWORD *)a1 + 176))(a1, 1149, 0, 0); return 0; } if ( a4 == 1149 ) { MessageBoxW(*( HWND *)(a1 + 28), L "验证失败!" , L "提示" , 0); return 0; } if ( a4 != 1148 ) { if ( a4 == 1150 && input_wchars ) { *(_OWORD *)(a1 + 628) = *input_wchars; *(_OWORD *)(a1 + 644) = input_wchars[1]; } return 0; } v11 = *(_DWORD *)(a1 + 616); if ( !v11 ) return 0; v12 = (*( int (__stdcall **)( int , int , int ))(*(_DWORD *)(v11 + 4) + 300))(v11 + 4, v15, a3); // sub_A510A7 v17 = v12; if ( v12 < 0x64u ) { if ( v12 <= 0x50u ) { if ( !byte_E43098 ) byte_E43098 = 1; goto LABEL_37; } v13 = byte_E43098; } else { v13 = byte_E43098; if ( byte_E43098 == 1 ) { v13 = 0; byte_E43098 = 0; } } if ( !v13 ) { (*( void (__cdecl **)( int , _DWORD))(*(_DWORD *)(*(_DWORD *)(a1 + 616) + 4) + 296))( *(_DWORD *)(a1 + 616) + 4, (unsigned __int8 )(v17 - 5)); byte_E43098 = 0; return 0; } LABEL_37: (*( void (__cdecl **)( int , _DWORD))(*(_DWORD *)(*(_DWORD *)(a1 + 616) + 4) + 296))( *(_DWORD *)(a1 + 616) + 4, (unsigned __int8 )(v17 + 5)); byte_E43098 = 1; return 0; } if ( input_wchars ) { if ( !*(_WORD *)input_wchars ) // a5 is input wchat_t * { j_j__free(input_wchars); return 0; } sha256sum(&input_sha256, input_wchars, ( int )v24, a6); // sub_406850 sha256 v23 = 0; p_input_sha256 = ( char *)&input_sha256; if ( input_sha256.capacity >= 0x10u ) p_input_sha256 = input_sha256.s; sub_406D20(&v19, p_input_sha256); // v20 may be rsa oaep encrypt result LOBYTE(v23) = 1; if ( v19.size ) { s = &v19; if ( v19.capacity >= 0x10u ) s = v19.s; (*( void (__thiscall **)( int , void *, int , _DWORD))(*(_DWORD *)(a1 + 680) + 8))( a1 + 680, s, v19.size, *(_DWORD *)(a1 + 28)); // call sub_40CF10 simulation_function_8 (*( void (__thiscall **)( int ))(*(_DWORD *)(a1 + 680) + 12))(a1 + 680); // call sub_40CF60 simulation_function_c } else { (*( void (__stdcall **)( int , int , _DWORD, _DWORD))(*(_DWORD *)a1 + 176))(a1, 1149, 0, 0); } if ( v19.capacity >= 0x10u ) { v9 = v19.s; if ( (unsigned int )(v19.capacity + 1) >= 0x1000 ) { v9 = ( char *)*((_DWORD *)v19.s - 1); if ( (unsigned int )(v19.s - v9 - 4) > 0x1F ) goto LABEL_42; } free_(v9); } v19.size = 0; v19.capacity = 15; LOBYTE(v19.s) = 0; if ( input_sha256.capacity < 0x10u ) return 0; v10 = input_sha256.s; if ( (unsigned int )(input_sha256.capacity + 1) < 0x1000 || (v10 = ( char *)*((_DWORD *)input_sha256.s - 1), (unsigned int )(input_sha256.s - v10 - 4) <= 0x1F) ) { free_(v10); return 0; } LABEL_42: _invalid_parameter_noinfo_noreturn(); } return 0; } |
消息为1146时参数是wchat_t *的输入字符串,然后调用了sub_406850
,调试发现输出是转换为char *后的sha256值。继续调用sub_406D20
,它的输出长度高达4096字节,在里面看到了RSA OAEP相关的虚表赋值,猜测这里做了一个RSA OAEP的加密。然后,依次调用了sub_40CF10
和sub_40CF60
函数。
消息为1149时直接提示验证失败
消息为1147时还会额外检查 a1 + 652 不为0才会提示验证成功。但仔细观察,消息为 1150 时输入的offset=24的字节会覆盖这个值。
sub_40CF10 只是一个赋值
sub_40CF60 比较重要,通过 QueueUserWorkItem 调用了 sub_40CF80 函数:
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 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 | // sub_40CF80 // local variable allocation has failed, the output may be wrong! DWORD __userpurge Function@<eax>( int a1@<ebx>, struct simulation_context *lpThreadParameter) { struct string *begin; // esi bool v3; // cf _BYTE *s; // eax struct string *v5; // edi char *v6; // edx char *v7; // edx char *v8; // edx struct simulation_context *v9; // esi char *p_load_code_dat_return_string; // ecx unsigned __int8 *p_userinput_sha256; // edi int v12; // esi char *v13; // edx _BYTE *v14; // eax struct string *v15; // eax char *v16; // ecx unsigned int v17; // edi char *p_inputstr_sha256; // esi char *v19; // esi char *v20; // edx int v21; // esi void *v22; // eax LRESULT (__stdcall *sendmessagew)( HWND , UINT , WPARAM , LPARAM ); // edi char *v24; // edx HWND hwnd; // eax char *v26; // edx unsigned __int64 v28; // [esp-4h] [ebp-1F4h] int v29; // [esp-4h] [ebp-1F4h] unsigned __int64 v30; // [esp+4h] [ebp-1ECh] struct string v31; // [esp+Ch] [ebp-1E4h] BYREF int v32; // [esp+24h] [ebp-1CCh] int v33; // [esp+28h] [ebp-1C8h] int v34; // [esp+2Ch] [ebp-1C4h] int v35; // [esp+30h] [ebp-1C0h] char v36; // [esp+34h] [ebp-1BCh] __int128 v37; // [esp+38h] [ebp-1B8h] int v38; // [esp+48h] [ebp-1A8h] int v39; // [esp+4Ch] [ebp-1A4h] char v40; // [esp+50h] [ebp-1A0h] int v41[18]; // [esp+54h] [ebp-19Ch] BYREF char v42; // [esp+9Ch] [ebp-154h] int *v43; // [esp+A8h] [ebp-148h] void (__cdecl *v44)(); // [esp+ACh] [ebp-144h] int (__cdecl *v45)( int , int ); // [esp+B0h] [ebp-140h] char *p_load_code_dat_return_string_; // [esp+B4h] [ebp-13Ch] int size; // [esp+B8h] [ebp-138h] unsigned __int8 *p_userinput_sha256_; // [esp+BCh] [ebp-134h] int v49; // [esp+C0h] [ebp-130h] int v50; // [esp+C4h] [ebp-12Ch] BYREF int v51; // [esp+C8h] [ebp-128h] LPVOID lpAddress; // [esp+CCh] [ebp-124h] struct simulation_context *v53; // [esp+D0h] [ebp-120h] char iftocheck; // [esp+D7h] [ebp-119h] BYREF void *Block; // [esp+D8h] [ebp-118h] BYREF int v56[39]; // [esp+DCh] [ebp-114h] BYREF struct string userinput_sha256; // [esp+178h] [ebp-78h] BYREF struct string inputstr_sha256; // [esp+190h] [ebp-60h] BYREF struct string load_code_dat_return_string; // [esp+1A8h] [ebp-48h] BYREF WPARAM wParam[2]; // [esp+1C0h] [ebp-30h] OVERLAPPED BYREF struct string tmp; // [esp+1C8h] [ebp-28h] BYREF int v62; // [esp+1ECh] [ebp-4h] v53 = lpThreadParameter; if ( !lpThreadParameter ) return 0; (*( void (__thiscall **)( struct simulation_context *))lpThreadParameter->vtbl)(lpThreadParameter); // sub_40D6A0 simulation_function_0 memset (&load_code_dat_return_string, 0, 20); load_code_dat_return_string.capacity = 15; LOBYTE(load_code_dat_return_string.s) = 0; v62 = 0; begin = lpThreadParameter->uuids.begin; lpAddress = v53->uuids.end; if ( begin != lpAddress ) { while ( 1 ) { v3 = begin->capacity < 0x10u; s = begin; iftocheck = 0; if ( !v3 ) s = begin->s; v5 = ( struct string *)load_code_dat(&tmp, &iftocheck, s); // sub_40DA90 if ( &load_code_dat_return_string != v5 ) { if ( load_code_dat_return_string.capacity >= 0x10u ) { v6 = load_code_dat_return_string.s; if ( (unsigned int )(load_code_dat_return_string.capacity + 1) >= 0x1000 ) { v6 = ( char *)*((_DWORD *)load_code_dat_return_string.s - 1); if ( (unsigned int )(load_code_dat_return_string.s - v6 - 4) > 0x1F ) goto LABEL_76; } free_(v6); } load_code_dat_return_string.size = 0; load_code_dat_return_string.capacity = 15; LOBYTE(load_code_dat_return_string.s) = 0; load_code_dat_return_string = *v5; v5->size = 0; v5->capacity = 15; LOBYTE(v5->s) = 0; } if ( tmp.capacity >= 0x10u ) { v7 = tmp.s; if ( (unsigned int )(tmp.capacity + 1) >= 4096 ) { v7 = ( char *)*((_DWORD *)tmp.s - 1); if ( (unsigned int )(tmp.s - v7 - 4) > 31 ) goto LABEL_76; } free_(v7); } if ( iftocheck ) break ; if ( ++begin == lpAddress ) goto LABEL_17; } if ( load_code_dat_return_string.size ) { v9 = v53; (*( void (__thiscall **)( struct simulation_context *, struct string *))(v53->vtbl + 4))(v53, &userinput_sha256); // sub_40E670 simulation_function_4 // may be rsa decrypt LOBYTE(v62) = 1; if ( !userinput_sha256.size ) // the output v57 is sha256 of user input { SendMessageW(( HWND )v53->hwnd, 1149u, 0, 0); LABEL_68: if ( userinput_sha256.capacity >= 0x10u ) { v26 = userinput_sha256.s; if ( (unsigned int )(userinput_sha256.capacity + 1) >= 0x1000 ) { v26 = ( char *)*((_DWORD *)userinput_sha256.s - 1); if ( (unsigned int )(userinput_sha256.s - v26 - 4) > 0x1F ) goto LABEL_75; } free_(v26); } userinput_sha256.size = 0; userinput_sha256.capacity = 15; LOBYTE(userinput_sha256.s) = 0; goto LABEL_18; } p_load_code_dat_return_string = ( char *)&load_code_dat_return_string; p_userinput_sha256 = (unsigned __int8 *)&userinput_sha256; if ( load_code_dat_return_string.capacity >= 0x10u ) p_load_code_dat_return_string = load_code_dat_return_string.s; p_load_code_dat_return_string_ = p_load_code_dat_return_string; if ( userinput_sha256.capacity >= 0x10u ) p_userinput_sha256 = (unsigned __int8 *)userinput_sha256.s; p_userinput_sha256_ = p_userinput_sha256; size = load_code_dat_return_string.size; iftocheck = 0; if ( p_userinput_sha256 && p_load_code_dat_return_string && load_code_dat_return_string.size ) { lpAddress = VirtualAlloc(0, ( SIZE_T )&loc_A00000, 0x3000u, 0x40u); if ( lpAddress ) { Block = 0; if ( uc_open(1, 0, &Block) ) // sub_44E640 UC_ARCH_ARM = 1 { sendmessagew = SendMessageW; } else { LODWORD(v28) = 17; uc_ctl(( int *)Block, 0x44000007u, v28, v30); memset (&inputstr_sha256, 0, 20); inputstr_sha256.capacity = 15; LOBYTE(inputstr_sha256.s) = 0; v12 = 0; v51 = 0; while ( 1 ) { memset (v56, 0, sizeof (v56)); v50 = p_userinput_sha256[v12]; memset (&v31, 0, sizeof (v31)); v32 = 0; v33 = 0; v37 = 0i64; v34 = 0; v35 = 0; v36 = 0; v38 = 0; v39 = 15; LOBYTE(v37) = 0; LOBYTE(v62) = 5; v40 = -1; sub_412030(v41, v29); v42 = 0; LOBYTE(v62) = 7; memset (&tmp, 0, sizeof (tmp)); std_string_assign__(&tmp, "%02x" , 4u); LOBYTE(v62) = 8; sub_412190(&v31, &tmp); LOBYTE(v62) = 7; if ( tmp.capacity >= 0x10u ) { v13 = tmp.s; if ( (unsigned int )(tmp.capacity + 1) >= 0x1000 ) { v13 = ( char *)*((_DWORD *)tmp.s - 1); if ( (unsigned int )(tmp.s - v13 - 4) > 0x1F ) goto LABEL_76; } free_(v13); } tmp.size = 0; tmp.capacity = 15; LOBYTE(tmp.s) = 0; LOBYTE(v62) = 9; v43 = &v50; v44 = nullsub_1; v45 = sub_414C90; v14 = sub_414130(&v31); sub_410F70(v14); LOBYTE(v62) = 11; sub_40F4A0(&v31); v15 = sub_4111C0(v56, &tmp); // hexencode input_sha256, result is only 2 bytes LOBYTE(v62) = 12; v16 = ( char *)v15; if ( v15->capacity >= 0x10u ) v16 = v15->s; v17 = v15->size; if ( v17 > inputstr_sha256.capacity - inputstr_sha256.size ) { LOBYTE(v49) = 0; sub_40A700(( void **)&inputstr_sha256.s, v17, v49, v16, v17); // / } else { p_inputstr_sha256 = ( char *)&inputstr_sha256; if ( inputstr_sha256.capacity >= 0x10u ) p_inputstr_sha256 = inputstr_sha256.s; v19 = &p_inputstr_sha256[inputstr_sha256.size]; inputstr_sha256.size += v17; memmove (v19, v16, v17); v19[v17] = 0; v12 = v51; } LOBYTE(v62) = 11; if ( tmp.capacity >= 0x10u ) { v20 = tmp.s; if ( (unsigned int )(tmp.capacity + 1) >= 0x1000 ) { v20 = ( char *)*((_DWORD *)tmp.s - 1); if ( (unsigned int )(tmp.s - v20 - 4) > 0x1F ) goto LABEL_76; } free_(v20); } LOBYTE(v62) = 2; sub_40F4A0(v56); v51 = ++v12; if ( v12 >= 32 ) break ; p_userinput_sha256 = p_userinput_sha256_; } uc_mem_map(( int )Block, 0, 0, 0xA00000, 7, ( int )lpAddress); v21 = size; uc_mem_write(( int )Block, 0x43000i64, ( int )p_load_code_dat_return_string_, size); // sub_44E460 v22 = &inputstr_sha256; if ( inputstr_sha256.capacity >= 0x10u ) v22 = inputstr_sha256.s; uc_mem_write(( int )Block, 0x4033i64, ( int )v22, inputstr_sha256.size); // sub_44E460 if ( uc_emu_start(a1, v17, ( int )Block, 0x43000, 0, v21 + 0x43000, 0, 0i64, 0) ) // sub_44DA50 { v9 = v53; sendmessagew = SendMessageW; } else { *(_OWORD *)wParam = 0i64; *(_OWORD *)&tmp.field_8 = 0i64; uc_mem_read(( int )Block, 0x14390u, 0, ( int )wParam); // sub_44E1F0 v9 = v53; sendmessagew = SendMessageW; iftocheck = 1; SendMessageW(( HWND )v53->hwnd, 1150u, ( WPARAM )wParam, 0); // first send, need offset 24 of wParam is 1 } sub_44E300(( int )Block, 0i64, (unsigned int )&loc_A00000); sub_44D470(( char *)Block); if ( inputstr_sha256.capacity >= 0x10u ) { v24 = inputstr_sha256.s; if ( (unsigned int )(inputstr_sha256.capacity + 1) >= 0x1000 ) { v24 = ( char *)*((_DWORD *)inputstr_sha256.s - 1); if ( (unsigned int )(inputstr_sha256.s - v24 - 4) > 0x1F ) LABEL_76: _invalid_parameter_noinfo_noreturn(); } free_(v24); } } VirtualFree(lpAddress, 0, 0x8000u); } else { sendmessagew = SendMessageW; } hwnd = ( HWND )v9->hwnd; if ( iftocheck ) { sendmessagew(hwnd, 1147u, 0, 0); // final check goto LABEL_68; } } else { hwnd = ( HWND )v53->hwnd; sendmessagew = SendMessageW; } sendmessagew(hwnd, 1149u, 0, 0); goto LABEL_68; } } LABEL_17: SendMessageW(( HWND )v53->hwnd, 1149u, 0, 0); LABEL_18: if ( load_code_dat_return_string.capacity >= 0x10u ) { v8 = load_code_dat_return_string.s; if ( (unsigned int )(load_code_dat_return_string.capacity + 1) < 0x1000 || (v8 = ( char *)*((_DWORD *)load_code_dat_return_string.s - 1), (unsigned int )(load_code_dat_return_string.s - v8 - 4) <= 0x1F) ) { free_(v8); return 0; } LABEL_75: _invalid_parameter_noinfo_noreturn(); } return 0; } |
第一步调用 sub_40D6A0 初始化了很多 uuid 常量。
然后循环遍历,作为参数调用 sub_40DA90 函数。如果成功返回了值,下一步用之前RSA OEAP加密的结果调用 sub_40E670,后者完成了解密,调试看到结果就是程序输入值的sha256。
继续跟踪数据流,如果要进入SendMessageW 1147,至少需要调用 sub_44DA50
的地方返回 0,然后还会根据 sub_44E1F0
的调用结果发送一个 1150 消息。回顾 sub_4061D0,1150 消息的offset=24的字节需要不为0才能在进入 1147 时进入“验证成功”的分支。
所以,决定验证通过的关键是 sub_44DA50
和 sub_44E1F0
两个函数。上面贴的伪代码已经把它们标注出来了,分别是 unicorn 的 uc_emu_start 和 uc_mem_read。
而初始化unicorn内存时,0x4033地址用的是程序输入值的sha256;0x43000地址用的是 sub_40DA90 函数的返回值,动态调试可以发现它来自于解密的 code.dat,且它的 crc32 恰好是code.dat的前8字节hexdecode。
往上看,sub_44E640
uc_open 时用的是 UC_ARCH_ARM,所以 unicorn 0x43000 的值是 arm 指令。
实际上,最初完全没有想到过这些函数竟然属于unicorn,因为里面不像CryptoPP一样有虚表暴露了类名,而且很难找到可读的字符串。
(当然,从全局字符串可以知道程序用了unicorn,如果对它的api很敏感或许可以猜出这些函数;但是逆到这里早就没有印象了)
不过,我们可以借助其他工具的力量,例如 BinaryAI (之前在论坛也看到了宣传文:链接)
文件的分析结果:链接
soui、cryptopp 都识别出来了(虽然版本都是错的),但是关键的 unicorn 却丢失了(IDA直接查字符串表就能看到unicorn,这个号称AI加持的平台却漏掉了)
(去平台首页-自定义比对-组件-比对内容,输入github链接,探测出平台对 unicorn 是有收录的,而soui只收录了 soui2 和 soui3 却唯独没有soui4。之前另一篇宣传文章提到的“数万代码仓库、亿级函数特征”、“准确定位开源项目具体版本”看起来还有很大提升空间)
但是,交互式分析 页面还是带来了一些小惊喜。
例如 sub_44E640
、sub_44E1F0
、sub_44E460
等关键函数都被正确识别出来了:uc_open
、uc_mem_read
、uc_mem_write
,连带它上下附近的函数都是 unicorn 相关的函数。(那么,平台首屏的组件列表为什么没展示呢)
(但如果筛选"匹配效果"为"命中",打上绿色Hit标记的函数反而都是soui2的一些奇奇怪怪的函数)
(而且,交互上不能按照十六进制地址搜索(浏览器地址栏的链接倒是能直接跳转到任意函数,但必须是十进制才行)、也不能按g或ctrl+g快速跳转到想要的函数,还是不太方便)
其他识别库函数的思路:
IDA的F.L.I.R.T函数签名:优点是十分精准,缺点是十分精准导致必须找到原始的.lib或.a等静态库才行。如果作者是从源代码自己编译的库,就完全无法发挥作用。
IDA Lumina:...
自己找到库的源代码编译一个二进制,然后用BinDiff或diaphora之类的工具找出匹配的二进制函数迁移符号:存在的问题题是自己编译库比较麻烦,还要确保库的版本和编译选项尽可能与程序接近才能保证最好的效果;而且,BinDiff和diaphora的识别率和准确率都不是很高。(这里再提一下BinaryAI,之前还针对这个场景发布过一个基于大模型的函数语义匹配功能(链接),官方评测效果非常好,不知道实际应用起来怎么样)
回到题目,确定是unicorn模拟执行code.dat解密出来的arm代码后,将其dump出来拖进IDA逆向分析:(基地址重定位到0x43000)
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 | void __noreturn sub_4363C() { int v0; // r1 int v1; // r0 const char *v2; // r2 int v3; // r5 int v4; // r3 int v5; // r4 v0 = 0xC; while ( 1 ) { v1 = 0x4033; v2 = "4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8" ; v3 = 0; --v0; do { v4 = *(_DWORD *)v1; v5 = *(_DWORD *)v2; if ( ++v3 >= 0x10 ) { if ( "4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8" == "6749dae311865d64db83d5ae75bac3c9e36b3" "aa6f24caba655d9682f7f071023" ) { MEMORY[0x14390] = 1; MEMORY[0x143A8] = 1; } LABEL_9: JUMPOUT(0x43754); } v1 += 4; v2 += 4; } while ( v4 == v5 ); if ( !v0 ) goto LABEL_9; } } |
能看到雏形:MEMORY[0x143A8]的赋值就是后面SendMessageW 1150的参数的offset=24的位置,v1的0x4033地址对应程序输入的flag的sha256,而do-while循环很明显是对比两个字符串。
但是,从伪代码看似乎永远无法到达对MEMORY[0x143A8]赋值的地方,这是IDA的反编译错误。
ROM:00043000 ; Processor : ARM ROM:00043000 ; ARM architecture: metaarm ROM:00043000 ; Target assembler: Generic assembler for ARM ROM:00043000 ; Byte sex : Little endian ROM:00043000 ROM:00043000 ; =========================================================================== ROM:00043000 ROM:00043000 ; Segment type: Pure code ROM:00043000 AREA ROM, CODE, READWRITE, ALIGN=0 ROM:00043000 ; ORG 0x43000 ROM:00043000 CODE32 ROM:00043000 8D 01 00 EA B sub_4363C ROM:00043000 ; --------------------------------------------------------------------------- ROM:00043004 65 30 62 63+aE0bc614e4fd035 DCB "e0bc614e4fd035a488619799853b075143deea596c477b8dc077e309c0fe42e9" ROM:00043004 36 31 34 65+ ; DATA XREF: sub_4363C+4↓o ROM:00043004 34 66 64 30+ DCB 0 ROM:00043045 00 DCB 0 ROM:00043046 36 62 38 36+a6b86b273ff34fc DCB "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" ROM:00043046 62 32 37 33+ DCB 0 ROM:00043087 00 DCB 0 ROM:00043088 64 38 62 64+aD8bdf9a0cb27a1 DCB "d8bdf9a0cb27a193a1127de2924b6e5a9e4c2d3b3fe42e935e160c011f3df1fc" ROM:00043088 66 39 61 30+ ; DATA XREF: sub_4363C+10↓o ROM:00043088 63 62 32 37+ DCB 0 ROM:000430C9 00 DCB 0 ROM:000430CA 64 34 37 33+aD4735e3a265e16 DCB "d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35" ROM:000430CA 35 65 33 61+ DCB 0 ROM:0004310B 00 DCB 0 ROM:0004310C 36 37 34 39+a6749dae311865d DCB "6749dae311865d64db83d5ae75bac3c9e36b3aa6f24caba655d9682f7f071023" ROM:0004310C 64 61 65 33+ ; DATA XREF: sub_4363C+1C↓o ROM:0004310C 31 31 38 36+ ; sub_4363C:loc_43724↓o ROM:0004310C 35 64 36 34+ DCB 0 ROM:0004314D 00 DCB 0 ROM:0004314E 65 61 39 36+aEa96b41c1f9365 DCB "ea96b41c1f9365c2c9e6342f5faaeab2a44471efe1e65a2356a974646d2588fd" ROM:0004314E 62 34 31 63+ DCB 0 ROM:0004318F 00 DCB 0 ROM:00043190 35 62 36 35+a5b65712d565c15 DCB "5b65712d565c1551340998102d418ceccb35db8dbfb45f9041c4cae483d8717b" ROM:00043190 37 31 32 64+ ; DATA XREF: sub_4363C+28↓o ROM:00043190 35 36 35 63+ DCB 0 ROM:000431D1 00 DCB 0 ROM:000431D2 34 65 30 37+a4e07408562bedb DCB "4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce" ROM:000431D2 34 30 38 35+ DCB 0 ROM:00043213 00 DCB 0 ROM:00043214 30 33 33 63+a033c339a797554 DCB "033c339a7975542785be7423a5b32fa8047813689726214143cdd7939747709c" ROM:00043214 33 33 39 61+ ; DATA XREF: sub_4363C+34↓o ROM:00043214 37 39 37 35+ DCB 0 ROM:00043255 00 DCB 0 ROM:00043256 34 62 32 32+a4b227777d4dd1f DCB "4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a" ROM:00043256 37 37 37 37+ DCB 0 ROM:00043297 00 DCB 0 ROM:00043298 63 38 31 64+aC81d40dbeed369 DCB "c81d40dbeed369f1476086cf882dd36bf1c3dc35e07006f0bec588b983055487" ROM:00043298 34 30 64 62+ ; DATA XREF: sub_4363C+40↓o ROM:00043298 65 65 64 33+ DCB 0 ROM:000432D9 00 DCB 0 ROM:000432DA 65 66 32 64+aEf2d127de37b94 DCB "ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d" ROM:000432DA 31 32 37 64+ DCB 0 ROM:0004331B 00 DCB 0 ROM:0004331C 39 65 32 35+a9e259b7f6b4c74 DCB "9e259b7f6b4c741937a96a9617b3e6b84e166ff6e925e414e7b72936f5a2a51f" ROM:0004331C 39 62 37 66+ ; DATA XREF: sub_4363C+4C↓o ROM:0004331C 36 62 34 63+ DCB 0 ROM:0004335D 00 DCB 0 ROM:0004335E 65 37 66 36+aE7f6c011776e8d DCB "e7f6c011776e8db7cd330b54174fd76f7d0216b612387a5ffcfb81e6f0919683" ROM:0004335E 63 30 31 31+ DCB 0 ROM:0004339F 00 DCB 0 ROM:000433A0 31 30 34 38+a1048f03db5d45f DCB "1048f03db5d45f654b955eae20d84b72673680fb13b318e7da22e8dce58df21c" ROM:000433A0 66 30 33 64+ ; DATA XREF: sub_4363C+58↓o ROM:000433A0 62 35 64 34+ DCB 0 ROM:000433E1 00 DCB 0 ROM:000433E2 37 39 30 32+a7902699be42c8a DCB "7902699be42c8a8e46fbbb4501726517e86b22c56a189f7625a6da49081b2451" ROM:000433E2 36 39 39 62+ DCB 0 ROM:00043423 00 DCB 0 ROM:00043424 38 66 30 37+a8f0703d406fdb0 DCB "8f0703d406fdb0ea8011d5de342c3aca62214758a8a2b5b8a4e9f1c8c6c42462" ROM:00043424 30 33 64 34+ ; DATA XREF: sub_4363C+64↓o ROM:00043424 30 36 66 64+ DCB 0 ROM:00043465 00 DCB 0 ROM:00043466 32 63 36 32+a2c624232cdd221 DCB "2c624232cdd221771294dfbb310aca000a0df6ac8b66b696d90ef06fdefb64a3" ROM:00043466 34 32 33 32+ DCB 0 ROM:000434A7 00 DCB 0 ROM:000434A8 31 38 32 62+a182b32359558eb DCB "182b32359558eb092511b7166867503ddd83fbe5b42f2545e1903016e721393d" ROM:000434A8 33 32 33 35+ ; DATA XREF: sub_4363C+70↓o ROM:000434A8 39 35 35 38+ DCB 0 ROM:000434E9 00 DCB 0 ROM:000434EA 31 39 35 38+a19581e27de7ced DCB "19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7" ROM:000434EA 31 65 32 37+ DCB 0 ROM:0004352B 00 DCB 0 ROM:0004352C 66 65 35 66+aFe5f0fc640cbbc DCB "fe5f0fc640cbbc113406f042d08cc60ba784c775f7c3299985665323c5fbcdc4" ROM:0004352C 30 66 63 36+ ; DATA XREF: sub_4363C+7C↓o ROM:0004352C 34 30 63 62+ DCB 0 ROM:0004356D 00 DCB 0 ROM:0004356E 34 61 34 34+a4a44dc15364204 DCB "4a44dc15364204a80fe80e9039455cc1608281820fe2b24f1e5233ade6af1dd5" ROM:0004356E 64 63 31 35+ DCB 0 ROM:000435AF 00 DCB 0 ROM:000435B0 34 66 63 38+a4fc82b26aecb47 DCB "4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8" ROM:000435B0 32 62 32 36+ ; DATA XREF: sub_4363C+88↓o ROM:000435B0 61 65 63 62+ DCB 0 ROM:000435F1 00 DCB 0 ROM:000435F2 31 62 61 35+a1ba586c0b89202 DCB "1ba586c0b89202f7307b61f1229330978a843afc98589ffc6a62f209225d3528" ROM:000435F2 38 36 63 30+ DCB 0 ROM:00043633 00 DCB 0 ROM:00043634 66 6C 61 67+aFlags DCB "flags:",0 ; DATA XREF: sub_4363C+94↓o ROM:0004363B 00 DCB 0 ROM:0004363C ROM:0004363C ; =============== S U B R O U T I N E ======================================= ROM:0004363C ROM:0004363C ; Attributes: noreturn ROM:0004363C ROM:0004363C ; void __noreturn sub_4363C() ROM:0004363C sub_4363C ; CODE XREF: ROM:00043000↑j ROM:0004363C 02 DA A0 E3 MOV SP, #0x2000 ROM:00043640 44 00 4F E2+ ADRL R0, aE0bc614e4fd035 ; "e0bc614e4fd035a488619799853b075143deea5"... ROM:00043640 06 0C 40 E2 ROM:00043648 04 00 2D E5 PUSH {R0} ROM:0004364C CC 00 4F E2+ ADRL R0, aD8bdf9a0cb27a1 ; "d8bdf9a0cb27a193a1127de2924b6e5a9e4c2d3"... ROM:0004364C 05 0C 40 E2 ROM:00043654 04 00 2D E5 PUSH {R0} ROM:00043658 54 00 4F E2+ ADRL R0, a6749dae311865d ; "6749dae311865d64db83d5ae75bac3c9e36b3aa"... ROM:00043658 05 0C 40 E2 ROM:00043660 04 00 2D E5 PUSH {R0} ROM:00043664 DC 00 4F E2+ ADRL R0, a5b65712d565c15 ; "5b65712d565c1551340998102d418ceccb35db8"... ROM:00043664 04 0C 40 E2 ROM:0004366C 04 00 2D E5 PUSH {R0} ROM:00043670 64 00 4F E2+ ADRL R0, a033c339a797554 ; "033c339a7975542785be7423a5b32fa80478136"... ROM:00043670 04 0C 40 E2 ROM:00043678 04 00 2D E5 PUSH {R0} ROM:0004367C FB 0F 4F E2 ADR R0, aC81d40dbeed369 ; "c81d40dbeed369f1476086cf882dd36bf1c3dc3"... ROM:00043680 00 00 A0 E1 NOP ROM:00043684 04 00 2D E5 PUSH {R0} ROM:00043688 DD 0F 4F E2 ADR R0, a9e259b7f6b4c74 ; "9e259b7f6b4c741937a96a9617b3e6b84e166ff"... ROM:0004368C 00 00 A0 E1 NOP ROM:00043690 04 00 2D E5 PUSH {R0} ROM:00043694 BF 0F 4F E2 ADR R0, a1048f03db5d45f ; "1048f03db5d45f654b955eae20d84b72673680f"... ROM:00043698 00 00 A0 E1 NOP ROM:0004369C 04 00 2D E5 PUSH {R0} ROM:000436A0 A1 0F 4F E2 ADR R0, a8f0703d406fdb0 ; "8f0703d406fdb0ea8011d5de342c3aca6221475"... ROM:000436A4 00 00 A0 E1 NOP ROM:000436A8 04 00 2D E5 PUSH {R0} ROM:000436AC 83 0F 4F E2 ADR R0, a182b32359558eb ; "182b32359558eb092511b7166867503ddd83fbe"... ROM:000436B0 00 00 A0 E1 NOP ROM:000436B4 04 00 2D E5 PUSH {R0} ROM:000436B8 65 0F 4F E2 ADR R0, aFe5f0fc640cbbc ; "fe5f0fc640cbbc113406f042d08cc60ba784c77"... ROM:000436BC 00 00 A0 E1 NOP ROM:000436C0 04 00 2D E5 PUSH {R0} ROM:000436C4 47 0F 4F E2 ADR R0, a4fc82b26aecb47 ; "4fc82b26aecb47d2868c4efbe3581732a3e7cbc"... ROM:000436C8 00 00 A0 E1 NOP ROM:000436CC 04 00 2D E5 PUSH {R0} ROM:000436D0 A4 10 4F E2 ADR R1, aFlags ; "flags:" ROM:000436D4 00 00 A0 E1 NOP ROM:000436D8 0C 10 A0 E3 MOV R1, #0xC ROM:000436DC ROM:000436DC loc_436DC ; CODE XREF: sub_4363C+E0↓j ROM:000436DC 01 09 A0 E3+ MOV R0, #0x4033 ROM:000436DC 33 00 80 E2 ROM:000436E4 04 20 9D E4 POP {R2} ROM:000436E8 02 80 A0 E1 MOV R8, R2 ROM:000436EC 00 50 A0 E3 MOV R5, #0 ROM:000436F0 01 10 41 E2 SUB R1, R1, #1 ROM:000436F4 ROM:000436F4 loc_436F4 ; CODE XREF: sub_4363C+D8↓j ROM:000436F4 00 30 90 E5 LDR R3, [R0] ROM:000436F8 00 40 92 E5 LDR R4, [R2] ROM:000436FC 01 50 85 E2 ADD R5, R5, #1 ROM:00043700 10 00 55 E3 CMP R5, #0x10 ROM:00043704 06 00 00 AA BGE loc_43724 ROM:00043708 04 00 80 E2 ADD R0, R0, #4 ROM:0004370C 04 20 82 E2 ADD R2, R2, #4 ROM:00043710 04 00 53 E1 CMP R3, R4 ROM:00043714 F6 FF FF 0A BEQ loc_436F4 ROM:00043718 00 00 51 E3 CMP R1, #0 ROM:0004371C EE FF FF 1A BNE loc_436DC ROM:00043720 0A 00 00 EA B loc_43750 ROM:00043724 ; --------------------------------------------------------------------------- ROM:00043724 ROM:00043724 loc_43724 ; CODE XREF: sub_4363C+C8↑j ROM:00043724 62 9E 4F E2 ADR R9, a6749dae311865d ; "6749dae311865d64db83d5ae75bac3c9e36b3aa"... ROM:00043728 00 00 A0 E1 NOP ROM:0004372C 09 00 58 E1 CMP R8, R9 ROM:00043730 06 00 00 1A BNE loc_43750 ROM:00043734 01 18 A0 E3+ MOV R1, #0x14390 ROM:00043734 01 19 81 E2+ ROM:00043734 03 1C 81 E2+ ROM:00043734 90 10 81 E2 ROM:00043744 01 20 A0 E3 MOV R2, #1 ROM:00043748 00 20 81 E5 STR R2, [R1] ROM:0004374C 18 20 81 E5 STR R2, [R1,#0x18] ROM:00043750 ROM:00043750 loc_43750 ; CODE XREF: sub_4363C+E4↑j ROM:00043750 ; sub_4363C+F4↑j ROM:00043750 00 00 A0 E3 MOV R0, #0 ROM:00043750 ; End of function sub_4363C ROM:00043750 ROM:00043750 ; ROM ends ROM:00043750 ROM:00043750 END
从汇编看,v2来自于 0x436E4 的 POP {R2}
指令。随着do-while的每次循环,pop出来的值都会变化。所以,相当于flag的sha256循环与这里硬编码的所有常量比较,如果相等且这个常量是 "6749dae311865d64db83d5ae75bac3c9e36b3aa6f24caba655d9682f7f071023",就会成功对MEMORY[0x143A8]赋值。
总结下来,这段代码的作用就是:检查flag的sha256是"6749dae311865d64db83d5ae75bac3c9e36b3aa6f24caba655d9682f7f071023"
(这里需要一个双重分割线)
WTF ?? 出题人搞笑吧??逆了大半天,逻辑就一个sha256检查,咋可能逆的出来??难道又是爆哈希(第二题的阴影还未散去)?但这啥约束都没有,长度又是32,怎么可能爆的出来??
回想起放题前交流群说题目有氪金和不氪金的两种解法,难道氪金解法是sha256反查?马上打开cmd5,满怀期待的把sha256输进去,结果是“未查到”??那这氪金也氪不出来啊?难道真的如群友所说,氪金解法是直接向出题人买flag??
秉承对题目质量的信任(CTF逆向题出了个给sha256求原值,只在搞笑段子里见过),会不会是程序其他地方还有对flag的额外约束?
回想前面的逆向过程,可能的坑一个是反调试导致分析出的逻辑是错的,另一个是起始的1146消息是谁发的。
在IDA汇编窗口全局搜索47Ah(1146),果然找到了另一处:sub_406490
。
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 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | void __thiscall sub_406490(_DWORD * this ) { int v1; // ecx int v2; // eax const unsigned __int16 *v3; // eax unsigned __int64 v4; // rax int v5; // ebx unsigned __int64 v6; // rax int v7; // edx _DWORD *v8; // ecx int v9; // esi int v10; // ebx int v11; // eax void *s; // edi int v13; // eax int *v14; // eax unsigned __int64 v15; // rax _WORD *v16; // ebx unsigned __int64 v17; // rax int v18; // edx int v19; // edi int v20; // esi unsigned __int64 v21; // rax int v22; // ecx unsigned __int64 v23; // rax __int16 v24; // dx unsigned __int64 v25; // rax int v26; // eax unsigned __int64 v27; // rax int v28; // eax _DWORD *v29; // edi int v30; // ebx char *v31; // eax __int16 *v32; // edx int v33[2]; // [esp+10h] [ebp-78h] BYREF int v34[2]; // [esp+18h] [ebp-70h] BYREF int v35[2]; // [esp+20h] [ebp-68h] BYREF _DWORD *v36; // [esp+28h] [ebp-60h] int v37; // [esp+2Ch] [ebp-5Ch] _DWORD *v38; // [esp+34h] [ebp-54h] int v39; // [esp+38h] [ebp-50h] int v40; // [esp+3Ch] [ebp-4Ch] int v41; // [esp+40h] [ebp-48h] int v42; // [esp+44h] [ebp-44h] struct wstring v43; // [esp+48h] [ebp-40h] BYREF __int64 v44; // [esp+60h] [ebp-28h] BYREF int v45; // [esp+68h] [ebp-20h] _DWORD v46[2]; // [esp+6Ch] [ebp-1Ch] BYREF int v47; // [esp+74h] [ebp-14h] int v48; // [esp+84h] [ebp-4h] v36 = this ; v1 = this [155]; if ( !v1 ) return ; v45 = 0; v44 = 0i64; (*( void (__thiscall **)( int , __int64 *, _DWORD))(*(_DWORD *)(v1 + 12) + 560))(v1 + 12, &v44, 0); v48 = 0; if ( sub_419D40(( int )&v44) || soui_wstring_size(( int )&v44) != 32 ) // v44 is real input goto LABEL_43; v2 = soui_wstring_size(( int )&v44); v3 = ( const unsigned __int16 *)sub_41AD51(( int )&v44, v2); sub_4028E0(&v43, v3); v4 = __rdtsc(); srand (v4); v5 = rand () % 32; v6 = __rdtsc(); v39 = v5 + 4; srand (v6); v7 = rand () % (v5 + 4); v8 = 0; v9 = 0; v10 = 0; v37 = v7; v38 = 0; v46[0] = 0; v42 = 0; v46[1] = 0; v41 = 0; v47 = 0; v11 = 0; LOBYTE(v48) = 2; v40 = 0; if ( v39 <= 0 ) goto LABEL_30; do { if ( v11 == v7 ) { s = &v43; if ( v43.capacity >= 8u ) s = v43.s; v35[0] = ( int )s; v13 = soui_wstring_size(( int )&v44); v35[1] = v13; if ( v9 != v10 ) { *(_DWORD *)v9 = s; *(_DWORD *)(v9 + 4) = v13; v9 += 8; v46[1] = v9; goto LABEL_28; } v14 = v35; } else { v15 = __rdtsc(); srand (v15); if ( ( rand () & 1) != 0 ) { v27 = __rdtsc(); srand (v27); v33[0] = 0; v28 = rand (); v33[1] = v28; if ( v9 != v10 ) { *(_DWORD *)v9 = 0; *(_DWORD *)(v9 + 4) = v28; v9 += 8; v46[1] = v9; goto LABEL_28; } v14 = v33; } else { v16 = (_WORD *)alloc_memory(72); v17 = __rdtsc(); srand (v17); v18 = rand () % 36; v19 = 1; v38 = (_DWORD *)(v18 + 1); if ( v18 + 1 > 1 ) { v20 = v18 + 1; do { v21 = __rdtsc(); srand (v21); v22 = rand () % 3; v23 = __rdtsc(); v38 = (_DWORD *)HIDWORD(v23); if ( v22 ) { if ( v22 == 1 ) { srand (v23); v24 = rand () % 25 + 97; } else { srand (v23); v24 = rand () % 9 + 48; } } else { srand (v23); v24 = rand () % 25 + 65; } v16[v19++] = v24; } while ( v19 < v20 ); v9 = v42; } *v16 = 0; v25 = __rdtsc(); srand (v25); v34[0] = ( int )v16; v26 = rand (); v34[1] = v26; if ( v9 != v41 ) { *(_DWORD *)v9 = v16; v10 = v41; *(_DWORD *)(v9 + 4) = v26; v9 += 8; v46[1] = v9; goto LABEL_28; } v14 = v34; } } sub_40ADB0(( const void **)v46, (_BYTE *)v9, v14); v10 = v47; v9 = v46[1]; v41 = v47; LABEL_28: v7 = v37; v11 = v40 + 1; v42 = v9; v40 = v11; } while ( v11 < v39 ); v8 = (_DWORD *)v46[0]; v38 = (_DWORD *)v46[0]; LABEL_30: v29 = v8; if ( v8 != (_DWORD *)v9 ) { v30 = ( int )v36; do { (*( void (__stdcall **)( int , int , _DWORD, _DWORD))(*(_DWORD *)v30 + 176))(v30, 1146, *v29, v29[1]); // 1146, sub_4028C0, sendmessagew__ v29 += 2; } while ( v29 != (_DWORD *)v9 ); v10 = v41; v8 = v38; } if ( v8 ) { v31 = ( char *)v8; if ( ((v10 - (_DWORD)v8) & 0xFFFFFFF8) < 0x1000 || (v8 = (_DWORD *)*(v8 - 1), (unsigned int )(v31 - ( char *)v8 - 4) <= 0x1F) ) { free_(v8); goto LABEL_38; } LABEL_45: _invalid_parameter_noinfo_noreturn(); } LABEL_38: if ( v43.capacity >= 8u ) { v32 = v43.s; if ( (unsigned int )(2 * v43.capacity + 2) >= 0x1000 ) { v32 = ( __int16 *)*((_DWORD *)v43.s - 1); if ( (unsigned int )(( char *)v43.s - ( char *)v32 - 4) > 0x1F ) goto LABEL_45; } free_(v32); } v43.size = 0; v43.capacity = 7; LOWORD(v43.s) = 0; LABEL_43: sub_41A562(&v44); } |
动态调试,这个函数开头是真实的获取输入框的内容,包括对长度32的检查;中间通过虚函数间接调用了sendmessage 1146,参数就是输入框的原始内容。
sub_406490
的父级是sub_405AF0
,已经明确的看到了L"btn_close"和L"check_va",所以发送消息这边应该没有更多逻辑了。
另一处1146的地方在sub_405C50
:
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 | BOOL __thiscall sub_405C50( HWND * this , int a2, int a4, _OWORD *arg8, HWND a5, int *a6, int a7) { HWND v8; // esi BOOL v9; // eax LRESULT v10; // eax int v12; // [esp-10h] [ebp-38h] _OWORD *v13; // [esp-Ch] [ebp-34h] HWND v14; // [esp-8h] [ebp-30h] int v15; // [esp+20h] [ebp-8h] BYREF int savedregs; // [esp+28h] [ebp+0h] BYREF if ( a7 ) return 0; switch ( a4 ) { case 1: this [181] = 0; break ; case 272: this [181] = ( HWND )1; *a6 = init_window(( int ) this ); // sub_405F10 goto LABEL_9; case 16: this [181] = ( HWND )1; deinit_windows( this ); break ; default : goto LABEL_10; } *a6 = 0; LABEL_9: if ( this [181] ) return 1; LABEL_10: v8 = this [153]; v9 = sub_403120( this , a2, a4, ( UINT_PTR )arg8, (unsigned int )a5, a6, 0); this [153] = v8; if ( v9 ) return 1; v15 = 1; v10 = sub_A47CEF( this + 1, a4, ( WPARAM )arg8, a5, &v15); *a6 = v10; if ( v10 || (unsigned int )(a4 - 306) > 6 ) { if ( v15 ) return 1; } switch ( a4 ) { case 1146: v14 = a5; v13 = arg8; v12 = 1146; goto LABEL_22; case 1147: v14 = a5; v13 = arg8; v12 = 1147; goto LABEL_22; case 1149: v14 = a5; v13 = arg8; v12 = 1149; goto LABEL_22; case 1148: v14 = a5; v13 = arg8; v12 = 1148; LABEL_22: v15 = 1; *a6 = handle_window_message(( int ) this , ( int )&savedregs, ( int ) this , v12, v13, ( int )v14); // sub_4061D0 return a2 != 0; } if ( a4 != 1150 ) return 0; v15 = 1; *a6 = handle_window_message(( int ) this , ( int )&savedregs, ( int ) this , 1150, arg8, ( int )a5); // sub_4061D0 return a2 != 0; } |
这里看起来是接收消息的主分发器所在(以及,它的上两级父级函数也是直接到了虚表中),前面有一个初始化函数 sub_405F10
:
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 | // sub_405F10 int __thiscall init_window( int this ) { int v2; // eax int v3; // esi int v4; // esi int v5; // eax int v6; // eax int v7; // esi int v8; // esi int v9; // eax bool v10; // zf HANDLE TimerQueue; // eax char v13[1160]; // [esp+10h] [ebp-920h] BYREF char Destination[1164]; // [esp+498h] [ebp-498h] BYREF int v15; // [esp+92Ch] [ebp-4h] int retaddr; // [esp+934h] [ebp+4h] *(_DWORD *)( this + 728) = 1; v2 = sub_A50E21(( void *)L "img_logo" , -1); v3 = v2; if ( v2 && (*( int (__stdcall **)( int , const wchar_t *))(*(_DWORD *)v2 + 12))(v2, L "img" ) ) { v4 = v3 - 4; } else { v5 = sub_A3C15E(Destination, "soui4" , 3, ( int )&unk_D3C680, ( int ) "SOUI::SWindow::FindChildByName2" , 640, retaddr); v15 = 0; sub_A3C2AE(v5); sub_A3C294( "FindChildByName2 Failed, no window of class " ); sub_A3C29D(L "img" ); sub_A3C294( " with name of " ); sub_A3C29D(L "img_logo" ); sub_A3C294( " was found within " ); sub_A3C264(-1); sub_A3C294( " levels" ); v15 = -1; sub_A3C1B2(Destination); v4 = 0; } *(_DWORD *)( this + 616) = v4; v6 = sub_A50E21(( void *)L "input_va" , -1); v7 = v6; if ( v6 && (*( int (__stdcall **)( int , const wchar_t *))(*(_DWORD *)v6 + 12))(v6, L "edit" ) ) { v8 = v7 - 12; } else { v9 = sub_A3C15E(v13, "soui4" , 3, ( int )&unk_D3C680, ( int ) "SOUI::SWindow::FindChildByName2" , 640, retaddr); v15 = 1; sub_A3C2AE(v9); sub_A3C294( "FindChildByName2 Failed, no window of class " ); sub_A3C29D(L "edit" ); sub_A3C294( " with name of " ); sub_A3C29D(L "input_va" ); sub_A3C294( " was found within " ); sub_A3C264(-1); sub_A3C294( " levels" ); v15 = -1; sub_A3C1B2(v13); v8 = 0; } v10 = *(_DWORD *)( this + 616) == 0; *(_DWORD *)( this + 620) = v8; if ( v10 || !v8 ) deinit_windows((_DWORD *) this ); TimerQueue = CreateTimerQueue(); *(_DWORD *)( this + 660) = TimerQueue; if ( TimerQueue ) { CreateTimerQueueTimer( ( PHANDLE )( this + 664), TimerQueue, _IsNonwritableInCurrentImage, *( PVOID *)( this + 28), 0x1F4u, 0x7D0u, 0); CreateTimerQueueTimer(( PHANDLE )( this + 668), *( HANDLE *)( this + 660), antidebug1, 0, 0x258u, 0x7D0u, 0); // sub_407900 CreateTimerQueueTimer(( PHANDLE )( this + 672), *( HANDLE *)( this + 660), antidebug2, 0, 0x2BCu, 0x7D0u, 0); // sub_4079D0 CreateTimerQueueTimer(( PHANDLE )( this + 676), *( HANDLE *)( this + 660), sub_407A60, 0, 0x320u, 0x7D0u, 0); } SetForegroundWindow(*( HWND *)( this + 28)); *(_DWORD *)( this + 624) = SetTimer(*( HWND *)( this + 28), 1u, 0x64u, TimerFunc); return 0; } |
CreateTimerQueueTimer初始化的 sub_407900 和 sub_4079D0 分别是 NtQueryInformationThread 和 NtQueryInformationProcess 调试器检测,如果检测到被调试就调用 ExitProcess 或 TerminateProcess 退出。
(但是,尝试patch这两处仍然会在调试时闪退,不知道哪里还有其他检测)
所以,看起来程序其他地方没有隐藏的逻辑了。所以题目真的能解吗?
无奈之下乱搞,把arm代码中的所有sha256都抄出来逐一往cmd5和google搜索塞,有了新的发现:
sha256 | plaintext |
---|---|
e0bc614e4fd035a488619799853b075143deea596c477b8dc077e309c0fe42e9 | 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b |
6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b | 1 |
d8bdf9a0cb27a193a1127de2924b6e5a9e4c2d3b3fe42e935e160c011f3df1fc | d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35 |
d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35 | 2 |
6749dae311865d64db83d5ae75bac3c9e36b3aa6f24caba655d9682f7f071023 | |
ea96b41c1f9365c2c9e6342f5faaeab2a44471efe1e65a2356a974646d2588fd | |
5b65712d565c1551340998102d418ceccb35db8dbfb45f9041c4cae483d8717b | 4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce |
4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce | 3 |
033c339a7975542785be7423a5b32fa8047813689726214143cdd7939747709c | 4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a |
4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a | 4 |
c81d40dbeed369f1476086cf882dd36bf1c3dc35e07006f0bec588b983055487 | ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d |
ef2d127de37b942baad06145e54b0c619a1f22327b2ebbcfbec78f5564afe39d | 5 |
9e259b7f6b4c741937a96a9617b3e6b84e166ff6e925e414e7b72936f5a2a51f | e7f6c011776e8db7cd330b54174fd76f7d0216b612387a5ffcfb81e6f0919683 |
e7f6c011776e8db7cd330b54174fd76f7d0216b612387a5ffcfb81e6f0919683 | 6 |
1048f03db5d45f654b955eae20d84b72673680fb13b318e7da22e8dce58df21c | 7902699be42c8a8e46fbbb4501726517e86b22c56a189f7625a6da49081b2451 |
7902699be42c8a8e46fbbb4501726517e86b22c56a189f7625a6da49081b2451 | 7 |
8f0703d406fdb0ea8011d5de342c3aca62214758a8a2b5b8a4e9f1c8c6c42462 | 2c624232cdd221771294dfbb310aca000a0df6ac8b66b696d90ef06fdefb64a3 |
2c624232cdd221771294dfbb310aca000a0df6ac8b66b696d90ef06fdefb64a3 | 8 |
182b32359558eb092511b7166867503ddd83fbe5b42f2545e1903016e721393d | 19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7 |
19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7 | 9 |
fe5f0fc640cbbc113406f042d08cc60ba784c775f7c3299985665323c5fbcdc4 | 4a44dc15364204a80fe80e9039455cc1608281820fe2b24f1e5233ade6af1dd5 |
4a44dc15364204a80fe80e9039455cc1608281820fe2b24f1e5233ade6af1dd5 | 10 |
4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8 | 11 |
1ba586c0b89202f7307b61f1229330978a843afc98589ffc6a62f209225d3528 | 4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8 |
相邻的两条sha256,有的其中一条是简单整数的sha256,另一条是前面sha256的sha256。
自己尝试正向计算了一下补全表格,只剩两个无法搞出来,其中 6749dae311865d64db83d5ae75bac3c9e36b3aa6f24caba655d9682f7f071023 按照之前的分析是程序输入框的正确flag,那么按照上面表格的规律,它应该也是 ea96b41c1f9365c2c9e6342f5faaeab2a44471efe1e65a2356a974646d2588fd 的sha256,但实际计算下来却不对。
然后!随手把它往程序的输入框里粘贴,结果验证成功??再一看是被截断成了32位,所以正确的flag是 ea96b41c1f9365c2c9e6342f5faaeab2,计算了下它的sha256确实是 6749dae311865d64db83d5ae75bac3c9e36b3aa6f24caba655d9682f7f071023。
(writeup完。这里需要第二个双重分割线)
如果这真的是出题人的预设解法(希望不是如此,而是程序还有什么隐藏的校验逻辑我没有找到,等出题人公开解法,如果错怪了,在此先提前道歉然后删除此段),只能说今天长见识了,第一次见到出成这样的题也敢称为Reverse。
最后一步就全靠脑洞吗?程序的唯一关键路径只有一个不可逆的sha256,只靠分析程序运算逻辑无法逆推出flag;当然,或许可以解释为“隐藏线索”,然后这些sha256确实是刻意构造且存在规律的。这种脑洞题虽然不鼓励但也是能出的(当然选手如何评价是另一回事),但惯例上,这类题最多只能被归类为 Misc。那么,按照今年的规则,misc题目禁止参赛,应当如何处理呢?
另外,其实不太明白本次比赛规则为什么特别禁止了misc题目参赛。难道是因为去年秋季赛的无解misc么?
传统的四大分类(Pwn、Reverse、Crypto、Web),虽然没有成文的标准,但基本上是比较死板严格的(以Reverse为例,目标就是逆推出输入,那么程序中给的约束必须是充分且可逆的,而且应该以运算的方式在从输入flag到输出判定结果的数据流的关键路径上;而本题,唯一的线索是所谓的24个sha256找规律,除了实际参与判断的那个,其他都删掉或乱给,程序的校验逻辑仍然能通过,也就是说这些规律并不参与到判断flag正确性的运算中,而只是作为旁路的一个提示,因此完全从运算逻辑无法逆推),misc更多的作为一个补集,收容所有不进入四大类的题目。所以,高质量比赛经常出现一些思路创新但不便于按传统分类的题目放到misc中。
KCTF如果允许其他四类题目参赛但不允许misc,对题目的多样性是不友好的。合理猜测禁止的初衷是保证题目质量,避免脑洞/无解/作者自己不解(例如,随手写个加密算法套层vmp/重量级混淆当逆向题,但是出题人自己不公开自动化去混淆方法的情况,这种题适合在论坛中长期交流或悬赏,放到以技术交流为目的的比赛中是对攻击方的严重不公平)等情况出现,题目质量应该依靠验题来保证。
据了解,一些高质量国际赛的出题规范之一就是作者自己必须在“做题人”的视角下“真的”会解,因为比赛的目的是技术交流,而不是破解悬赏。例如,如果题目加了强混淆的,在公开预期解writeup时必须要给出自己的去混淆方法(自动化或工作量可承受的人工),而不是仅仅依靠自己知道混淆前的原算法,给一份逆推代码了事(这样搞等着被做题人喷吧),因为做题人在对抗混淆之前看不到原算法,所以出题人当站在做题人的视角上验题时,就不能一上来假设自己拥有原算法。
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界