强网杯 2022 Writeup
强网杯 2022 Summary
不愧是 PWN 杯,出了 17 道 pwn 题。最近队伍缺 web、re 和 pwn (这可以算是什么都缺了吧喂),我还是照常日我的 misc……
排名 60,没进前 32,差了两道题的程度,一方面可惜,另一方面队员还有待培养……
谍影重重
题目给了一个压缩包,内含一个流量包、一份配置文件和另一个压缩包。题目是一开始就上线了的,所以当时看到那份 config.json
就猜到了这是要做 vmess 的流量解析,于是翻出来了万恶的 vmess 文档,依照文档开始写解析脚本。
用 wireshark 将两份一去一回的流量取出,起了一个 jupyter,开始折腾。
首先解析客户端请求部分,起始 16 字节是认证信息,为一串 HMAC 哈希值,由用户 UUID、范围内随机时间戳计算得到,计算方式为:
hash = HMAC(key: uuid, msg: timestamp, digestmod: md5)
由流量包解析得到发送请求的时间戳,构建 HMAC 哈希函数并进行一次遍历便可得到用于加密的 timestamp
值。后续用于解码指令的 key 和 iv 便是用以上的数据计算得来:
cmd_key = md5(uuid + b"c48619fe-8f02-49e0-b9e9-edf763e17e21")
cmd_iv = md5(p64(timestamp, endian='big') * 4)
其中字节数组是第一处坑,论我把字符数组作为 UUID 后无论如何都无法解密,卡了两三小时…… cmd_iv
就很可爱了,时间戳转换为 uint64 后 padding 到 16 字节,而后计算得到 md5 值作为 key。
client_id = uuid.UUID("b831381d-6324-4d53-ad4f-8cda48b30811").bytes
md5 = lambda x: hashlib.md5(x).digest()
vmess_hmac = lambda x: hmac.new(client_id, x, hashlib.md5).hexdigest()
req = bytes.fromhex("4dd11f9b04f2b562b9db539d939f1d52" + "b48b35bf592c09b21545392f73f6cef91143786464578c1c361aa72f638cd0135f25343555f509aef6c74cd2a2b86ee0a9eb3b93a81a541def4763cc54f91ba02681add1b815e8c50e028c76bde0ee8a9593db88d901066305a51a9586a9e377ee100e7d4d33fcfc0453c86b1998a95275cd9368a68820c2a6a540b6386c146ea7579cfe87b2e459856772efdcf0e4c6ab0f11d018a15561cf409cbc00491d7f4d22b7c486a76a5f2f25fbef503551a0aeb90ad9dd246a9cc5e0d0c0b751eb7b54b0abbfef198b1c4e5e755077469c318f20f3e418af03540811ab5c1ea780c886ea2c903b458a26")
cut_time = 1615528962
target_hash = req[:16].hex()
def get_cmd_iv(time, target_hash):
for t in range(time - 50, time + 50):
cur_hash = vmess_hmac(p64(t))
if cur_hash == target_hash:
print(f"time = {t}")
return md5(p64(t, endian='big') * 4)
cmd_key = md5(client_id + b'c48619fe-8f02-49e0-b9e9-edf763e17e21')
cmd_iv = get_cmd_iv(cut_time, target_hash)
print(f"cmd_key = {cmd_key.hex()}")
print(f"cmd_iv = {cmd_iv.hex()}")
得到:
time = 1615528982
cmd_key = "b50d916ac0cec067981af8e5f38a758f"
cmd_iv = "881eb47d4d3b67b24328c5178c0eedcc"
而后对指令部分进行解码,指令部分使用 AES-128-CFB 进行加密:
from fnvhash import fnv1a_32
cmd_aes = lambda: AES.new(cmd_key, AES.MODE_CFB, cmd_iv, segment_size=128)
cmd = req[16:]
ret = cmd_aes().decrypt(cmd)
print(f"ver = {ret[0:1].hex()}")
print(f"iv = {ret[1:17].hex()}")
print(f"key = {ret[17:33].hex()}")
print(f"v = {ret[33:34].hex()}")
print(f"opt = {ret[34]:b}")
print(f"p = {ret[35:36].hex()[0]}")
print(f"sec = {ret[35:36].hex()[1]}")
p = int(ret[35:36].hex()[0], 16)
print()
print(f"cmd = {ret[37:38].hex()}")
print(f"port = {bytes_to_long(ret[38:40])}")
print(f"type = {ret[40:41].hex()}") #ipv4
print()
print(f"host = {'.'.join(str(i) for i in ret[41:45])}")
print(f"rand = {ret[45:45 + p].hex()}")
print(f"F = 0x{ret[45 + p:45 + p + 4].hex()}")
print(f"check = {hex(fnv1a_32(ret[:45 + p]))}")
data = cmd[45 + p + 4:]
data_iv = ret[1:17]
data_key = ret[17:33]
得到:
ver = "01"
dat_iv = "13277f5732da52ada790d87b8829daa9"
dat_key = "5e4a9aa9ba58c7e3ad36fe2499dca259"
v = "a2"
opt = 0b1101
p = 6
sec = 3
cmd = 0x01
port = 5000
type = 0x01
host = 127.0.0.1
rand = "1ace7d9bb0b5"
F = 0x39182c03
check = 0x39182c03
这里遇到了第二个坑,因为文档太久,依然写着 sec = 0x03
时候所对应的加密方法为 ChaCha20-Poly1305
然后实测了很久依然无法解密成功,持续了约半天到一天后翻找代码才看到 sec = 0x03
时候对应的实际加密方法为 AES-128-GCM
,才有了下一步的处理。
由于需要 SHA3-shake128 生成随机的 padding 长度和 size 掩码,附带整个解密函数,另外所需的全部函数在 Crypto 包中均有提供:
from Crypto.Hash import SHAKE128
class SizeParser:
def __init__(self, nonce):
self.shake = SHAKE128.new(nonce)
def next(self):
return bytes_to_long(self.shake.read(2))
def enc(self, size):
return self.next() ^ size
def dec(self, size):
return self.next() ^ size
def next_padding(self):
return self.next() % 64
def decrypt(arr, key, iv):
count = 0
parser = SizeParser(iv)
output = []
print(f"key = {key.hex()}")
print(f"iv = {iv.hex()}")
while len(arr) > 0:
padding = parser.next_padding()
L = parser.dec(bytes_to_long(arr[:2])) - padding
arr = arr[2:]
e_iv = p64(count, endian='big')[6:] + iv[2:12]
try:
dec = AES.new(key, AES.MODE_GCM, e_iv).decrypt_and_verify(arr[:L - 16], arr[L - 16:L])
output.append(dec)
except:
print("[!] Decryption failed")
finally:
count += 1
arr = arr[L + padding:]
return output
解密得到请求信息:
decrypt(data, data_key, data_iv)
key = 5e4a9aa9ba58c7e3ad36fe2499dca259
iv = 13277f5732da52ada790d87b8829daa9
GET /out HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: curl/7.75.0
Accept: */*
Connection: close
使用 wireshark 将响应保存为数据文件,以供解码时直接读取。首先解码指令部分,其密钥对为:
res_key = md5(data_key)
res_iv = md5(data_iv)
res = open('res.bytes','rb').read()
res_key = md5(data_key)
res_iv = md5(data_iv)
print(f"res_key = {res_key.hex()}")
print(f"res_iv = {res_iv.hex()}")
res_aes = lambda: AES.new(res_key, AES.MODE_CFB, res_iv, segment_size=128)
dec_res = res_aes().decrypt(res[:16])
print(f"v = {dec_res[0:1].hex()}")
print(f"opt = {dec_res[1:2].hex()}")
print(f"cmd = {dec_res[2:3].hex()}")
print(f"c_l = {dec_res[3:4].hex()}")
cmd_len = int(dec_res[3:4].hex(), 16)
print(f"cmd = {dec_res[4:4+cmd_len].hex()}")
data = res[4 + cmd_len:]
plaintext = decrypt(data, res_key, res_iv)
res_key = b22984cda4143a919b5b6de8121b6159
res_iv = fa2a8ab0fadb4854943df690335a99b5
v = 0xa2
opt = 0x00
cmd = 0x00
c_l = 0x00
cmd = ""
解密所得文件为一个 html 文件,其中以 base64 编码存放有一份宏病毒。因此这里取出其内容,实测电脑中的杀毒软件对此病毒十分敏感,一旦落入文件系统文件立刻会被损坏,最后为了查看内容直接存储为 zip 文件解压后查看。
from base64 import b64decode
data = "".join(i.decode() for i in plaintext)
start = data.find("atob('") + len("atob('")
end = data.find("');", start)
binary = b64decode(data[start:end])
check_sum = hashlib.sha256(binary).hexdigest()
open("doc.zip", "wb").write(binary)
得到此病毒的 sha256: 3a5648f7de99c4f87331c36983fc8adcd667743569a19c8dafdd5e8a33de154d
PS: 其实到这一步就已经可以把病毒查出来了,但是这里又去解包看了看内部的数据,最后使用其子 dll 部分的哈希值同样有效。
使用 foremost 工具抽出 _Ole10Native 文件中的 dll,并检查其哈希值:
$ sha256sum 00000000.dll
0d7aa23a72d22dcf47f8723c58d101b3b113cbc79dd407a6fac0e65d67076ea1 00000000.dll
在其行为分析中:
PID: 3528
Process: rundll32.exe
Method: GET
HTTP Code: 200
IP: 3.220.57.224:80
URL: http://api.ipify.org/
CN: US
于是使用 md5(b"api.ipify.org") = "08229f4052dde89671134f1784bed2d6"
作为解压密码,得到压缩包内文件。
Gob 文件可以使用 degob - Go library/tool for viewing and reversing Go gob data [Moved to GitLab] 进行反序列化。
编译后运行得到结果为三个字节数组,其中最长的为一个被打乱的 PNG 文件,结合所给时间和语言,推测是 Go 语言的 shuffle。
对于被打乱的字节数组,恢复其只需要记录一个序列数组被打乱后的值就可以进行反推,于是借助 copilot 写了段 Go:
package main
import (
"fmt"
"math/rand"
"time"
"os"
)
func main() {
index: = make([]int, 70475)
for i: = 0; i < 70475; i++ {
index[i] = i
}
t: = time.Date(2022, 7, 19, 14 - 8, 49, 56, 0, time.UTC)
rand.Seed(t.Unix())
rand.Shuffle(len(index), func(i, j int) {
index[i], index[j] = index[j], index[i]
})
f, err: = os.OpenFile("flag.index", os.O_CREATE, 0666)
if err != nil {
fmt.Println(err)
return
}
for _, i: = range index {
fmt.Fprintf(f, "%d,", i)
}
}
而后进行逆向恢复处理:
png_data = bytes(eval(open("flag.bs", "r").read()))
shuffled_idx = eval(f"[{open('flag.index','r').read()}]")
res = [0 for _ in range(len(png_data))]
for i in range(len(png_data)):
res[shuffled_idx[i]] = png_data[i]
ans = bytes(res)
open("flag.png", "wb").write(ans)
图片这里就不放了,使用多方工具检查,只觉得似乎透明度部分有些可疑,抱着试一试的心态提取了像素:
from PIL import Image
import numpy as np
img = Image.open("flag.png")
arr = np.array(img)
ans = bytes(arr[:,:,3].reshape(2000 * 973)).replace(b'\xff',b'').replace(b'\x00',b'')
print(ans)
得到:
b'flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}flag{898161df-fabf-4757-82b6-ffe407c69475}...
也即最后的 flag 为:
flag{898161df-fabf-4757-82b6-ffe407c69475}
后续:据说是 r3 队里的空白出的题,等有机会一定和他认识认识.jpg
more?
基本就是这样了,misc 手也就做了个 misc 题,最近忙着写新的 CTF 比赛平台,进度一直在推。不过这两年基本上没钻什么特别的方向,不过毕竟玩 CTF 也是为了玩嘛……希望校赛能够顺利举办,让我认识更多志同道合的朋友。
先这样了,面对缺少逆向和web手的现状还是要操心怎么培养培养。