挑战极限:CSIT信息安全挑战赛中的逆向工程与密码学攻防
引言:红色警报! 🔗
上月,战略信息通信技术中心(CSIT)邀请本地网络安全爱好者参加信息安全挑战赛(TISC)。该挑战赛采用夺旗赛形式,包含6个难度递增的网络安全与编程挑战。
新年前夜,PALINDROME组织的黑客对一家大型金融公司发起勒索软件攻击,加密了其部分关键数据服务器。你的任务是完成一系列操作,尽可能恢复数据,避免公司向PALINDROME屈服。任务难度将逐步增加,请准备好迎接挑战。
带着这份激动人心的介绍,我投入了一系列涵盖逆向工程、二进制利用和密码学的难题。这远超出我熟悉的应用安全领域,但由于我想提升这些技能,这是一个受欢迎的挑战。
第一阶段:这是什么? 🔗
第一个挑战描述了情况:用户信任了恶意的StackOverflow答案,在计算机上运行了未知脚本,因此无意中下载并执行了勒索软件。
第一阶段提供了一个由脚本下载的可疑zip文件。根据描述,该文件受简单密码(6字符十六进制)保护,并有多层压缩。我需要提取其中的加密数据。
破解密码相对简单。我尝试用for i in {0..16777215}; do echo $(printf "%06X\n" $i) >> hex.txt; done生成可能的十六进制密码组合,但耗时过长,因此改用Rust脚本:
1
2
3
4
5
|
fn main() {
for n in 0..16777216 {
println!("{:06x}", n);
}
}
|
我用rustc gen_hex.rs构建可执行文件,然后用./gen_hex > hex.txt重定向输出。接下来,用fcrackzip -D -p hex.txt suspicious.zip -u暴力破解候选密码。我偏好使用fcrackzip工具破解zip文件,因为它会在认为匹配密码时自动尝试解压文件,避免误报。几分钟后,我获得了密码。
解压文件后,提取出temp.mess文件。运行file temp.mess返回temp.mess: zlib compressed data,表明这是一个zlib文件。根据证据,恶意脚本还运行了sudo apt install git wget zip unzip lzma gzip bzip2 python3 pip3,因此我预计会遇到多种压缩格式。用pigz -d < temp.mess > temp.mess2解压zlib文件后,file temp.mess2返回temp.mess2: bzip2 compressed data, block size = 900k。之后还有另一个压缩文件,依此类推。这是一个可怕的俄罗斯套娃式嵌套压缩。到第20层时,我决定必须自动化处理,毕竟这也是一个编程挑战。
通过一些试错,我缩小了各种压缩格式及其解压命令的范围,并将它们整理成bash脚本:
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
|
#!/bin/bash
i=1
while :
do
file=$(file compressed$i.unk);
echo $file;
let "next = i + 1";
if [[ "$file" == *zlib* ]] || [[ "$file" == *TeX* ]]; then
echo "compressed$i.unk is zlib";
pigz -d < compressed$i.unk > compressed$next.unk;
elif [[ "$file" == *bzip2* ]]; then
echo "compressed$i.unk is bzip2";
bzip2 -dc compressed$i.unk > compressed$next.unk;
elif [[ "$file" == *gzip* ]]; then
echo "compressed$i.unk is gzip";
gunzip -c compressed$i.unk > compressed$next.unk;
elif [[ "$file" == *XZ* ]]; then
echo "compressed$i.unk is XZ";
unxz compressed$i.unk -S unk -c > compressed$next.unk;
elif grep -q -E "^([0-9A-Fa-f]{2})+$" "compressed$i.unk"; then
echo "compressed$i.unk is hex";
cat compressed$i.unk | xxd -r -p > compressed$next.unk;
elif grep -q -E "(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?" "compressed$i.unk"; then
echo "compressed$i.unk is base64";
base64 -d compressed$i.unk > compressed$next.unk;
else
exit 1;
fi
let "i+=1";
done
|
你可能想知道为什么我检查TeX格式文件。这是因为在压缩层中间,挑战抛出了一个曲线球,返回一个被file识别为TeX字体度量数据的文件((w\332\203\326\335\367\275\365\276\256\262\356\316\271\232\225y\262\345\327P\027\257u\265\265\266\273\233\226w,wf7w(;\356\344u1\214\373\276]=\266\225\272\367y\324\236\367n\275\272\275\334\256\257\276\367\243\357\271\332n\367\274\227\275\207ml\271\261\355\323\255+\034\335s{\275y\346\325\267\313_})。这让我困惑,因为我不知道如何处理TeX字体文件。幸运的是,当我用binwalk而不是file重新检查文件时,发现它只包含另一个zlib文件。
1
2
3
4
5
6
|
$ binwalk -e compressed36.unk
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Zlib compressed data, default compression
7 0x7 bzip2 compressed data, block size = 900k
|
添加此检查后,我的脚本完美完成解压,通过了约150层压缩!
最终文件是一个简单的JSON,包含标志:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
{
"anoroc":"v1.320",
"secret":"TISC20{q1_418f04b27e58165f62b6bddfc47cf6d1}",
"desc":"Submit this.secret to the TISC grader to complete challenge",
"constants":[
1116352408,
1899447441,
3049323471,
3921009573,
961987163,
1508970993,
2453635748,
2870763221
],
"sign":"0lqcBkBNXPqA"
}
|
第二阶段:找到密钥 🔗
下一阶段提供了勒索软件样本本身,并要求找到嵌入在二进制中的base64字符串形式的公钥。勒索软件名为anoroc,以保持回文主题。尽管挑战提供了Dockerfile以在容器中运行勒索软件,我决定像正常人一样工作,将其放入虚拟机以便使用图形界面。
我从静态分析开始,在IDA中打开anoroc。不幸的是,它未能分析二进制文件。检查字符串子视图时,我发现了以下字符串:$Info: This file is packed with the UPX executable packer http://upx.sf.net $\n。
啊哈!我只需用upx -d anorocware解压它。
使用新解压的文件,IDA将anoroc正确分析为64位ELF二进制文件,返回大量调试符号。在函数子视图的第一眼,我猜测这是一个Golang二进制文件,因为导入了net/http和encoding/json等函数。不出所料,公钥未在字符串子视图中找到。它可能在一个函数内实例化。因此,我转向使用GDB进行动态分析。
在IDA的伪代码中,我注意到在调用net_http___Client__PostForm之前,勒索软件会运行main_QbznvaAnzrTrarengvbaNytbevguz。反过来,该函数调用net_http___Client__Get后跟encoding_json___Decoder__Decode。还有对math_rand___Rand__Intn和runtime_concatstring3的调用,这表明main_QbznvaAnzrTrarengvbaNytbevguz是域名生成函数。
通过在GDB中调试net/http.(*Client).Get并转递给它的参数,我发现HTTPS请求是一个GET请求到https://worldtimeapi.org/api/timezone/Etc/UTC.json,返回一堆时间相关数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
{
"abbreviation":"UTC",
"client_ip":"<IP ADDRESS>",
"datetime":"2020-09-07T18:29:49.031611+00:00",
"day_of_week":1,
"day_of_year":251,
"dst":false,
"dst_from":null,
"dst_offset":0,
"dst_until":null,
"raw_offset":0,
"timezone":"Etc/UTC",
"unixtime":1599503389,
"utc_datetime":"2020-09-07T18:29:49.031611+00:00",
"utc_offset":"+00:00",
"week_number":37
}
|
因此,我怀疑勒索软件使用API响应中的数据生成C2域名。我决定通过将请求重定向到worldtimeapi.org到我自己的本地服务器来测试我的假设。不幸的是,我的第一次重定向尝试失败。我编辑/etc/hosts将worldtimeapi.org指向127.0.0.1(我的本地服务器运行处),但恶意软件失败并显示panic: Get "https://worldtimeapi.org/api/timezone/Etc/UTC.json": tls: first record does not look like a TLS handshake。它可能检查安全的HTTPS/TLS连接。因此,我使用网络上的片段运行了一个带有自签名证书的HTTPS服务器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# python3 version, derived from python2 version https://gist.github.com/dergachev/7028596
#
# taken from http://www.piware.de/2011/01/creating-an-https-server-in-python/
# generate server.xml with the following command:
# openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
# run as follows:
# python3 simple-https-server.py
# then in your browser, visit:
# https://localhost:4443
import http.server
import ssl
httpd = http.server.HTTPServer(('localhost', 4443), http.server.SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket (httpd.socket, certfile='./server.pem', server_side=True)
httpd.serve_forever()
|
不幸的是,这未能欺骗勒索软件,它再次失败并显示panic: Get "https://worldtimeapi.org/api/timezone/Etc/UTC.json": x509: certificate signed by unknown authority。由于恶意软件使用的HTTPS协议,这是一个死胡同。
我决定在运行时修补可执行文件,使用GDB修改传递给net/http.(*Client).Get函数的URL。经过一些试错,我成功将字符串从https://worldtimeapi.org/api/timezone/Etc/UTC.json更改为http://xworldtimeapi.org/api/timezone/Etc/UTC.json,使用set {int64}0x6f9966 = 0x782f2f3a70747468(十六进制解码为x//:ptth,小端格式反转)。通过将URL开头从https://改为http://,我有效地将请求协议从HTTPS更改为HTTP,这将不检查有效证书!接下来,我修改hosts文件将xworldtimeapi.org指向我使用python3 -m http.server 80启动的本地服务器。这样,恶意软件成功运行并将API请求发送到我的服务器。
然而,每次运行都修补参数非常耗时,因此我写了一个GDB脚本来自动化此过程:
1
2
3
4
5
6
7
8
|
b main.QbznvaAnzrTrarengvbaNytbevguz
b net/http.(*Client).Get
r
c
c
set {int64}0x6f9966 = 0x782f2f3a70747468
c
quit
|
每当我用脚本运行GDB时,恶意软件会自动修补。
立即,我注意到如果我发送相同的JSON响应,anoroc总是请求相同的域名。这证实了anoroc基于JSON响应中的某个值生成域名。接下来,我逐个调整JSON中的每个值,直到请求的域名改变。结果发现,unixtime是种子。
即使有了这些知识,我也无法逆向工程生成域名的算法,因为该代码段被很好地混淆了。然而,利用我现有的设置,我意识到我仍然可以通过修改服务器JSON响应中的unixtime值以匹配挑战给出的时间戳,然后在Wireshark中检查修补后恶意软件发送的DNS查询以检索生成的域名来解决挑战。这使我完全跳过了逆向工程复杂混淆代码的过程。
这工作了……对于前20个答案。在那一刻,我意识到挑战可能期望某种自动化。如果我逆向工程了算法,这会很简单。然而,我找到了一种方法,通过尽可能自动化我的动态求解器来绕过这个问题。例如,我写了一个脚本根据输入自动编辑JSON文件:
1
2
3
4
5
6
7
8
9
10
|
import json
import sys
with open("/home/ubuntu/Documents/fakeworldtimeapi/api/timezone/Etc/UTC.json", "r") as jsonFile:
data = json.load(jsonFile)
data["unixtime"] = int(sys.argv[1])
with open("/home/ubuntu/Documents/fakeworldtimeapi/api/timezone/Etc/UTC.json", "w") as jsonFile:
json.dump(data, jsonFile)
|
接下来,我组合所有脚本来检索挑战、更新JSON,然后启动修补后的二进制文件:
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
|
from pwn import *
import subprocess
conn = remote('fqybysahpvift1nqtwywevlr7n50zdzp.ctf.sg', 31090)
conn.recvuntil('SUBMISSION_TOKEN?')
conn.send('mVwoHxiiprhNnxtEghHOHeylkAYLEGKRnPLoMTCgkfArVTkgkAOgKQPpwudgmCbl\r\n')
#'220 FTP server (vsftpd)'
while True:
try:
question = conn.recvuntil('? ')
print(question)
except:
conn.interactive()
timestamp = question.split()[-1][:-1]
print("Timestamp: {}".format(timestamp))
subprocess.Popen(['python3', '/home/ubuntu/Documents/fakeworldtimeapi/updatejson.py', timestamp], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.Popen(['gdb', '/home/ubuntu/Desktop/anorocware2', '--command=/home/ubuntu/Desktop/gdbcommands'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
domain = str.encode(input("Enter domain: ").strip())
conn.send(domain + b'\r\n')
#'331'
conn.recvline()
#'Please specify the password.\r\n'
conn.close()
|
然而,Python的input有些挑剔,如果我意外输入错误键会立即失败,因此我回退到netcat会话来输入答案。在脚本和一些复制粘贴的帮助下,我最终解决了系列问题(约100个)并获得了标志!
第五阶段:公告板系统 🔗
下一阶段向我展示了运行在攻击者C2上的服务,并要求我黑回去!它还提供了运行服务的二进制文件——某种消息板系统。暗示是我必须在二进制文件中找到一个漏洞来利用攻击者的C2服务器。然而,当我在IDA中打开二进制文件时,它警告我头文件已损坏。我用readelf -h bbs检查了这一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400a60
Start of program headers: 64 (bytes into file)
Start of section headers: 65535 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 6
Size of section headers: 64 (bytes)
Number of section headers: 65535
Section header string table index: 65535 (3539421402)
readelf: Error: Reading 4194240 bytes extends past end of file for section headers
|
似乎3个头文件损坏:节头起始位置、节头数量和节头字符串表索引。使用Wikipedia的文件布局,我在十六进制编辑器中通过将它们设置为0来修复这些头文件到正确的偏移量。
接下来,当我运行二进制文件时,它提示输入用户名和密码,尽管只有来宾账户可用。我必须找出来宾账户的密码。我查看了验证周围的汇编代码,但发现很难完全理解汇编。挑战类似于CrackMe挑战,这是我本会基于的方法。
超时 🔗
不幸的是,此时我时间不足,结束了我的48小时。尽管完全使用它会很好,但我们都有生活要过!我会在闲暇时尝试破解二进制文件。
挑战真正推动我学习新的逆向工程和密码学技能。尽管我是这些领域的新手,但在CTF格式中应用它们教会了我许多实用技巧并建立了我的信心。绝对零应用安全挑战,这迫使