狗儿

热爱的话就坚持吧~

0%

华为xctf第一场

逆向太难了 我太菜了

MISC

who moved my flag

给出一个流量包,看了下,可分为两段。

第一段是172.17.0.25访问172.17.0.1上使用python开启的httpSimple服务,下载了一个名为tshd的文件。tshd是tiny shell tsd的服务端,可用于加密流量的传输。

第二段就是tshd与tsh通信的过程。172.17.0.24和172.17.0.25

项目开源在creaktive/tsh

项目基本框架如下:

tsh.c和tshd.c分别是客户端和服务端,调用pel.c的pel_send_msg和pel_recv_msg来收发数据。

pel.c是封包加密层,但应该不是作者写的,而是Christophe Devine devine@cr0.net。搜了下,这老哥写过很多密码学的轮子。但是没有社工到具体的关于pel.c这个框架的解释,没办法,只能自己去读源码了。

然后归纳下通信流程:

客户端tsh主动生成两个20Byte的IV,并向服务端tshd发送长为40Byte的Packet.

tshd接收两个IV之后,双方建立加密通信(密钥是secret,提前写死在tsh和tshd中)

此后的第一对packet,是挑战与应答。tsh发送一个使用IV1加密后的数据,tshd必须响应使用IV2加密后的数据。如果tsh解密出原有信息,则通过challenge。双方正式进行通信。

通信时,packet结构为:

头部的两个字节是message的长度,其后是message,二者组装后填充0向上取整到16的倍数字节,aes-128加密得到密文。然后计算密文的hmac,共20Byte,追加在packet的后面。

收发时,要动态更新上一个密文块的值,并与当前解密结果异或(CBC模式)

tsh和tshd支持三种后门,get_file, put_file, run_shell.

非空通信流量共7条,前三条是传输iv和应答挑战。

image-20201221084432695

计算aes密钥的相关代码:

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
/* this routine computes the AES & HMAC session keys 这个例程计算AES和HMAC会话密钥*/

void pel_setup_context( struct pel_context *pel_ctx,
char *key, unsigned char IV[20] )
{
int i;
struct sha1_context sha1_ctx;

sha1_starts( &sha1_ctx );
sha1_update( &sha1_ctx, (uint8 *) key, strlen( key ) );
sha1_update( &sha1_ctx, IV, 20 );
sha1_finish( &sha1_ctx, buffer ); // sha1(secret + IV)

aes_set_key( &pel_ctx->SK, buffer, 128 ); // sha1(secret + IV)[:16]作为aes密钥

memcpy( pel_ctx->LCT, IV, 16 );

memset( pel_ctx->k_ipad, 0x36, 64 ); // 以下是计算hmac,此题只需要解密流量,所以我们不需要关注用于数据校验的hmac
memset( pel_ctx->k_opad, 0x5C, 64 );

for( i = 0; i < 20; i++ )
{
pel_ctx->k_ipad[i] ^= buffer[i];
pel_ctx->k_opad[i] ^= buffer[i];
}

pel_ctx->p_cntr = 0; // 封包序号
}

secret可从流量包中提取出来的tshd中找到(可以自己编译一份带符号的tshd,对比着可以很快找到secret.

解密脚本如下:

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
from scapy.all import rdpcap
import socket
import time
import struct
from Crypto.Cipher import AES


def get_sha1(b):
import hashlib
return hashlib.sha1(b).digest()

def get_session_key(secret, iv):
return get_sha1(secret + iv)[:16]

class PrpCrypt(object):
def __init__(self, key, iv):
assert len(key) == 16
assert len(iv) == 16
self.key = key
self.iv = iv
self.mode = AES.MODE_CBC

def encrypt(self, data):
cryptor = AES.new(self.key, self.mode, self.iv)
if len(data) % 16 != 0:
data += b'\x00' * (16 - len(data) % 16)
cipher = cryptor.encrypt(data)
return cipher

def decrypt(self, data):
#assert len(data) % 16 == 0
data = data[:len(data)//16*16]
cryptor = AES.new(self.key, self.mode, self.iv)
plain = cryptor.decrypt(data)
return plain


packets = rdpcap("flow.pcap")

from_24 = []
sport=8888
src="172.17.0.24"
for packet in packets:
if "TCP" in packet and packet['TCP'].payload:
if packet["IP"].src==src and packet["TCP"].sport==sport:
from_24.append(packet['TCP'].payload.load)

to_24 = []
sport=55794
src="172.17.0.25"
for packet in packets:
if "TCP" in packet and packet['TCP'].payload:
if packet["IP"].src==src and packet["TCP"].sport==sport:
to_24.append(packet['TCP'].payload.load)


send_iv, recv_iv = from_24[0][20:], from_24[0][:20]
secret = b'6.3.0-18+deb9u1'
#print(send_iv)
#print(recv_iv)

send_aes_key = get_session_key(secret, send_iv)
recv_aes_key = get_session_key(secret, recv_iv)

cryp = PrpCrypt(key = send_aes_key, iv = send_iv[:16])
plain = cryp.decrypt(to_24[2][:16*5] + to_24[2][16*5+20:16*5+20+16] + to_24[2][16*5+20+16+20:])
print(plain)
#plain2 = cryp.decrypt(to_24[2][16*5+20:-20])
#print(plain2)

需要注意的是,最后一条流量是个粘包。

当时我iv没找对,密钥搞对了。但是由于是cbc模式,所以只有第一个密文块会求不对,后面的理论上还是可以求的。但是第一个密文块求不对,就不知道第一个包的长度(前两字节),就没法自动剔除20Byte的hmac。但是由于是比赛,所以怎么省事怎么来,所以我直接尝试多次,手动分包,还好,分到第三个包的时候,就看到flag了。

image-20201221085123755

2021.1.5的补充

刚考完网安,闲得没事干,所以又把这题翻出来分析了下,写出了下面的脚本,正确处理了iv和分包。

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
from scapy.all import rdpcap
import socket
import time
import struct
from Crypto.Cipher import AES


# 只加密一个块
def aes_ecb_decrypt(data, key):
assert len(data) == 16
assert len(key) == 16
cryptor = AES.new(key, AES.MODE_ECB)
plain = cryptor.decrypt(data)
return plain

# 只加密一个块,return (明文,LCT)
def aes_cbc_decrypt(data, key, iv):
assert len(data) == 16
assert len(key) == 16
assert len(iv) == 16
LCT = iv
plain = aes_ecb_decrypt(data, key)
new_plain = b''
for i in range(len(plain)):
new_plain += struct.pack('B', plain[i] ^ LCT[i])
return new_plain, data


def get_sha1(b):
import hashlib
return hashlib.sha1(b).digest()

def get_session_key(secret, iv):
return get_sha1(secret + iv)[:16]



packets = rdpcap(r"D:\桌面\flow.pcap")

from_24 = []
sport=8888
src="172.17.0.24"
for packet in packets:
if "TCP" in packet and packet['TCP'].payload:
if packet["IP"].src==src and packet["TCP"].sport==sport:
from_24.append(packet['TCP'].payload.load)

to_24 = []
sport=55794
src="172.17.0.25"
for packet in packets:
if "TCP" in packet and packet['TCP'].payload:
if packet["IP"].src==src and packet["TCP"].sport==sport:
to_24.append(packet['TCP'].payload.load)


send_iv, recv_iv = from_24[0][20:], from_24[0][:20]
secret = b'6.3.0-18+deb9u1'

send_aes_key = get_session_key(secret, send_iv)
recv_aes_key = get_session_key(secret, recv_iv)


print('客户端tsh send:')

data = b''
for i in from_24[1:]:
data += i

key = recv_aes_key
LCT = recv_iv[:16]
index = 0
while index < len(data):
first_block, _ = aes_cbc_decrypt(data[index : index + 16], key, LCT)
msg_len = (first_block[0]<<8) + first_block[1]
blk_len = ((msg_len + 2 + 16) // 16 if (msg_len + 2) % 16 != 0 else (msg_len + 2) // 16) * 16
msg_data = data[index : index + blk_len]
msg = b''
for i in range(blk_len//16):
_msg, LCT = aes_cbc_decrypt(msg_data[i*16 : i*16+16], key, LCT)
msg += _msg
msg = msg[2:msg_len+2]
print('msg_len = %d\tblk_len = %d\tmsg = %s' % (msg_len, blk_len, msg))
index += blk_len + 20


print('\n服务端tshd send:')
data = b''
for i in to_24:
data += i

key = send_aes_key
LCT = send_iv[:16]
index = 0
while index < len(data):
first_block, _ = aes_cbc_decrypt(data[index : index + 16], key, LCT)
msg_len = (first_block[0]<<8) + first_block[1]
blk_len = ((msg_len + 2 + 16) // 16 if (msg_len + 2) % 16 != 0 else (msg_len + 2) // 16) * 16
msg_data = data[index : index + blk_len]
msg = b''
for i in range(blk_len//16):
_msg, LCT = aes_cbc_decrypt(msg_data[i*16 : i*16+16], key, LCT)
msg += _msg
msg = msg[2:msg_len+2]
print('msg_len = %d\tblk_len = %d\tmsg = %s' % (msg_len, blk_len, msg))
index += blk_len + 20

输出为:

image-20210106022234652

第一条封包看起来是乱码,其实它是挑战响应机制的那十六字节,详见源项目中的:

image-20210105222445023

over~