首页
社区
课程
招聘
[原创] 第四题 英雄救美题解
2021-5-15 01:44 6041

[原创] 第四题 英雄救美题解

2021-5-15 01:44
6041

使用ida打开CrackMe.exe, 跳转到主函数F5, 先做初步分析:

 

image-1

 

可以看到输入首先经过check_1, 再经过check_2, 然后进行 md5 -> sub_401ed0 -> 申请内存 -> 从4181A0拷贝再做某些处理 > 执行该内存.

 

后面的部分先放一下, 首先分析check_1:

 

image-2

 

通过从地址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));
});

输出结果是

 

image-20210515004447099

 

共81字节.

 

然后:

 

image-4

 

主体逻辑是在循环中从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告诉我它是数独. 于是找了个在线解数独的网站:

 

image-5

 

数独数据为546719238921834657837625419718463925453291786692587143284956371365172894179348562, 要填入的数据是5619238183457621978469254539786692871328563617281793452.

 

然而填入数据来源是输入字符在81字节长的somekey中位置的模9加1, 此时每个字符仍有9种可能, 分析到这时的我还没搞懂check_1中输入是数字字符时的判断逻辑是要我输入什么, 先继续分析接下来的流程.

 

回到主函数:

 

image-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:

 

image-6

 

查看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

image-7

 

image-8

 

可以看出解密后的数据是匹配的.

 

在没有找到更多对输入的限制的情况下, 似乎只能爆破? 然而输入的可能性仍比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虚拟机自动化脱壳的方法

收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回