挑战时钟:CSIT信息安全竞赛中的逆向工程与密码学实战

本文详细记录了作者参与CSIT信息安全挑战赛的全过程,涵盖逆向工程、二进制漏洞分析、AES-CTR加密算法破解、动态调试与GDB脚本自动化等技术实战,展示了从压缩包破解到C2服务器域名生成的完整攻击链。

挑战时钟: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/httpencoding/json等函数,推测为Golang二进制。公钥未在字符串中找到,可能实例化于函数内,故转向GDB动态分析。

IDA伪代码中,注意到encoding_base64__ptr_Encoding_DecodeStringencoding_pem_Decodecrypto_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_visitmain_visit_func1调用crypto_aes_NewCiphercrypto_cipher_NewCTRio_ioutil_WriteFile,使用main_encKeymain_encIV参数,表明使用AES CTR流加密。

追溯main_encKeymain_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参数发现此差异。最终输出匹配。

加密算法弱点分析:

  1. 加密密钥和IV用rand.Intn(1337)初始化,字节范围受限为1337。
  2. IV前两字节设为文件名前两字母。因密钥和IV不变,相同前两字母的文件使用相同AES-CTR密钥流加密,我曾于Cryptopals挑战中利用此弱点。
  3. 随机种子设为时间戳,可从加密文件修改时间戳推导。

但无法利用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)
	
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计