挑战时钟:CSIT信息安全竞赛
引言:红色警报! 🔗
上月,新加坡战略资讯科技中心(CSIT)邀请本地网络安全爱好者参加信息安全挑战赛(TISC)。比赛采用夺旗赛形式,包含6个难度递增的网络安全与编程挑战。
新年前夜,PALINDROME黑客组织对某金融公司发起勒索软件攻击,加密了其关键数据服务器。你的任务是通过一系列操作尽可能恢复数据,避免公司向黑客屈服。任务难度逐步增加,需做好全力应对准备。
基于这一背景,我 tackling 了一系列涉及逆向工程、二进制漏洞利用和密码学的难题。这远超我熟悉的应用安全领域,但为了提升技能,我欣然接受挑战。
第一阶段:这是什么? 🔗
首关介绍了背景:用户因信任恶意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
),输出重定向到hex.txt
,再用fcrackzip -D -p hex.txt suspicious.zip -u
暴力破解。fcrackzip工具会在认为密码匹配时自动解压,避免误报。几分钟后,密码破解成功。
解压后得到temp.mess
文件。file temp.mess
显示其为zlib压缩数据。恶意脚本还运行了sudo apt install git wget zip unzip lzma gzip bzip2 python3 pip3
,故预期会遇到多种压缩格式。用pigz -d < temp.mess > temp.mess2
解压后,file temp.mess2
显示为bzip2压缩数据。此后还有更多压缩层,如同俄罗斯套娃。到第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字体度量数据的文件(内容略)。我一度困惑,但用binwalk -e compressed36.unk
检查发现实为zlib文件:
1
2
3
4
|
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Zlib compressed data, default compression
7 0x7 bzip2 compressed data, block size = 900k
|
添加此逻辑后,脚本成功解压约150层,最终得到含flag的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"
}
|
第二阶段:寻找密钥 🔗
本阶段提供勒索软件样本(名为anoroc,呼应回文主题),要求找出二进制中嵌入的Base64公钥。虽提供Dockerfile,我选择在虚拟机中分析以便使用图形界面。
静态分析:用IDA打开anoroc,分析失败。查看字符串子视图,发现$Info: This file is packed with the UPX executable packer http://upx.sf.net $\n
。用upx -d anorocware
解压后,IDA成功分析为64位ELF二进制,含大量调试符号。从函数子视图看,因导入net/http
和encoding/json
等函数,推测为Golang二进制。公钥未在字符串中找到,可能实例化于函数内,故转向GDB动态分析。
IDA伪代码中,注意到encoding_base64__ptr_Encoding_DecodeString
在encoding_pem_Decode
和crypto_x509_ParsePKIXPublicKey
前调用,推测此处读取并使用公钥。
用gdb anorocware2
启动GDB,info functions
获取函数名列表,对base64解码函数设断点:b encoding/base64.(*Encoding).DecodeString
,运行r
。断点触发后,info args
显示截断的Base64字符串。设set print elements 0
打印完整字符串。
确认Base64字符串解码为公钥后,哈希得到flag。
第三阶段:恢复文件 🔗
除样本外,挑战提供多个被anoroc加密的文件,需从加密数据库文件中提取flag。
动态测试知,anoroc加密所有非.txt
或.anoroc
扩展名的文件。从IDA伪代码重构加密算法:主函数中,path_filepath_Walk
前调用main_visit
,main_visit_func1
调用crypto_aes_NewCipher
、crypto_cipher_NewCTR
和io_ioutil_WriteFile
,使用main_encKey
和main_encIV
参数,表明使用AES CTR流加密。
追溯main_encKey
和main_encIV
引用,发现它们通过math_rand___Rand__Intn
初始化,每字节设为byte(rand.Intn(1337))
。
据Golang文档,随机函数默认种子为1,但每次运行加密文件不同,故种子应为伪随机值。IDA函数列表中,main_init_0
执行time_Now
后调用math_rand__ptr_Rand_Seed
,表明种子设为勒索软件运行时间。
基于此,重现main_visit_func1
加密算法:
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
|
package main
import (
"crypto/aes"
"crypto/cipher"
"fmt"
"io/ioutil"
"log"
"math/rand"
)
func main() {
// 受害文件
plainFile := "secret_investments.db"
plaintext, err := ioutil.ReadFile(plainFile)
if err != nil {
log.Fatal(err)
}
var seed int64
encKey := make([]byte, 16)
encIV := make([]byte, 16)
// 初始化种子
seed := time.Now().UnixNano() / 1000
rand.Seed(seed)
// 初始化encKey
for i := range encKey {
encKey[i] = byte(rand.Intn(1337))
}
// 初始化encIV
for i := range encIV {
encIV[i] = byte(rand.Intn(1337))
}
// 将encIV前两字节改为文件名前两字母
encIV[0] = byte(encryptedFile[0])
encIV[1] = byte(encryptedFile[1])
// 从encKey初始化密码
block, err := aes.NewCipher(encKey)
if err != nil {
log.Fatal(err)
}
// 从密码和envIV初始化密码流
stream := cipher.NewCTR(block, encIV)
// 用密码流加密明文
ciphertext := make([]byte, len(plaintext))
stream.XORKeyStream(ciphertext, plaintext)
err = ioutil.WriteFile(plainFile+".anoroc", ciphertext, 0644)
if err != nil {
log.Fatal(err)
}
}
|
GDB验证:在math/rand.(*Rand).Seed
设断点转储种子值,插入脚本。初始代码遗漏了算法将IV前两字节设为文件名前两字母,通过调试cipher.NewCTR
参数发现此差异。最终输出匹配。
加密算法弱点分析:
- 加密密钥和IV用
rand.Intn(1337)
初始化,字节范围受限为1337。
- IV前两字节设为文件名前两字母。因密钥和IV不变,相同前两字母的文件使用相同AES-CTR密钥流加密,我曾于Cryptopals挑战中利用此弱点。
- 随机种子设为时间戳,可从加密文件修改时间戳推导。
但无法利用1)和2)。1)仍有1337^16种组合,难以暴力破解。2)需多个相同密钥流加密的密文样本,但符合条件者少,且目标数据库文件不符。
研究时间戳:奇怪的是,anoroc生成的时间戳与我用time.Now()
生成的不同。例如,加密文件修改时间为2020-08-07 01:49:10.000000000 +0800(Unix时间戳1596736150000000),但加密所用种子实为1559245967038138,可能有抖动增加难度。
在虚拟机测试多轮加密,比较种子与“真实”Unix时间戳,抖动约-37486400000000。误差范围大,需暴力破解数百亿候选!故最初拒绝此路,花费多时寻找密码学弱点,最终发现只能暴力破解。
优化攻击:Golang支持Goroutines并发,加速测试。性能调优后,设定100万并发Goroutines(更多会导致系统锁)。研究如何加速暴力解密时,意识到因使用AES-CTR流加密,仅需解密并检查少量字节而非整个文件!此处针对提供的加密PNG文件,暴力破解寻找PNG魔数(前5字节),速度提升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
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
74
75
76
77
|
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"fmt"
"io/ioutil"
"log"
"math/rand"
"sync"
)
func main() {
encryptedFile := "slopes.png.anoroc"
ciphertext, err := ioutil.ReadFile(encryptedFile)
if err != nil {
log.Fatal(err)
}
ciphertext = ciphertext[:5]
var x int64
var y int64
var seed int64
encKey := make([]byte, 16)
encIV := make([]byte, 16)
var wg sync.WaitGroup
y = 0
// 暴力破解时间戳最后8位;并发最后6位以提升性能
for y = 8500; y < 10000; y = y + 1 {
fmt.Printf("Currently bruteforcing %d\n", 1559240000000000+(y*1000000))
for x = 0; x < 1000000; x = x + 1 {
wg.Add(1)
go func(x int64) {
defer wg.Done()
// 从暴力破解时间戳初始化随机种子
seed = 1559240000000000 + x + (y * 1000000)
rand.Seed(seed)
// 初始化encKey
for i := range encKey {
encKey[i] = byte(rand.Intn(1337))
}
// 初始化encIV
for i := range encIV {
encIV[i] = byte(rand.Intn(1337))
}
// 将encIV前两字节改为文件名前两字母
encIV[0] = byte(encryptedFile[0])
encIV[1] = byte(encryptedFile[1])
// 从encKey初始化密码
block, err := aes.NewCipher(encKey)
if err != nil {
log.Fatal(err)
}
// 从密码和envIV初始化密码流
stream := cipher.NewCTR(block, encIV)
// 用密码流解密密文
plaintext := make([]byte, 5)
stream.XORKeyStream(plaintext, ciphertext)
// 检查PNG头匹配以确认暴力破解成功
if bytes.Compare(plaintext, []byte{137, 80, 78, 71, 13}) == 0 {
fmt.Println("Found!")
fmt.Printf("Seed is %d\n", seed)
}
}(x)
}
wg.Wait()
}
}
|
优化前,通宵运行失败。优化后,扩大搜索空间,在多个机器(包括亚马逊EC2)上运行。几小时后,成功暴力破解种子!
但因Goroutines不精确,需在缩小范围内运行无并发脚本二次确认:
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
|
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"fmt"
"io/ioutil"
"log"
"math/rand"
)
func main() {
encryptedFile := "slopes.png.anoroc"
ciphertext, err := ioutil.ReadFile(encryptedFile)
if err != nil {
log.Fatal(err)
}
ciphertext = ciphertext[:5]
var x int64
encKey := make([]byte, 16)
encIV := make([]byte, 16)
// 暴力破解时间戳最后8位
for x = 1559245967000000; x < 1559245968000000; x = x + 1 {
rand.Seed(x)
// 初始化encKey
for i := range encKey {
encKey[i] = byte(rand.Intn(1337))
}
// 初始化encIV
for i := range encIV {
encIV[i] = byte(rand.Intn(1337))
}
// 将encIV前两字节改为文件名前两字母
encIV[0] = byte(encryptedFile[0])
encIV[1] = byte(encryptedFile[1])
// 从encKey初始化密码
block, err := aes.NewCipher(encKey)
if err != nil {
log.Fatal(err)
}
// 从密码和envIV初始化密码流
stream := cipher.NewCTR(block, encIV)
// 用密码流解密密文
plaintext := make([]byte, 5)
stream.XORKeyStream(plaintext, ciphertext)
// 检查PNG头匹配
if bytes.Compare(plaintext, []byte{137, 80, 78, 71, 13}) == 0 {
fmt.Println("Found!")
fmt.Printf("Seed is %d\n", x)
}
}
}
|
匹配种子后,用以下脚本解密数据库文件:
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
|
package main
import (
"crypto/aes"
"crypto/cipher"
"fmt"
"io/ioutil"
"log"
"math/rand"
)
func main() {
encryptedFile := "secret_investments.db.anoroc"
ciphertext, err := ioutil.ReadFile(encryptedFile)
if err != nil {
log.Fatal(err)
}
var seed int64
encKey := make([]byte, 16)
encIV := make([]byte, 16)
// 正确种子
seed = 1559245967038138
rand.Seed(seed)
// 初始化encKey
for i := range encKey {
encKey[i] = byte(rand.Intn(1337))
}
// 初始化encIV
for i := range encIV {
encIV[i] = byte(rand.Intn(1337))
}
// 将encIV前两字节改为文件名前两字母
encIV[0] = byte(encryptedFile[0])
encIV[1] = byte(encryptedFile[1])
// 从encKey初始化密码
block, err := aes.NewCipher(encKey)
if err != nil {
log.Fatal(err)
|