-
-
[原创] 第四题 英雄救美题解
-
2021-5-15 01:44 6041
-
使用ida打开CrackMe.exe, 跳转到主函数F5, 先做初步分析:
可以看到输入首先经过check_1, 再经过check_2, 然后进行 md5 -> sub_401ed0 -> 申请内存 -> 从4181A0拷贝再做某些处理 > 执行该内存.
后面的部分先放一下, 首先分析check_1:
通过从地址0x416260乱序拷贝数据来初始化somekey, 长度是5*0x10+1
共81字节. 这里选择在动态调试 somekey赋值完成后读取somekey的值.
使用frida代码:
1 2 3 | Interceptor.attach(PE.base.add( 0x12A8 ), function() { console.log((this.context as Ia32CpuContext).ebp.sub( 0x58 ).readCString( 81 )); }); |
输出结果是
共81字节.
然后:
主体逻辑是在循环中从somekey中找到输入字符的位置, 模9加1后填入input2中. 同时在输入是数字字符时会判断 该字符等于9减去临时已输入的字符数量, 符合时会将临时已输入的字符数量置0, 然后将 从somekey中找输入字符时的起始位置 加9.
然后看check_2:
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 | int __fastcall check_2( int * input2, int inplen) { ipos = 0 ; data_p = ( int * )&unk_4187C4; do { if ( ! * (data_p - 1 ) ) { v4 = input2[ipos + + ]; * (data_p - 1 ) = v4; } if ( ! * data_p ) { v5 = input2[ipos + + ]; * data_p = v5; } if ( !data_p[ 1 ] ) { v6 = input2[ipos + + ]; data_p[ 1 ] = v6; } if ( !data_p[ 2 ] ) { v7 = input2[ipos + + ]; data_p[ 2 ] = v7; } if ( !data_p[ 3 ] ) { v8 = input2[ipos + + ]; data_p[ 3 ] = v8; } if ( !data_p[ 4 ] ) { v9 = input2[ipos + + ]; data_p[ 4 ] = v9; } if ( !data_p[ 5 ] ) { v10 = input2[ipos + + ]; data_p[ 5 ] = v10; } if ( !data_p[ 6 ] ) { v11 = input2[ipos + + ]; data_p[ 6 ] = v11; } if ( !data_p[ 7 ] ) { v12 = input2[ipos + + ]; data_p[ 7 ] = v12; } if ( ipos > = inplen ) break ; data_p + = 9 ; } while ( ( int )data_p < ( int )&unk_418908 ); |
在循环中将input2填入地址0x4187C4到0x418908之间, 如果该地址内的值非0就会跳过. 注意到是9个为一轮且数据大小为9*9*4
.使用frida按照9*9
打印该地址的数据:
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 | function show2d(data: NativePointer, width: number, height: number, showChar = false) { let text = ""; const map = new Uint32Array(data.readByteArray(width * height * 4 )); for (let y = 0 ; y < height; + + y) { for (let x = 0 ; x < width; + + x) { const chr = map [y * width + x]; if (showChar) text + = String.fromCharCode( chr ); else text + = ( "00" + chr .toString( 16 )).substr( - 2 ); text + = " " ; } text + = "\n" ; } console.log(text); } Interceptor.attach(PE.base.add( 0x1000 ), { / / check_2 函数地址 onEnter: function(args) { show2d(PE.base.add( 0x187C0 ), 9 , 9 ); }, onLeave: function(retVal) { show2d(PE.base.add( 0x187C0 ), 9 , 9 ); console.log(`check_2: ${retVal}`); if (!retVal.equals( 1 )) eval (interact); / / 此处为可交互式断点, 实现方法参见https: / / github.com / tacesrever / easy - frida / blob / master / agent / index.ts #L18 interact的定义 } }) |
在onEnter时的输出为:
1 2 3 4 5 6 7 8 9 | 00 04 00 07 00 00 00 00 00 09 02 00 00 00 00 06 00 07 08 03 00 00 00 05 04 00 00 00 01 00 00 00 03 00 00 00 00 00 00 02 00 01 00 00 00 00 00 00 05 00 00 00 04 00 00 00 04 09 00 00 00 07 01 03 00 05 00 00 00 00 09 04 00 00 00 00 00 08 00 06 00 |
九乘九, 1到9填空, 后面还有一坨验证逻辑, DNA告诉我它是数独. 于是找了个在线解数独的网站:
数独数据为546719238921834657837625419718463925453291786692587143284956371365172894179348562
, 要填入的数据是5619238183457621978469254539786692871328563617281793452
.
然而填入数据来源是输入字符在81字节长的somekey中位置的模9加1, 此时每个字符仍有9种可能, 分析到这时的我还没搞懂check_1中输入是数字字符时的判断逻辑是要我输入什么, 先继续分析接下来的流程.
回到主函数:
注意sub_401ed0的定义, ida认为它是int __cdecl sub_401ED0(int a1, unsigned __int8 *a2)
其中a1和a2都来自栈, 但是在函数中其实使用的是ecx和栈上一个值作为参数, 需要更改该函数的定义为int __fastcall sub_401ED0(int a1, unsigned __int8 *a2, char *a3, char *a4)
, a1, a2来自ecx和edx, 其它来自栈, 函数没有用到a2和a3.
改完后再看sub_401ed0:
查看byte_415960处的数据, 搜索可发现该数据是AES算法的s_box, 可以判断该函数功能是初始化AES密钥. 结合上面的对输入进行md5hash的操作来看,很可能是将输入的md5值作为密钥, 解密接下来的数据. 使用frida调试配合本地nodejs脚本测试解密可以验证算法正确:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | / / 保存加密后的数据为文件 const outfile = new File ( "./enced.bin" , "wb" ); outfile.write(PE.base.add( 0x181A0 ).readByteArray( 0x620 )); outfile.close(); Interceptor.attach(PE.base.add( 0x14BA ), function() { / / 即将跳转到解密后数据时断点 const cpu = <Ia32CpuContext>this.context; const target = cpu.esp.add( 0xc ).readPointer(); console.log(hexdump(target)); / / 输出跳转目标的数据 eval (interact); }); const sdinput = "5619238183457621978469254539786692871328563617281793452" let i = - 1 ; for (let c of sdinput) { myInput + = somekey[i + parseInt(c)]; } myInput + = "sss" ; / / 输入可以是符合数独的字符串加任意 3 个其它字符 console.log(myInput.length, myInput); / / :u$YBPf$fPV:buB$YbfVuYB:V:PYbfuuYBfb$PBf:uPu$bBf$bYPV:Bsss |
可以看出解密后的数据是匹配的.
在没有找到更多对输入的限制的情况下, 似乎只能爆破? 然而输入的可能性仍比9^55还要多, 爆破是不可能爆破的.
这时想到对输入包含0到9字符时的处理逻辑, 看来要猜一猜出题人想要我们输入什么.
当输入了i <= 9个字符后输入数字9-i, 它会将somekey前面截短9个字符. 只输入数独的话用55个字符, 如果再输入9个字符, 长度就会达到64, 刚好满足输入长度限制64. 可以猜测是不是想要选手分9行输入数独, 每行输入完成后输入该行已有的的数字数量, 同时somekey进入下一节.
于是构造输入的逻辑就要变成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const somekey = "$BPV:ubfYp}]DtN>aT^MGmJQ#*Hr`O'wjic0!hdy{oZz-@n+?&%s_/g<e[W)XUxRFSLRA;.l=CEkvK-(q" ; const sdinputs = [ "5619238" , "18345" , "76219" , "7846925" , "4539786" , "6928713" , "28563" , "61728" , "1793452" ] let myInput = ""; let i = - 1 ; for (let line of sdinputs) { for (let c of line) { myInput + = somekey[i + parseInt(c)]; } i + = 9 ; myInput + = String.fromCharCode( 0x39 - line.length); } console.log(myInput); / / :u$YBPf2pa]Dt4 #QM^H4ic'j0`w2y{d-Zzo2%/n_s@+2<UW)e4AR;F.4=-qEkvC2 |
验证发现该输入就是正确的输入, 也是最终的flag.
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法