2020 confidence

波兰战队p4办的比赛

本来不知道有这个比赛的,好久没去ctf time上看了。开赛前pizza突然问De1ta打不打,我说不打。之后Decade发了个这个链接给我。俺寻思反正也没啥事,写作业写累了,就做做题。毕竟老外的比赛质量高一些

然后Decade就准备面试去了,就我一个人和一个学弟在做(据我所知)

比赛时间: 14 March 2020, 11:00 UTC - 15 March 2020, 11 UTC

比赛结果:3道逆向AK,29名。

比赛官网

broken_counter

一看所有东西都在main函数,还有很强的编译优化,纯静态可能比较吃力。先运行几遍跑一跑。

随便输点字符串,会返回一个数字。同样的输入对应的输出是固定的,且总的输出是每个单独字符输出的总和。

题目还给了一份out.txt,看了下又一万多行数字。一开始猜测要从这个输出还原输入。

总之先写个脚本把所有单独字符的输入爆一遍,看看都有啥。

结果一看,每个字符的输出都在0-7之间,这就蛋疼了,因为单个结果可能对应很多很多的输入。。。

再回过头来看看程序。开头先对一些常量进行解密操作,通过调试很容易拿到解密后的字符串。

注意到接下来的循环内,对字符串取值的时候是10*i+j,所以它应当是10个为一行的:

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
955+*1-v) 
v_# #<v
1 v<v<v<#
+!:g/912D
0`00\:+g#
0*809g+%U
p5gp%009#
^<^<^<\C
>00g:9/^
v$U#D#K#<
s s/7Y\s
" 7
*
s! !!.9
s.\s [
6 s ;[
d Y[
. ,
( ;/[
;.6m/[5
;{722(*y
s5d6.!!9
;1'*+=


循环内是一个大的swich,根据取得值进入不同分支,是一个栈结构的vm,特别的,ip变化是由两个下标组成,vm的一些指令会改变两个下标的增量,所以可以理解为整个vm是在一张地图中进行的。我们可以在取指令的地方下断点,打印出取的指令,然后把控制方向的这些字符去掉,就和普通的vm一样了。

不过更简单的做法还是调试,观察栈中数据的变化,猜测程序在做什么。通过快速调试,不难看出程序在计算输入的字符的二进制有多少个1。

这样我们之前的想法就不对了。因为我们不可能通过一个数据里有多少个1还原出原始数据是什么。

似乎陷入了僵局。

观察一下输出的文本,每一行后面没有空格,而是以回车结尾的。而运行时的输出以空格结尾。找字符串的时候,找到了%d\n这个字符串。在每次循环开头都会检查一个全局变量是否为0,非零的话会输出指令的下标。

我们进行一下修改,强行执行这段,输出的内容开头果然和给出的文件相同。

接下来只要找到某一位是1/0时不同的地方了。注意到113是模指令,每一位必定执行一次,当这一位是1,他的后三位是142,否则是144,依此很容易写出脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# out = [113, 123, 143, ...]

i = 0
tmp = ""
while(i<len(out)):

if out[i] == 113:
if out[i+3] == 142:
tmp += "1"
else:
tmp += "0"
i+=1
tmp = tmp[::-1]
res = ""
for i in range(0,len(tmp),7):
t = tmp[i:i+7]
t = int(t, 2)
res = res+chr(t)
print(res[::-1])

stuck_in_the_past

这个题做得非常的迷,直到做出来我都不知道它是什么。。。

由于看不懂它的混淆方式,所以采用最简单的打硬件断点+观察的方式。首先查找调用,在4011b8调用ReadFile,一次读取一个字节,读到406015,然后马上将它入栈。最后可以在栈上看到一个一个读入的字符。

等全部读完后,打硬件断点观察。我直接在第一个字符打断点,并没有断下来。之后我只输入一个字符,成功断了下来,很快在401133这里有条sub eax, ebx指令,eax是输入的字符,ebx是,直觉告诉我这里可能有问题,于是打个断点调试。

通过不停的f9,最终看到这里的ebx一直在变,当变化到'}'字符后就推出了。}正是flag的最后一个字符。我又尝试输入'}',果然对比完'}'之后就进入下一位了,观察到下一位是'd'。

于是可以手动调出flag的每一位,或是打个条件记录断点,每次运行到这里把eax赋值并输出,就出flag了。

locked_pe

一开始有点无从下手,后来在程序的资源里找到一个叫SCRIPT的资源,结合程序里出现的字符串 AutoIt v3,猜测它可能是AutoIt脚本,开始查资料。

AutoIt脚本编译成exe

找到个反编译工具,然而不能用

http://www.hexacorn.com/blog/2015/01/08/decompiling-compiled-autoit-scripts-64-bit-take-two/

用官方的AutoIt编译了一份官方的例子calculator.au3,然后用这个工具可以反编译。

diff一下,代码部分只有几个数值不一样

1
2
3
4
5
6
7
8
# flagcheck
.text:000000014007A38B add edx, 0BEEFh
.text:000000014007A6F7 add r9d, 0BEEFh
.text:000000014007A90C lea r9d, [rdi+0BEEFh]
# calculator
.text:000000014007A38B add edx, 2477h
.text:000000014007A6F7 add r9d, 2477h
.text:000000014007A90C lea r9d, [rdi+0F479h]

同时注意到sub_14007A02C的一段代码

1
2
3
4
5
6
7
8
9
10
if ( v6 )
{
v8 = v6;
do
{
*v7++ ^= sub_140079FF4((__int64)&v9, a5);
--v8;
}
while ( v8 );
}

似乎是在加密,这个函数的参数也是之前不同的那几个数据。

编译后的脚本被当作资源文件放在exe里面了,用resource hacker可以轻松提取出来

根据反编译工具的过程,首先它只能反编译32位的。作者写了一个工具将64转32。exe的代码部分直接赋值官方的一个bin文件。

思考:

作者修改了编译后的exe和脚本文件,使得正常的反编译工具无法运行。

一种思路是将脚本文件解密后,再按官方的加密方式加密,然后用反编译工具反编译。

我们先看看加密的内容是啥。在异或的地方打断点,观察异或后的值。

有一部分是一样的。不一样的部分解密出来是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1
# calculator 0x68:0x68+0x8C
C:\Users\Administrator.000\AppData\Local\AutoIt v3\Aut2Exe\aut1CFC.tmp
# flagcheck 0x68:0x68+0x74
Zzzzz:\I wonder if this path even has to be real.Maybe?No


# 2
# calculator 0x143:0x143+0x94
C:\Users\Administrator.000\AppData\Local\AutoIt v3\Aut2Exe\aut1CFB.tmp.tok
# flagcheck 0x12B:0x12B+0x7C
Another path i need to fake so you dont see my windows user))

# 3
# calculator 0x1F4:0x1F4+0x2828
# ...script code
# flagcheck 0x1C4:0x1C4+0x2C78
# ...script code

尝试把代码部分变成正常的加密方式,用ida脚本(条件记录断点)提取出两个程序异或的字符流,然后异或。

用工具反编译成功

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
#cs
省略
#ce

Func oooooooo(ByRef $oo)
Local $ooooooooo = StringToASCIIArray("oOoOooOO")
Local $ooooo
For $ooo = 1 To BinaryLen(Binary($oo)) Step +1
$ooooo &= Chr(BitXOR($ooooooooo[Mod($ooo - 1, UBound($ooooooooo))], BinaryMid(Binary($oo), $ooo, 1)))
Next
Return $ooooo
EndFunc

Opt(oooooooo("0x3826011b061b232a222e1b2c0722202b0a"), 2)
ClipPut("")
Local $ooooooooooooooooooooooooooo = oooooooo("0x097e0b7609097f78582b5b7f5f582c780d775c7a0e5a7a78587a5e7a095b7a2e5b7c0c7d0b09297c0c765a295d0b2e790b7f587b58097f2a572c572b090b7b7c0e78572c5b572e7d5e7f5e7d5e5a792958760b7c590d7b7f0d2c5d7b585d78770a7b5e2c5c5a777c5f7e5e7b5d0c2b2a562e0b780b0d7b7d5b775a2c560a7b2e5c7f0b2d575f767d5b2a582e570a2e7d0929577c5b58787e0b7d582e56097e785b2a582a0e0e78790c2e59780a0b2d7d097e577b0d0b2b7e5e295f2e5a097e2e0b2b097b5a0a7b2e5b7f0e29560d767b0b2e0c7a565b297b0a7e5d7f095c76795d2a0e7e5a0e2a29097c572c575a7f7d5e7a0c295f0a2d7b0e2d0a7e0b0c7b2a5a29592c0a0b7e76587d5b77560e7c2a5e775e775e097a2b09295d7c5e092d780b7b5f78090c7d775d7c5f2958097a760d2d0e2c5d562a2b577e577a565c2e7c5f765d775b5c7d2d0c2e0e2a5f592a2a0c7d0d7c0b0b7d7c5a77582a5a59767a0e79562b5e092d2d5d785a7b0e097c7a0d79092c0a0c2b79097f097f57572e295b7b5a7a585f787c0a7c5e2a5b0b2d2b0d7b5b2d5d587c7b5e2e592a5d5c7b290d2e5f79590d7a7b0c7f0e7d575c7d79597f5b79595c772c587b5c7e0b0b7a7e5f2d5a78575b2b2d597b0a780d0e7e795d7b0976585a7a7e567c57785d562a2d0d2b0c290e577e2c587f0e775f0e2e7b09795a2d5c58292c5f765f7f58092978")
WinActivate(oooooooo("0x09230e28411b373b"))
WinWaitActive(oooooooo("0x09230e28411b373b"))
Send(oooooooo("0x31342700222a32"))
For $o = 1 To 2 * 2 * 2 * 2 Step +1
Send(oooooooo("0x44343d0628271b32"))
Send(oooooooo("0x44343d0628271b32"))
Send(oooooooo("0x44343d0628271b32"))
Send(oooooooo("0x312c"))
Send(oooooooo("0x141d2608273b32"))
Local $ooooooooooooooooooooooooo = Binary(oooooooo("0x5f37") & StringMid($ooooooooooooooooooooooooooo, ($o - 1) * (8 + 16 - 8) * 2 + 1 - 2 + 3 - 1, 2 * 2 * 2 * 2 * 2))
Local $oooooooooooooooooooooo = _crypt_hashdata(ClipGet(), 32771)
If $ooooooooooooooooooooooooo <> $oooooooooooooooooooooo Then
MsgBox(0, oooooooo("0x23200c240a0b6f1f2a"), oooooooo("0x26210c201d1d2a2c1b6e"))
Exit (BitXOR(57005, 57006))
EndIf
Next
MsgBox(0, oooooooo("0x23200c240a0b6f1f2a"), oooooooo("0x2c201d3d0a0c3b6e"))
Exit (BitXOR(57005, 57005))

很明显这里是一些加密

根据这篇文章,我们可以在命令行运行脚本,并输出一些数据。

进行适当的输出后,我们知道了oooooooo这个函数是负责加密字符串,然后能看到解密后的字符串。

ClipGet函数获取剪切板中的字符串,哈希后和指定结果比对

稍微修改下,可以提取出哈希值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Func oooooooo(ByRef $oo)
Local $ooooooooo = StringToASCIIArray("oOoOooOO")
Local $ooooo
For $ooo = 1 To BinaryLen(Binary($oo)) Step +1
$ooooo &= Chr(BitXOR($ooooooooo[Mod($ooo - 1, UBound($ooooooooo))], BinaryMid(Binary($oo), $ooo, 1)))
Next
Return $ooooo
EndFunc

Local $ooooooooooooooooooooooooooo = oooooooo("0x097e0b7609097f78582b5b7f5f582c780d775c7a0e5a7a78587a5e7a095b7a2e5b7c0c7d0b09297c0c765a295d0b2e790b7f587b58097f2a572c572b090b7b7c0e78572c5b572e7d5e7f5e7d5e5a792958760b7c590d7b7f0d2c5d7b585d78770a7b5e2c5c5a777c5f7e5e7b5d0c2b2a562e0b780b0d7b7d5b775a2c560a7b2e5c7f0b2d575f767d5b2a582e570a2e7d0929577c5b58787e0b7d582e56097e785b2a582a0e0e78790c2e59780a0b2d7d097e577b0d0b2b7e5e295f2e5a097e2e0b2b097b5a0a7b2e5b7f0e29560d767b0b2e0c7a565b297b0a7e5d7f095c76795d2a0e7e5a0e2a29097c572c575a7f7d5e7a0c295f0a2d7b0e2d0a7e0b0c7b2a5a29592c0a0b7e76587d5b77560e7c2a5e775e775e097a2b09295d7c5e092d780b7b5f78090c7d775d7c5f2958097a760d2d0e2c5d562a2b577e577a565c2e7c5f765d775b5c7d2d0c2e0e2a5f592a2a0c7d0d7c0b0b7d7c5a77582a5a59767a0e79562b5e092d2d5d785a7b0e097c7a0d79092c0a0c2b79097f097f57572e295b7b5a7a585f787c0a7c5e2a5b0b2d2b0d7b5b2d5d587c7b5e2e592a5d5c7b290d2e5f79590d7a7b0c7f0e7d575c7d79597f5b79595c772c587b5c7e0b0b7a7e5f2d5a78575b2b2d597b0a780d0e7e795d7b0976585a7a7e567c57785d562a2d0d2b0c290e577e2c587f0e775f0e2e7b09795a2d5c58292c5f765f7f58092978")

For $o = 1 To 2 * 2 * 2 * 2 Step +1

Local $ooooooooooooooooooooooooo = Binary(oooooooo("0x5f37") & StringMid($ooooooooooooooooooooooooooo, ($o - 1) * (8 + 16 - 8) * 2 + 1 - 2 + 3 - 1, 2 * 2 * 2 * 2 * 2))
Local $input = "2"
Local $oooooooooooooooooooooo = _crypt_hashdata($input, 32771)
ConsoleWrite(StringToBinary($ooooooooooooooooooooooooo))
ConsoleWrite(Chr(10))
Next
MsgBox(0, oooooooo("0x23200c240a0b6f1f2a"), oooooooo("0x2c201d3d0a0c3b6e"))
Exit (BitXOR(57005, 57005))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0xF1D9FF077D4007C7B835A5577515F45A
0x43C2DFF3C95F2DA6D0747F0E8C8DFD43
0xA78C48A21012156F79D36B40BC247278
0xE41C358301142CDE9AD7DB42485C9E4A
0x30DB80924E7A8EA2FF834771D27A9F17
0x4E7EAA76CA67EDB2F184BDD11F0A5F1A
0xDDF45E4A40AF9B94DAC594F4E120F396
0x2EA15AEFF38C850215CF0EB4ABE1DC4E
0x5F6CED1972489A3E18181F5DFF231FB7
0xD407FC28230F7F59BBAC29ED818593A3
0x0928432BCAAE06EEC2B3DD23587E5695
0xA69D1FBB2754AF35B6FCECD6F0F088AF
0x44557073E31E4DBDB44B27341A6E234F
0xBA066B54C0A283266046638C7431DD51
0x0B5784DB64E7BA1624F97551938729EB
0xBDCFA81C70A80AA4F65B37FC09007FF7

哈希的过程在上面我省略的一些函数里。_crypt_derivekey的参数包括calg_md5,试一下md5。用somd5一下就能解出来。写个脚本爆破即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from hashlib import md5
res = ["F1D9FF077D4007C7B835A5577515F45A", "43C2DFF3C95F2DA6D0747F0E8C8DFD43", "A78C48A21012156F79D36B40BC247278", "E41C358301142CDE9AD7DB42485C9E4A", "30DB80924E7A8EA2FF834771D27A9F17", "4E7EAA76CA67EDB2F184BDD11F0A5F1A", "DDF45E4A40AF9B94DAC594F4E120F396", "2EA15AEFF38C850215CF0EB4ABE1DC4E", "5F6CED1972489A3E18181F5DFF231FB7", "D407FC28230F7F59BBAC29ED818593A3", "0928432BCAAE06EEC2B3DD23587E5695", "A69D1FBB2754AF35B6FCECD6F0F088AF", "44557073E31E4DBDB44B27341A6E234F", "BA066B54C0A283266046638C7431DD51", "0B5784DB64E7BA1624F97551938729EB", "BDCFA81C70A80AA4F65B37FC09007FF7", ]

def bruce(s):
for i in range(0x20, 0x7f):
for j in range(0x20, 0x7f):
for k in range(0x20, 0x7f):
tmp = bytes([i,j,k])
if md5(tmp).hexdigest().upper() == s:
return tmp

flag = b""
for i in res:
flag+=bruce(i)
print(flag.decode())

得到flag:

1
p4{1_h0p3_y0u_d1dnt_vi0lat3_th3_@ut0it_lic3ns3!}

作者告诉我们不要违反autoit的license,去逆向破解autoit编译的可执行文件。。。

在Autoit的wiki也有着一篇文章关于反编译Autoit:

https://www.autoitscript.com/wiki/Decompiling_FAQ