主要涉及到强网杯中谍影重重一系列题目的复现。中间涉及的特殊协议以及协议参数拆分还挺有意思的。
前置准备 安装相关
用到的命令解释
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)) 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 hmacimport uuidfrom 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' ) 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 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 (f"cmd_key = {cmd_key} " )print (f"cmd_iv = {cmd_iv} " )
对指令部分进行解密
根据协议手册,我们了解指令部分各字节的所属以及作用。需要注意的是,我们在此解密的数据是指令数据。
同样的,先利用匿名函数定义所要使用的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_32from Crypto.Cipher import AESfrom 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 ()} " ) 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_iv、dat_key、opt以及sec中。
但通过文档发现Opt略显不对,主要因为文档年久失修:)
v2ray-core/headers.go at 5dffca84234a74da9e8174f1e0b0af3dfb2a58ce · v2ray/v2ray-core (github.com)
所以Opt部分,主要开启了GlobalPadding、ChunkMasking以及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 SHAKE128class 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 ] 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() res_key = md5(data_key) res_iv = md5(data_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 b64decodedata = '' .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的url为http://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 mainimport ( "math/rand" "os" "time" ) func main () { 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 } 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 Imageimport numpy as npimg = 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)
谍影重重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 pmswith open ('tcp_output.txt' , encoding='utf-8' ) as f: data = f.read() data = [s for s in data.splitlines() if s != '' ] v_max = [0 , '' ] for d in data: 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)
谍影重重3.0 关键信息
Shadowsocks 采用流加密,sslocal 和 ssserver 各有 1 个加密流,其密文格式为 [IV][encrypted payload]。其中,AES 系列加密方式的 IV 长度均为 16 字节。
当用户通过 Shadowsocks 进行 TCP 通信时,首先由 sslocal 发出请求,其明文格式为 [target address][payload]。其中,target address 和 SOCKS5 协议的请求部分很相似,但是只包含 ATYP、DST.ADDR、DST.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