-
-
[原创]看雪.TSRC 2017CTF秋季赛第三题WP
-
2017-10-29 02:01 2584
-
看雪.TSRC 2017CTF秋季赛第三题WP
初识
首先将程序拉入IDA,这已经成为我做CTF题目的习惯了。
查看入口,还算正常。应该也是vc写的,通常8.0以上版本的入口为call,jmp
的形式,但此题不是,是两个call
。随后在第二个call
中看到去main
函数的跳转函数,跳到434CA0
。
.text:0043D38E mov ecx, [ebp+var_78] .text:0043D391 push ecx ; int .text:0043D392 mov edx, [ebp+var_68] .text:0043D395 push edx ; int .text:0043D396 push 0 ; int .text:0043D398 push 400000h ; hInstance .text:0043D39D call sub_42E040
main
函数直接看伪代码:
int __stdcall sub_434CA0(HINSTANCE hInstance, int a2, int a3, int a4) { int v4; // eax@1 int v6; // [sp+0h] [bp-CCh]@1 char v7; // [sp+Ch] [bp-C0h]@1 memset(&v7, 0xCCu, 0xC0u); dword_49C784 = (int)hInstance; v4 = DialogBoxParamA(hInstance, (LPCSTR)0x65, 0, DialogFunc, 0); sub_42DE51(&v6 == &v6, v4); return sub_42DE51(1, 0); }
DialogFunc
跳转去的伪代码如下(patch过的):
int __stdcall sub_434EF0(HWND hDlg, int a2, int a3, int a4) { int v4; // eax@8 int v5; // ST0C_4@11 CHAR *v6; // esi@11 int v7; // eax@11 int v8; // eax@13 int v9; // eax@14 int v11; // [sp+0h] [bp-1A4Ch]@8 int v12; // [sp+Ch] [bp-1A40h]@1 int i; // [sp+1C4h] [bp-1888h]@8 char v14[1032]; // [sp+1D0h] [bp-187Ch]@10 char v15[40]; // [sp+5D8h] [bp-1474h]@8 int v16; // [sp+600h] [bp-144Ch]@8 char v17; // [sp+60Ch] [bp-1440h]@8 char v18; // [sp+60Dh] [bp-143Fh]@8 char v19; // [sp+A14h] [bp-1038h]@8 char v20; // [sp+A15h] [bp-1037h]@8 char v21; // [sp+E1Ch] [bp-C30h]@8 char v22; // [sp+E1Dh] [bp-C2Fh]@8 CHAR String; // [sp+1224h] [bp-828h]@8 char v24; // [sp+1225h] [bp-827h]@8 int v25; // [sp+162Ch] [bp-420h]@8 char v26; // [sp+1638h] [bp-414h]@1 char v27; // [sp+1639h] [bp-413h]@1 int v28; // [sp+1A40h] [bp-Ch]@1 unsigned int v29; // [sp+1A48h] [bp-4h]@1 int savedregs; // [sp+1A4Ch] [bp+0h]@1 memset(&v12, 0xCCu, 0x1A40u); v29 = (unsigned int)&savedregs ^ dword_49B344; v28 = 0; v26 = 0; memset_42D5E6((int)&v27, 0, 1023); v12 = a2; if ( a2 == 16 ) ExitProcess(0); if ( v12 == 0x110 ) { sub_42D4F1(); v28 = 0; sub_42E428(); v28 = 0; v28 = sub_42D825(); sub_42D14F(hDlg, 1); } else if ( v12 == 0x111 ) { v12 = (unsigned __int16)a3; if ( (unsigned __int16)a3 == 1002 ) { String = 0; memset_42D5E6((int)&v24, 0, 1023); v21 = 0; memset_42D5E6((int)&v22, 0, 1023); v4 = GetDlgItemTextA(hDlg, 1001, &String, 1025); v25 = sub_42DE51(&v11 == &v11, v4); v19 = 0; memset_42D5E6((int)&v20, 0, 1023); j_debase_42D267((int)&String, 1024, (int)&v21); v17 = 0; memset_42D5E6((int)&v18, 0, 1023); j_debase_42D267((int)&v21, 1024, (int)&v19); j_trans_42D96A((int)&v19, (int)&v17, 1024); v16 = 3; j_sm3_hash_437E70((int)&v19, 3, (int)v15); for ( i = 0; i < 32; ++i ) sprintf_42DF05((int)&v14[2 * i], "%02x", v15[i]); v5 = strlen_42D794((int)v14); v6 = &String + strlen_42D794((int)&String); v7 = strlen_42D794((int)v14); if ( !j_compare_42DB27((int)v14, (int)&v6[-v7], v5) ) { sub_42D0B4(); if ( (unsigned __int8)j_maze_435400((int)&dword_49B000, (int)&v17) == 1 ) { v8 = MessageBoxA(0, "ok", "CrackMe", 0); sub_42DE51(&v11 == &v11, v8); } } } } sub_42D65E(&savedregs, &dword_435250); v9 = sub_42D1E5((unsigned int)&savedregs ^ v29); return sub_42DE51(1, v9); }
明显此处应该是主要流程所在了。
算法分析
通过仔细分析,此处有4个算法,一是定制的base64解码算法,一是SM3 hash算法,一是替换算法,一是走迷宫算法。
base64 解码算法
此解法是与通常解法不同之处在于,取编码字符在base64常串中的位置偏移使用编码字符直接索引数组,数组如下:
56 57 58 59 5A 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 30 31 32 33 34 35 36 37 38 39 2B 2F 3E FF FF FF 3F 34 35 36 37 38 39 3A 3B 3C 3D FF FF FF FF FF FF FF 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 FF FF FF FF FF FF 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 00 00 00 00 00
这个数组使部分非base64字符也能解码。虽然对于索引进行了>=0x2b
的限制,但由于文中进行了两次base64解码,使第一次解码后末位字符为\x00
--\x2a
,就会产生基本一致的多解,如;
TDkA2465b4d68ca27b9210e4aa31e0aa8da7e618e79f2d56c4326fede74b16e2b667 TDkB2465b4d68ca27b9210e4aa31e0aa8da7e618e79f2d56c4326fede74b16e2b667 TDkC2465b4d68ca27b9210e4aa31e0aa8da7e618e79f2d56c4326fede74b16e2b667 ...
当然这种解码算法比较强悍,鲁棒性好。
SM3 hash算法
算法过程请百度,这里只说解题怎么应对。
题目中将两次base64解码后输入的前3字节进行hash计算,然后比较hash结果与输入的后64字节。除此之外还有一个校验,为了简便计算,我们可以把64字节的比较输入始终附加在原有输入后面。
替换算法
此算法是由两次base64解码后的字符,通过改写一个8byte的字串s
,然后与规则对照进行替换,替换规则如下:
2D 2D 2D 2D 2D 0 2E 2D 2D 2D 2D 1 2E 2E 2D 2D 2D 2 2E 2E 2E 2D 2D 3 2E 2E 2E 2E 2D 4 2E 2E 2E 2E 2E 5 2D 2E 2E 2E 2E 6 2D 2D 2E 2E 2E 7 2D 2D 2D 2E 2E 8 2D 2D 2D 2D 2E 9 2E 2D 2A 2A a 2D 2E 2E 2E b 2D 2E 2D 2E c 2D 2E 2E 2A d 2E 2A 2A 2A e 2E 2E 2D 2E f 2D 2D 2E 2A g 2E 2E 2E 2E h 2E 2E 2A 2A i 2E 2D 2D 2D j 2D 2E 2D 2A k 2E 2D 2E 2E l 2D 2D 2A 2A m 2D 2E 2A 2A n 2D 2D 2D 2A o 2E 2D 2D 2E p 2D 2D 2E 2D q 2E 2D 2E 2A r 2E 2E 2E 2A s 2D 2A 2A 2A t 2E 2E 2D 2A u 2E 2E 2E 2D v 2E 2D 2D 2A w 2D 2E 2E 2D x 2D 2E 2D 2D y 2D 2D 2E 2E z
当前输入为\x2f
时,则目标值为\x20
;若当前为\x20
时,则用规则进行替换;若为其它字符,则改写s
。一次替换后,s
还原默认状态。
迷宫算法
这部分应该是一个10X10的深宫走法算法,深宫如下:
0 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 0 0 0 1 0 0 0 0 0 1 0 1 1 1 1 1 1 1 0 1 0 0 1 1 0 0 0 1 0 1 0 0 1 1 0 1 0 0 0 1 0 1 1 1 0 1 1 1 1 1 0 0 1 1 0 0 0 0 1 1 1 0 0 1 1 1 1 0 0 0 0 1 0 1 1 1 1 1 1 1 0 0 0
其中的0
为可行路径。题目中,替换后的输入字符为q z
分别表示向上一行和向下一行;p l
分别表示向左一格和向右一格。[3,8]
为死路。若输入为\x20
,则结束,返回True
;若走错,则返回False
,验证失败。
这里并没有对路径完成量进行约束,也产生了多解可能。这里,我们为简化,可以直接让替换后的第一个字符为\x20
。
反调试
这题反调试都是常规做法,但是比较多。主要方式有:API调用查询调试状态,如CheckRemoteDebuggerPresent
、ZwQueryInformationProcess
和IsDebuggerPresent
等;枚举窗体名;枚举进程;通过打开设备检查调试类驱动;抛出异常等方式。
这些IDA简单patch下就好
反算
上面说了,为了简化,最后替换完成后这有一个\x20
就行,那替换前那就是\x2f
。还原到第一次debase后,这也会产生多解,如L1
-L9
;再进行base64还原,得到TDE
、TDk
等。因为后面还要附加上hash串,所以要在hash串前结束base64的解码动作,方法有三个:第一是让等解码字符小于\0x2b
或大于\x7f
,这一般在第二次解码时利用;第二是让等解码字符索引base64解码表时落入\xff
;两次解码时都能利用;第三是等解码字符为=
,两次解码时都能利用。但是因为比赛规则是不能有符号,所以有些情况用不了。
注册码为base64最终的编码串附加上hash,这两者之间也可以附加任何字串,也是产生多解的另一个小地方,这个最终还是因为解base时遇到不恰当的字符就中止解码。
最后附上一组我用的:
TDkT2465b4d68ca27b9210e4aa31e0aa8da7e618e79f2d56c4326fede74b16e2b667
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界