Insomni'hack teaser 2020
这次比赛我们获得了...好吧就签了个道...比赛出的题很有新意,还是很值得一做的。
warmup
This year we added a Proof of Work to some of our challenges. Just run python pow.py \<target\>, were target is the value provided by the server and get the flag.
签到题,类似加密货币的挖矿环节,服务器给出一个哈希值的 0-6 字节,如果我们给出一段文字的哈希值的前 6 位与之相符,就可以拿到 Flag。简而言之就是哈希碰撞。
由给出的程序知道这个需要我们碰撞的 hash 算法是 md5。下面是我写的生成的 hash 和被 hash 值对应的 dict 的小程序,将 dict 用 pickle 保存。
import hashlib
import pickle
dic = {}
def save(obj):
with open('cache.pkl', 'wb') as f:
pickle.dump(dic, f)
i = 0
try:
while i < 1000000:
m = hashlib.md5()
m.update(str(i).encode())
h = m.hexdigest()
dic[h[:6]] = str(i)
if len(dic.items()) % 10000 == 0:
print(len(dic.items()))
i += 1
save(dic)
except KeyboardInterrupt as e:
save(dic)
然后用循环爆破,如果在 dict 里找到了对应的字符串及其 hash,就将字符串传给服务器以拿到 Flag。
import socket
import re
import pickle
regex = re.compile('with "(.+)"')
host = 'welcome.insomnihack.ch'
port = 1337
with open('cache.pkl', 'rb') as f:
dic = pickle.load(f)
while True:
s = socket.socket()
s.connect((host, port))
res = s.recv(1024)
h = regex.findall(res.decode())[0]
if h in dic.keys():
print(f'{h} Found {dic[h]}')
s.send(f'{dic[h]}\n'.encode())
print(s.recv(128))
else:
print(f'{h} Not found')
s.close()
getdents
Oh shit! Data have been stolen from my computer... I looked for malicious activity but found nothing suspicious. Could ya give me a hand and find the malware and how it's hiding?
题目给了 profile 和 .vmem,应该是分析 Linux 内存镜像找出病毒的题。所以这题上了 volatility。需要注意的是 Linux 的 profile 需要放到 volatility 项目目录下的 volatility\plugins\overlays\linux
文件夹,例如我的 volatility 放在了F:\volatility\
,那么打包好的 profile (zip 文件)应该放在 F:\volatility\volatility\plugins\overlays\linux
,我因为放错了位置导致没搞出来...这是官方文档:https://github.com/volatilityfoundation/volatility/wiki/Linux#Linux-Profiles。按照官方文档放好 profile 后就可以使用 profile 进行分析了。
使用命令 python vol.py -f memory.vmem --profile=LinuxUbuntu_4_15_0-72-generic_profilex64 linux_pslist
于是尝试 dump meterpreter,但是发现 meterpreter 是stageless 的,只能通过 netscan
知道他连接上了内网的一台机器。
volatility > python vol.py -f memory.vmem --profile=LinuxUbuntu_4_15_0-72-generic_profilex64 linux_psaux
Volatility Foundation Volatility Framework 2.6.1
Pid Uid Gid Arguments
...
1750 0 0 sudo ./meterpreter
1751 0 0 ./meterpreter
...
2950 1000 1000 /usr/lib/deja-dup/deja-dup-monitor
2964 0 0 /bin/sh
volatility > python vol.py -f memory.vmem --profile=LinuxUbuntu_4_15_0-72-generic_profilex64 linux_netscan
Volatility Foundation Volatility Framework 2.6.1
...
8a9df81f6000 TCP 192.168.180.132 :51934 192.168.180.131 : 1337 ESTABLISHED
于是看看文件 python vol.py -f memory.vmem --profile=LinuxUbuntu_4_15_0-72-generic_profilex64 linux_enumerate_file | less
。看见了看见了 home
目录下的 rkit.ko
。根据后缀名判断这是个内核模块。使用python vol.py -f memory.vmem --profile=LinuxUbuntu_4_15_0-72-generic_profilex64 linux_find_file -i 0xffff8a9dd42755e8 -O rkit.ko
导出了文件
0xffff8a9dc38422e8 2363716 /home/julien/Downloads
0x0 ------------------------- /home/julien/Downloads/.hidden
0xffff8a9dd42755e8 2359303 /home/julien/Downloads/rkit.ko
0xffff8a9d948151a8 2367790 /home/julien/Downloads/meterpreter
接下来就是逆向了。大致猜测程序先使用 base64 解密了某一个字符串,然后通过网络获取数据将 pk
进行第一次 xor 解密得到 sk
,然后再通过其他方式调用了解密函数将 sk
和 ep
进行异或,((unsigned __int64)(0x8421084210842109LL * (unsigned __int128)(v4 >> 1) >> 64) >> 4)
是被优化的取模运算,可以带入 python 中跑一下就知道了。
通过 volshell 提取出数据。首先通过 IDA 中显示的偏移和内存中相同数据的位置算出 module 的基地址 '0xffffffffc0942700',然后就可以通过这个基地址 + 偏移计算出程序不同数据的位置,就可以进行数据提取了。
memory:
0xffffffffc0943080 00 00 00 00 00 00 00 00 88 30 94 c0 ff ff ff ff .........0......
0xffffffffc0943090 88 30 94 c0 ff ff ff ff 72 6b 69 74 00 00 00 00 .0......rkit....
IDA:
0000000000000980 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000000000990 00 00 00 00 00 00 00 00 72 6B 69 74 00 00 00 00 ........rkit....
base = 0xffffffffc0943090 - 0x990 -> 0xffffffffc0942700
sk = [0xe5, 0xd1, 0xa6, 0xf8, 0xc1, 0xf0, 0xd5, 0xdd, 0xf9, 0xe6, 0xca, 0xc6, 0xdb, 0xf4, 0xf6, 0xe1,
0xda, 0xd4, 0xe7, 0xd9, 0xfd, 0xc7, 0xa5, 0xfc, 0xc8, 0xc4, 0xe4, 0xde, 0xe3, 0xa2, 0xf7, 0xc5,
0xfe, 0xa3, 0xff, 0xd0, 0xc3, 0xe0, 0xab, 0xc2, 0xa7, 0xd8, 0xd7, 0xe2, 0xdf, 0xeb, 0xdc, 0xaa,
0xa1, 0xa0, 0xd3, 0xcb, 0xa4, 0xf1, 0xfa, 0xc0, 0xfb, 0xf5, 0xd6, 0xf3, 0xea, 0xe8, 0x00, 0x00,
0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x12, 0xfc, 0xa1, 0x9d, 0x8a, 0xff, 0xff,
0x40, 0x02, 0x60, 0xab, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
ep = [0xac, 0x9f, 0xf5, 0x83, 0x93, 0xc0, 0xe5, 0xa9, 0xb2, 0xd7, 0xbe, 0x80, 0xeb, 0x86, 0xa4, 0x8e,
0xea, 0xbf, 0x8e, 0xbc, 0x8e, 0xba, 0x00, 0x00, 0x60, 0xda, 0xaf, 0xaa, 0xff, 0xff, 0xff, 0xff,
0x7a, 0x73, 0x84, 0xf8, 0x09, 0xcc, 0xdd, 0xc4, 0xd0, 0x9b, 0xaa, 0xaa, 0xff, 0xff, 0xff, 0xff,
0xb0, 0x9b, 0xaa, 0xaa, 0xff, 0xff, 0xff, 0xff, 0xb0, 0x4e, 0xb0, 0xaa, 0xff, 0xff, 0xff, 0xff,
0xc0, 0xf0, 0x68, 0xc0, 0x51, 0xd7, 0xff, 0xff, 0x80, 0xf0, 0x68, 0xc0, 0x51, 0xd7, 0xff, 0xff,
0x00, 0xf1, 0x68, 0xc0, 0x51, 0xd7, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
print(''.join(chr(sk[i] ^ ep[i]) for i in range(62)))
kaboom
Defuse the bomb!
这个程序一打开就输出了 "KABOOM!",也没什么输入之类的,非常符合题目描述:拆弹。
当时在和队友讨论时就发现未脱壳的原程序不对劲,未脱壳程序中出现了两个 "INS" (Flag 开头)的字符串。一般情况下 UPX 会把字符串也一起压缩,出现字符串说明可能有其他程序藏在了未脱壳程序外面。
和队友讨论时候想到了上面的情况,但是没往那个方向做...
1
上面是 UPX 加壳程序和题目的部分对比,可以发现最大的特点在于题目的代码开始在 UPX2 段,然后 jmp 到 UPX1 段上继续解密。在下图中的代码中跳转到了原本的解压函数,两个程序就非常相似了。
2
那么重点放在不一样的代码上,直接啃汇编发现一部分代码是在解析 PE 文件以动态调用 GetCommandLineA
获取命令行参数。
UPX2:0046F0A8 pusha
UPX2:0046F0A9 xor ecx, ecx
UPX2:0046F0AB mov eax, fs:[ecx+30h]
UPX2:0046F0AF mov eax, [eax+0Ch]
UPX2:0046F0B2 mov esi, [eax+14h]
UPX2:0046F0B5 lodsd
UPX2:0046F0B6 xchg eax, esi
UPX2:0046F0B7 lodsd
UPX2:0046F0B8 mov ebx, [eax+10h]
UPX2:0046F0BB mov edx, [ebx+3Ch]
UPX2:0046F0BE add edx, ebx
UPX2:0046F0C0 mov edx, [edx+78h]
UPX2:0046F0C3 add edx, ebx
UPX2:0046F0C5 mov esi, [edx+20h]
UPX2:0046F0C8 add esi, ebx
UPX2:0046F0CA xor ecx, ecx
UPX2:0046F0CC
UPX2:0046F0CC loc_46F0CC: ; CODE XREF: start+2E↓j
UPX2:0046F0CC ; start+37↓j ...
UPX2:0046F0CC inc ecx
UPX2:0046F0CD lodsd
UPX2:0046F0CE add eax, ebx
UPX2:0046F0D0 cmp dword ptr [eax], 43746547h
UPX2:0046F0D6 jnz short loc_46F0CC
UPX2:0046F0D8 cmp dword ptr [eax+4], 616D6D6Fh
UPX2:0046F0DF jnz short loc_46F0CC
UPX2:0046F0E1 cmp dword ptr [eax+8], 694C646Eh
UPX2:0046F0E8 jnz short loc_46F0CC
UPX2:0046F0EA cmp dword ptr [eax+0Ch], offset word_41656E
UPX2:0046F0F1 jnz short loc_46F0CC
UPX2:0046F0F3 mov esi, [edx+24h]
UPX2:0046F0F6 add esi, ebx
UPX2:0046F0F8 mov cx, [esi+ecx*2]
UPX2:0046F0FC dec ecx
UPX2:0046F0FD mov esi, [edx+1Ch]
UPX2:0046F100 add esi, ebx
UPX2:0046F102 mov edx, [esi+ecx*4]
UPX2:0046F105 add edx, ebx
UPX2:0046F107 call edx ; 调用 GetCommandLineA
UPX2:0046F109 jmp short loc_46F10C
一部分则是在计算命令行参数的长度,然后跳转到命令行参数的末尾,从尾往头检查命令行参数。
UPX2:0046F10B
UPX2:0046F10B loc_46F10B: ; CODE XREF: start+67↓j
UPX2:0046F10B inc eax
UPX2:0046F10C
UPX2:0046F10C loc_46F10C: ; CODE XREF: start+61↑j
UPX2:0046F10C cmp byte ptr [eax], 0 ; 计算命令行参数的长度
UPX2:0046F10F jnz short loc_46F10B
UPX2:0046F111 push 0
UPX2:0046F113 push 33E377FDh
UPX2:0046F118 push 0D7831BBAh
UPX2:0046F11D push 4CE1B463h
UPX2:0046F122 push 42h
UPX2:0046F124 pop ecx
UPX2:0046F125
UPX2:0046F125 loc_46F125: ; CODE XREF: start+8F↓j
UPX2:0046F125 xor edx, edx
UPX2:0046F127 dec eax
UPX2:0046F128 add cl, [eax]
UPX2:0046F12A cmp cl, [esp+10h+var_10] ; 比较
UPX2:0046F12D jz short loc_46F132
UPX2:0046F12F or dl, 1
UPX2:0046F132
UPX2:0046F132 loc_46F132: ; CODE XREF: start+85↑j
UPX2:0046F132 inc esp
UPX2:0046F133 cmp [esp+10h+var_10], 0 ; 比较字符串是否结束
UPX2:0046F137 jnz short loc_46F125
如果上面的检查结束发现命令行参数成功匹配,则将二进制文件中加密的假 Flag 替换成加密的真 Flag,然后返回 UPX 解压部分代码。
UPX2:0046F139 pop ecx
UPX2:0046F13A cmp dl, 0
UPX2:0046F13D jnz short loc_46F15B
UPX2:0046F13F
UPX2:0046F13F loc_46F13F: ; CODE XREF: start+B1↓j
UPX2:0046F13F mov al, ds:byte_46F160[ecx]
UPX2:0046F145 mov byte ptr ds:word_46D1A7[ecx], al
UPX2:0046F14B mov ds:byte_46F160[ecx], 0
UPX2:0046F152 inc ecx
UPX2:0046F153 cmp ecx, 11A1h
UPX2:0046F159 jb short loc_46F13F
UPX2:0046F15B
UPX2:0046F15B loc_46F15B: ; CODE XREF: start+95↑j
UPX2:0046F15B jmp loc_46E351
用 python 很容易写出解密脚本,输出部分命令行参数 "Plz&Thank-Q"
LazyIDA 真好用
encoded = b"\x42"
encoded += b"\x63\xB4\xE1\x4C"
encoded += b"\xBA\x1B\x83\xD7"
encoded += b"\xFD\x77\xE3\x33"
decoded = ''
for i in range(0, len(encoded) - 1):
decoded = chr((encoded[i+1] - encoded[i]) & 0xFF) + decoded
print(decoded)
根据字符串可以定位到输出 "KABOOM!" 的地方,通过在 x32dbg 中添加命令行参数,在输出点下断点就可以看到 x32dbg 显示出真正的 Flag 了。
3
但是这其实还没完,根据 WP,在主函数中的第一个检查是命令行参数的个数,如果个数少于 2,第二个参数不是 "defuse",就不输出 Flag。所以 "拆弹" 的真正方式是在 Powershell 中以 ".\kaboom.exe defuse Plz`&Thank-Q!" (要转义 &)启动程序。
静态编译的程序 + 取出符号表是逆向噩梦啊
signed int __cdecl print_result(signed int argc, char **a2)
{
int v2; // eax
signed int result; // eax
if ( argc >= 2
&& !strcmp((int)a2[1], (int)aDefuse)
&& (v2 = sub_401523((int)aIns), !sub_4011AE((int)aIns_0, (int)aHttpsWwwYoutub, v2)) )
{
sub_401393(aCongratsTheFla, (unsigned int)aHttpsWwwYoutub_0);
result = 0;
}
else
{
sub_401393(aS, (unsigned int)aKaboom);
result = 1;
}
return result;
}
这是输出
PS C:\Users\chenx> .\kaboom-origin.exe defuse Plz`&Thank-Q!
Congrats! The flag is INS{GG EZ clap PogU 5Head B) Kreygasm <3<3}
refs
https://ctftime.org/writeup/17984
https://github.com/Insomnihack/Teaser-2020/tree/master/kaboom