7

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

 3 years ago
source link: https://bbs.pediy.com/thread-267616.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
[原创] 第四题 英雄救美题解-CTF对抗-看雪论坛-安全社区|安全招聘|bbs.pediy.com
[原创] 第四题 英雄救美题解
3天前 833

使用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代码:

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:

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打印该地址的数据:

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时的输出为:

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脚本测试解密可以验证算法正确:

// 保存加密后的数据为文件
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进入下一节.

于是构造输入的逻辑就要变成:

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.

[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年6月班火热招生!!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK