The_Bourne_Identity

主要涉及到强网杯中谍影重重一系列题目的复现。中间涉及的特殊协议以及协议参数拆分还挺有意思的。

前置准备

安装相关

用到的命令解释

  • tshark -r <capture_file> -Y "<display_filter>" -T fields -e <field_name>
    • -r : 指定要读取的抓包文件(例如,.pcap, .pcapng)。
    • -Y ““: 使用 Wireshark 的显示过滤器来选择特定的 TCP 数据包。
    • -T fields: 指定输出格式为 “fields”,即只输出指定的字段。
    • -e : 指定要提取的字段名称。 tcp.payload 或 tcp.data 可以用来提取 TCP payload。

谍影重重1.0

强网杯 2022 Writeup | GZTime’s Blog

2022年第六届“强网杯”网络安全大赛部分writeup (ehangwang.cn)

  • Route.pcapng
  • config.json
  • Amazing.zip

Amazing.zip为加密压缩包,加密文件为flag,基本判断通过其他两个文件得到压缩包的解压密码。打开压缩包也得到了明确的提示。

vmess协议

[协议细节 - VMess 协议 - 《Project V(V2RAY)文档手册》 - 书栈网 · BookStack](https://www.bookstack.cn/read/V2RAY/developer-protocols-vmess.md#VMess 协议)

v2fly/v2ray-core: A platform for building proxies to bypass network restrictions. (github.com)

主要就是看手册分析协议内容,根据协议定义进行计算。

时刻注意调用函数的格式,是字符串还是字节!本题基本都是字节。

协议初始定义

计算cmd_key以及cmd_iv

根据协议,先把指令部分求解出来。

我们的用户ID在config.json中体现。

1
2
3
4
5
6
7
"settings": {
"clients": [
{
"id": "b831381d-6324-4d53-ad4f-8cda48b30811"
}
]
}

使用匿名函数lambda把一些本题的常用函数给定义一下,简化调用步骤。

1
2
md5 = lambda x: hashlib.md5(x).hexdigest()
vmess_hmac = lambda x: hmac.new(client_id, x, hashlib.md5).hexdigest()

可以从协议定义来看,关键点在M之上。所以我们需要利用hmac值,爆破出关键的时间点M

但这一步莫名其妙一直没成功,无法爆出需要的时间,所以还有待改进中。不是python版本问题。

根据协议提示,写获取M的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 理论上
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)
# 硬凑版
def get_cmd_iv(time, target_hash):
for t in range(time - 50, time + 50):
cur_hash = vmess_hmac(p64(t))
# print(t)
# print(cur_hash)
if t == 1615528982:
print(f'time = {t}')
return md5(p64(t, endian='big') * 4)

p64()在pwntools库中,需要进行安装。

前置条件,需要找到进行响应的数据包基本条件:时间较早;数据量较大;存在数据。

大概的时间范围,以该流量包中的时间进行时间戳的转换。

找到参照时间戳为1615528962

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
import hmac
import uuid
from pwn import *

client_id = uuid.UUID('b831381d-6324-4d53-ad4f-8cda48b30811').bytes

md5 = lambda x: hashlib.md5(x).hexdigest()
vmess_hmac = lambda x: hmac.new(client_id, x, hashlib.md5).hexdigest()

req = bytes.fromhex('4dd11f9b04f2b562b9db539d939f1d52' + 'b48b35bf592c09b21545392f73f6cef91143786464578c1c361aa72f638cd0135f25343555f509aef6c74cd2a2b86ee0a9eb3b93a81a541def4763cc54f91ba02681add1b815e8c50e028c76bde0ee8a9593db88d901066305a51a9586a9e377ee100e7d4d33fcfc0453c86b1998a95275cd9368a68820c2a6a540b6386c146ea7579cfe87b2e459856772efdcf0e4c6ab0f11d018a15561cf409cbc00491d7f4d22b7c486a76a5f2f25fbef503551a0aeb90ad9dd246a9cc5e0d0c0b751eb7b54b0abbfef198b1c4e5e755077469c318f20f3e418af03540811ab5c1ea780c886ea2c903b458a26')
# print(req)
cut_time = 1615528962
target_hash = req[:16].hex()
# print(target_hash)

def get_cmd_iv(time, target_hash):
for t in range(time - 50, time + 50):
cur_hash = vmess_hmac(p64(t))
# print(t)
# print(cur_hash)
if t == 1615528982:
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(type(cmd_iv))
# print(type(cmd_key))

print(f"cmd_key = {cmd_key}")
print(f"cmd_iv = {cmd_iv}")

# time = 1615528982
# cmd_key = "b50d916ac0cec067981af8e5f38a758f"
# cmd_iv = "881eb47d4d3b67b24328c5178c0eedcc"

对指令部分进行解密

根据协议手册,我们了解指令部分各字节的所属以及作用。需要注意的是,我们在此解密的数据是指令数据。

同样的,先利用匿名函数定义所要使用的AES-128-CFB

1
cmd_aes = lambda: AES.new(bytes.fromhex(cmd_key), AES.MODE_CFB, bytes.fromhex(cmd_iv), segment_size=128)

这边得高亮一下,python3中对称密码的使用非常麻烦,因为主要处理的数据都是字节形式,此处注意,不能直接使用b’’来进行转换,会导致长度错误,必须使用bytes.fromhex()进行转换。

AES-128的密钥长度必须为16字节,而b’’转换成字节是32字节,所以会导致报错。

校验F部分,使用FNV1a hash

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
from fnvhash import fnv1a_32
from Crypto.Cipher import AES
from Crypto.Util.number import *
cmd_aes = lambda: AES.new(bytes.fromhex(cmd_key), AES.MODE_CFB, bytes.fromhex(cmd_iv), segment_size=128)

cmd = req[16:]
ret = cmd_aes().decrypt(cmd)
print(f"ver = {ret[0:1].hex()}")
print(f"dat_iv = {ret[1:17].hex()}")
print(f"dat_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]))}")

输出得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ver      = 01
dat_iv = 13277f5732da52ada790d87b8829daa9
dat_key = 5e4a9aa9ba58c7e3ad36fe2499dca259
v = a2
opt = 1101
p = 6
sec = 3

cmd = 01
port = 5000
type = 01

host = 127.0.0.1
rand = 1ace7d9bb0b5
F = 0x39182c03
check = 0x39182c03

根据协议协定信息进行解密

切分后得到的协议信息,比较关键的部分主要在dat_ivdat_keyopt以及sec中。

但通过文档发现Opt略显不对,主要因为文档年久失修:)

v2ray-core/headers.go at 5dffca84234a74da9e8174f1e0b0af3dfb2a58ce · v2ray/v2ray-core (github.com)

所以Opt部分,主要开启了GlobalPaddingChunkMasking以及ChunkStream,所以我们得到信息,元数据开启了数据混淆,所以我们客户端和服务端分别需要构造两个Shake实例。并且解密的时候注意Padding

sec部分,也不出意外的年久失修了:)

https://github.com/v2ray/v2ray-core/blob/5dffca84234a74da9e8174f1e0b0af3dfb2a58ce/common/protocol/headers.proto

sec=3的情况下,我们应该选择AES-128-GCM进行解密。

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
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

print('------------------------------------------------')
def decrypt(arr, key, iv):
count = 0
parser = SizeParser(iv)
output = []
print(f"dat_iv = {key.hex()}")
print(f"dat_key = {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

data = cmd[45 + p + 4:]
data_iv = ret[1:17]
data_key = ret[17:33]
# print(type(data_key))
pprint(decrypt(data, data_key, data_iv)[0].decode().split('\r\n'))

得到输出:

1
2
3
4
5
6
7
8
9
dat_iv   = 5e4a9aa9ba58c7e3ad36fe2499dca259
dat_key = 13277f5732da52ada790d87b8829daa9
['GET /out HTTP/1.1',
'Host: 127.0.0.1:5000',
'User-Agent: curl/7.75.0',
'Accept: */*',
'Connection: close',
'',
'']

将所有响应数据进行解密

插播,前面都是请求数据,本部分为响应数据。

响应数据依旧使用AES-128-CFB进行解密。

响应数据我们如何得到?直接利用Wireshark追踪数据流功能进行查看。先转换成原始数据,再把所有蓝色数据导出即可。保存为res.bytes

依旧根据协议定义对解密后的数据进行切分提取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
res = open('res.bytes', 'rb').read()
# print(res)
res_key = md5(data_key)
res_iv = md5(data_iv)
# print(res_key)
# print(res_iv)
print(f"res_key = {res_key}")
print(f"res_iv = {res_iv}")
res_aes = lambda: AES.new(bytes.fromhex(res_key), AES.MODE_CFB, bytes.fromhex(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, bytes.fromhex(res_key), bytes.fromhex(res_iv))

输出得到:

1
2
3
4
5
6
7
8
9
res_key  = b22984cda4143a919b5b6de8121b6159
res_iv = fa2a8ab0fadb4854943df690335a99b5
v = a2
opt = 00
cmd = 00
c_l = 00
cmd =
dat_iv = b22984cda4143a919b5b6de8121b6159
dat_key = fa2a8ab0fadb4854943df690335a99b5

解密之后所得文件

解密所得文件为一个 html 文件,其中以 base64 编码存放有一份宏病毒。因此这里取出其内容,实测电脑中的杀毒软件对此病毒十分敏感,一旦落入文件系统文件立刻会被损坏,最后为了查看内容直接存储为 zip 文件解压后查看。

1
2
3
4
5
6
7
8
9
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)

这一步,如此如此这般这般就得到html文件,并且为宏病毒文件。暂且缘由不太理解,先放着。

利用病毒sha256值反向查找

把zip文件直接放foremost分析一下,能够得到病毒的dll。

1
2
$ sha256sum 00000277.dll
0d7aa23a72d22dcf47f8723c58d101b3b113cbc79dd407a6fac0e65d67076ea1 00000277.dll

检索得到:Malware analysis extracted_at_0x22a7b.exe Malicious activity | ANY.RUN - Malware Sandbox Online

得到api的urlhttp://api.ipify.org

md5得到压缩密码为08229f4052dde89671134f1784bed2d6

得到flag文件。

go文件

得到的文件使用WINHEX打开一下,发现提示了文件类型,是Gob文件

利用属性来进行定义,从而反序列化。

导包的时候出现了missing path,找了报错原因,没解决,后来发现是因为格式错了。

1
2
3
4
5
6
import (
"fmt"
"math/rand"
"time"
"os"
)

然后脚本的原理暂且还不太理解,先放着。

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
package main

import (
"math/rand"
"os"
"time"
)

func main() {
//seed init
loc, _ := time.LoadLocation("Local")
timeObj, _ := time.ParseInLocation(
"2006-01-02 15:04:05",
"2022-07-19 14:49:56", loc)
seed := timeObj.Unix()
rand.Seed(seed)

input, _ := os.Open("./src.png")
in := make([]byte, 70475)
lenx, _ := input.Read(in)
table := make([]int, lenx)
out := make([]byte, lenx)
for i := 0; i < lenx; i++ {
table[i] = i
}

//shuffle
rand.Shuffle(len(table), func(i, j int) {
table[i], table[j] = table[j], table[i]
})

for i := 0; i < lenx; i++ {
out[table[i]] = in[i]
}
output, _ := os.Create("./flag.png")
output.Write(out)

}

得到图片。

图片隐写

搞出一张图片了,但没明白为啥图片的大小就是70450bytes,有点迷惑。

没找到合适的工具提取隐写,十有八九是提取像素点的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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'').decode()[:42]
print(ans)
# flag{898161df-fabf-4757-82b6-ffe407c69475}

# from PIL import Image
# pic = Image.open('flag.png')
# w, h = pic.size
# flag = []
# for i in range(h):
# for j in range(w):
# piexl = list(pic.getpixel((j, i)))[3]
# if(piexl != 0xff):
# flag.append(chr(int(piexl)))
# if len(flag) == 42:
# print(''.join(flag))

谍影重重2.0

  • 使用如下命令,把tcp包中的tcp的payload部分的data提取出来,并使用正则表达式进行数据简单清洗。

tshark -r attach.pcapng -T fields -e tcp.payload | sed '/^\s*$/d' > tcp_output.txt

  • 直接利用ADS-B的流量拆分,求解出最快速度飞机对应的ICAO
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
import pyModeS as pms
with open('tcp_output.txt', encoding='utf-8') as f:
data = f.read()
data = [s for s in data.splitlines() if s != '']
# print(data)
v_max = [0, '']
for d in data:
# print(pms.tell(d[18:]))
# print(len(d)) # 长度为46的时候可以解密出数据
if len(d) != 46:
continue
msg = d[18:]
icao = pms.icao(msg)
print(icao)
pms.tell(msg)
try:
v = pms.adsb.velocity(msg)
print(v)
if v[0] > v_max[0]:
v_max = [v[0], icao]
except:
pass
print('===============')
print(v_max)
# 提交的答案是ICAO对应大写形式的md5值。

谍影重重3.0

关键信息

  • Shadowsocks 采用流加密,sslocal 和 ssserver 各有 1 个加密流,其密文格式为 [IV][encrypted payload]。其中,AES 系列加密方式的 IV 长度均为 16 字节。

  • 当用户通过 Shadowsocks 进行 TCP 通信时,首先由 sslocal 发出请求,其明文格式为 [target address][payload]。其中,target address 和 SOCKS5 协议的请求部分很相似,但是只包含 ATYPDST.ADDRDST.PORT 这 3 个字段,即 [1-byte type][variable-length host][2-byte port]。ssserver 直接对来自目标服务器的响应进行流加密,发送给 sslocal。

  • UDP 通信的过程略有不同。和 VMess 这种纯 TCP 协议不同,Shadowsocks 的 UDP 和 TCP 是分别传输的,通过 TCP 传输 TCP,通过 UDP 传输 UDP。(PS. 如果用户用 docker 运行 Shadowsocks,一个常见的配置错误就是忘记进行 UDP 端口转发。如果 VPS 平台带防火墙,很多用户也会经常忘记放行 UDP 流量。)在报文方面 UDP 和 TCP 也有区别。Shadowsocks 的 UDP 响应头部会重复客户端的目标地址部分,即请求和响应格式均为 [target address][payload]

references

-------------THE END-------------