CONFidence CTF re: Locked PE

记录一下上周末跟着队里打的CONFidence CTF,只做了一个re,水平有限,花了蛮长时间的。

题目描述

x64的exe文件,直接运行没有回显,后台挂有图标,点击没有界面反应,得不到什么信息。

后来发现是AutoIt转的exe,以前没有听说过,貌似是某种自动化脚本,当然这些不重要,重要的是怎么从exe反编译回AutoIt脚本,拖到IDA根本看不出东西来。

解题过程

先利用脚本将64位的exe转化为32位,我试图直接用Exe2Aut转成AutoIt脚本语言,转化过程中报错(其实也就是运行时报错)。我还尝试了各种各样的工具,都没能直接转化成功。

既然原64位程序可以跑,但是转成32位之后就出问题了,说明这个binary肯定哪个地方要patch以下,而且它报的错是”Unable to open the script file”,那我就根据这个来找到底在哪里没有满足条件而出错。

我先是动态调试,把反调的函数patch掉,跟踪找到32位程序弹窗报错的执行流:

在这里调用sub_4128CF之后会对返回值进行一个check,如果返回1的话,或跳转到LABEL_3执行,而这个LABEL_3:

会调用sub_40B506之后直接返回,之后这个程序就逐步地推出了。这个sub_40B506实际上正是完成弹出”Unable to open the script file”窗口的操作:

那么显然sub_4128CF函数部分就是我们需要分析的重点:

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
int __userpurge sub_4128CF@<eax>(int a1@<eax>, int a2, void *a3)
{
int v3; // esi@1
__int32 v4; // ST38_4@1
FILE *v5; // ST34_4@1
int v7; // edi@4
FILE *v8; // ST0C_4@4
int v9; // ebx@4
void *v10; // eax@4
FILE *v11; // ST3C_4@4
int v12; // eax@4
void *v13; // esi@4
wchar_t v15; // [sp+10h] [bp-688h]@1
void *v16; // [sp+220h] [bp-478h]@7
void *v17; // [sp+224h] [bp-474h]@7
int v18; // [sp+234h] [bp-464h]@7
int v19; // [sp+238h] [bp-460h]@7
void *v20; // [sp+688h] [bp-10h]@4
int v21; // [sp+68Ch] [bp-Ch]@1
int v22; // [sp+690h] [bp-8h]@4
char v23; // [sp+697h] [bp-1h]@4
void *v24; // [sp+6A4h] [bp+Ch]@4

v3 = a1;
sub_41143A();
v4 = *(_DWORD *)(v3 + 4);
v5 = *(FILE **)v3;
v21 = 1;
fseek(v5, v4, 0);
if ( sub_412A3F(L">>>AUTOIT SCRIPT<<<", &v15) )
return 6;
fread(&v23, 1u, 1u, *(FILE **)v3);
fread(&v22, 4u, 1u, *(FILE **)v3);
v7 = v22 ^ 0x87BC;
fread(&v22, 4u, 1u, *(FILE **)v3);
v8 = *(FILE **)v3;
*(_DWORD *)a3 = v22 ^ 0x87BC;
fread(&v22, 4u, 1u, v8);
v9 = v22 ^ 0xA685;
fseek(*(FILE **)v3, 16, 1);
v24 = malloc(*(_DWORD *)a3);
v10 = malloc(v7);
v11 = *(FILE **)v3;
v20 = v10;
fread(v10, v7, 1u, v11);
v12 = *(_DWORD *)(v3 + 8);
v13 = v20;
sub_412597(v20, v7, v12 + 9335);
sub_4113E6(v13, v7);
if ( v9 != v21 )
{
free(v24);
free(v13);
return 10;
}
if ( v23 == 1 )
{
sub_41143A();
v18 = 1;
v19 = 1;
v16 = v24;
v17 = v13;
sub_411468();
free(v13);
}
else
{
free(v24);
v24 = v13;
}
*(_DWORD *)a2 = v24;
return 0;
}

实际上原64位程序函数名全是sub_xxxxxx,转成32位还识别出一些文件操作,这里可以看出,是从原binary中取出了一些bytes进行一些计算然后比较,我开始认为可能是转化成32位程序之后,这些bytes可能读错了位置,导致判断错误,事实上比对之后并没有出错,那是为什么?

后来我拿32位程序和64位动态跟踪比对,发现分歧就出现在这里:

1
2
3
4
5
6
if ( v9 != v21 )
{
free(v24);
free(v13);
return 10;
}

原64位程序这里v9 == v21,而在32位程序执行过程中v9 != v21,并且v9的值相同而v21的值不同。

那么看看这个v21怎么来的,从动态分析的结果看,在执行到这里之前:

1
sub_412597(v20, v7, v12 + 9335);

其他参数都保持一致,而执行之后,就造成v9 != v21,对比一下果然:

这个值在64位程序里是0xBEEF,在32位程序里却是0x2477(9335),patch一下改成0xBEEF之后,重新运行,原来的错误没有了(但是报了其他错,实质上没有影响后面反编译的结果)。

这个时候用Exe2Aut转化一下,成功了:

这里只是脚本最后的部分,但实际上也就是这部分有价值,前面都是一些函数定义。

分析这个AutoIt脚本,实质上就是对一串给定的16进制值,进行异或操作得到的512bytes分成16组,每组32bytes也就是256bits。然后将ClipGet()得到的数据进行_crypt_hashdata然后和前面提到的16组数据进行比较。

很容易猜测这个16组256bits的数据其实就是md5散列,随便拿个查一下明文,正好是3个字符的md5散列值,之后就可以写个脚本自己逐个爆破了:

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
table_1 = [ord(item) for item in "oOoOooOO"]

raw = "097e0b7609097f78582b5b7f5f582c780d775c7a0e5a7a78587a5e7a095b7a2e5b7c0c7d0b09297c0c765a295d0b2e790b7f587b58097f2a572c572b090b7b7c0e78572c5b572e7d5e7f5e7d5e5a792958760b7c590d7b7f0d2c5d7b585d78770a7b5e2c5c5a777c5f7e5e7b5d0c2b2a562e0b780b0d7b7d5b775a2c560a7b2e5c7f0b2d575f767d5b2a582e570a2e7d0929577c5b58787e0b7d582e56097e785b2a582a0e0e78790c2e59780a0b2d7d097e577b0d0b2b7e5e295f2e5a097e2e0b2b097b5a0a7b2e5b7f0e29560d767b0b2e0c7a565b297b0a7e5d7f095c76795d2a0e7e5a0e2a29097c572c575a7f7d5e7a0c295f0a2d7b0e2d0a7e0b0c7b2a5a29592c0a0b7e76587d5b77560e7c2a5e775e775e097a2b09295d7c5e092d780b7b5f78090c7d775d7c5f2958097a760d2d0e2c5d562a2b577e577a565c2e7c5f765d775b5c7d2d0c2e0e2a5f592a2a0c7d0d7c0b0b7d7c5a77582a5a59767a0e79562b5e092d2d5d785a7b0e097c7a0d79092c0a0c2b79097f097f57572e295b7b5a7a585f787c0a7c5e2a5b0b2d2b0d7b5b2d5d587c7b5e2e592a5d5c7b290d2e5f79590d7a7b0c7f0e7d575c7d79597f5b79595c772c587b5c7e0b0b7a7e5f2d5a78575b2b2d597b0a780d0e7e795d7b0976585a7a7e567c57785d562a2d0d2b0c290e577e2c587f0e775f0e2e7b09795a2d5c58292c5f765f7f58092978"
raw_int = []
for i in range(0, len(raw), 2):
raw_int.append(int(raw[i:i+2], 16))

string = ""
i = 0
for item in raw_int:
string += chr(item ^ table_1[i % len(table_1)])
i += 1
# print(string)

cipher = "f1d9ff077d4007c7b835a5577515f45a43c2dff3c95f2da6d0747f0e8c8dfd43a78c48a21012156f79d36b40bc247278e41c358301142cde9ad7db42485c9e4a30db80924e7a8ea2ff834771d27a9f174e7eaa76ca67edb2f184bdd11f0a5f1addf45e4a40af9b94dac594f4e120f3962ea15aeff38c850215cf0eb4abe1dc4e5f6ced1972489a3e18181f5dff231fb7d407fc28230f7f59bbac29ed818593a30928432bcaae06eec2b3dd23587e5695a69d1fbb2754af35b6fcecd6f0f088af44557073e31e4dbdb44b27341a6e234fba066b54c0a283266046638c7431dd510b5784db64e7ba1624f97551938729ebbdcfa81c70a80aa4f65b37fc09007ff7"
md5_list = []

for i in range(0, len(cipher), 32):
md5_list.append(cipher[i:i+32])
# print(md5_list)

from hashlib import md5
from itertools import permutations

flag = ""
table = ""
for i in range(32, 127):
table += chr(i)
i = 0
for item in md5_list:
for s in permutations(table, 3):
target = ""
for ch in s:
target += ch
m = md5()
m.update(target.encode("utf-8"))
if m.hexdigest() == item:
flag += target
i += 1
print(flag)
break

这样就拿到flag了。

flag

参考资料

  1. https://www.cnblogs.com/DeeLMind/p/7147024.html
Author: Nop
Link: https://n0nop.com/2020/03/18/CONFidence-CTF-re-Locked-PE/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.