2026复现文档

2026复现文档

路虽远,行则将至;事虽难,做则必成

VNCTF2026

ez_iot

先看附件,一个bin和capture.raw,

file命令查看,可以发现bin为Xtensa架构的可执行文件。

image-20260207121049677

由于这是iot题目,而在iot背景中,Xtensa架构通常用于ESP芯片的固件,可以猜到,题目的逻辑被编译到了这个esp固件里面。

使用ghidra进行逆向分析:

app_main是esp32的入口函数:

image-20260210133000134

在app_main函数中,ESP-NOW通信的设置和初始化在esp_now_init()(行0xf3-0xfd),但实际的数据发送是在sender_task任务中完成的。这是典型的ESP32编程模式:在app_main中初始化,在任务中执行主逻辑。

分析sender_task函数:

image-20260210133535742

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

void sender_task(void *pvParameter)

{
int iVar1;
FILE *fp;
long lVar2;
size_t len;
int iVar3;
int iVar4;
undefined1 auStack_2c0 [4];
undefined1 uStack_2bc;
undefined1 uStack_2bb;
undefined1 uStack_2ba;
undefined1 uStack_2b9;
undefined1 uStack_2b8;
undefined1 uStack_2b7;
undefined1 uStack_2b6;
undefined1 uStack_2b5;
undefined1 auStack_2b4 [16];
undefined1 auStack_2a4 [222];
uint8_t auStack_1c6 [192];
uint8_t auStack_106 [208];
uint8_t auStack_36 [18];
size_t asStack_24 [9];

/* Unresolved local var: FILE * f@[???]
Unresolved local var: long file_size@[???]
Unresolved local var: uint32_t total_chunks@[???]
Unresolved local var: uint8_t[250] packet@[???]
Unresolved local var: uint8_t[192] chunk_data@[???]
Unresolved local var: uint8_t[208] encrypted@[???]
Unresolved local var: uint8_t[16] iv@[???]
Unresolved local var: uint32_t seq@[???] */
fp = fopen(&DAT_3c095278,&DAT_3c095274);
if (fp == (FILE *)0x0) {
esp_log_timestamp();
esp_log((esp_log_config_t)0x1,&DAT_3c09528c,&DAT_3c095298);
esp_log_timestamp();
esp_log((esp_log_config_t)0x1,&DAT_3c09528c,&DAT_3c0952c8);
vTaskDelete((TaskHandle_t)0x0);
return;
}
fseek(fp,0,2);
lVar2 = ftell(fp);
fseek(fp,0,0);
iVar1 = (lVar2 + 0xbf) / 0xc0;
esp_log_timestamp();
esp_log((esp_log_config_t)0x3,&DAT_3c09528c,&DAT_3c095314);
esp_log_timestamp();
esp_log((esp_log_config_t)0x3,&DAT_3c09528c,&DAT_3c095350);
esp_log_timestamp();
esp_log((esp_log_config_t)0x3,&DAT_3c09528c,&DAT_3c095378);
esp_log_timestamp();
esp_log((esp_log_config_t)0x3,&DAT_3c09528c,&DAT_3c09539c);
esp_log_timestamp();
esp_log((esp_log_config_t)0x3,&DAT_3c09528c,&DAT_3c095314);
iVar4 = 0;
do {
while( true ) {
/* Unresolved local var: size_t bytes_read@[???]
Unresolved local var: size_t encrypted_len@[???]
Unresolved local var: size_t packet_len@[???]
Unresolved local var: esp_err_t result@[???] */
while (len = fread(auStack_1c6,1,0xc0,fp), len == 0) {
esp_log_timestamp();
esp_log((esp_log_config_t)0x3,&DAT_3c09528c,&DAT_3c0953c0);
fseek(fp,0,0);
vTaskDelay(300);
iVar4 = 0;
}
esp_fill_random(auStack_36,0x10);
asStack_24[0] = 0;
iVar3 = aes_encrypt(auStack_1c6,len,auStack_36,auStack_106,asStack_24);
if (iVar3 == 0) break;
esp_log_timestamp();
esp_log((esp_log_config_t)0x1,&DAT_3c09528c,&DAT_3c0953f8);
}
memcpy(auStack_2c0,MAGIC,4);
uStack_2bc = (undefined1)iVar4;
uStack_2bb = (undefined1)((uint)iVar4 >> 8);
uStack_2ba = (undefined1)((uint)iVar4 >> 0x10);
uStack_2b9 = (undefined1)((uint)iVar4 >> 0x18);
uStack_2b8 = (undefined1)iVar1;
uStack_2b7 = (undefined1)((uint)iVar1 >> 8);
uStack_2b6 = (undefined1)((uint)iVar1 >> 0x10);
uStack_2b5 = (undefined1)((uint)iVar1 >> 0x18);
memcpy(auStack_2b4,auStack_36,0x10);
memcpy(auStack_2a4,auStack_106,asStack_24[0]);
iVar3 = esp_now_send(s_broadcast_mac,auStack_2c0,asStack_24[0] + 0x1c);
if (iVar3 == 0) {
esp_log_timestamp();
esp_log((esp_log_config_t)0x3,&DAT_3c09528c,&DAT_3c095428);
}
else {
esp_log_timestamp();
esp_err_to_name(iVar3);
esp_log((esp_log_config_t)0x1,&DAT_3c09528c,&DAT_3c095454);
}
iVar4 = iVar4 + 1;
vTaskDelay(10);
} while( true );
}


这是一个通过ESP-NOW发送加密文件的应用,也就可以知道,我们需要解密capture.raw文件

这里定义了数据包结构:

image-20260210133653602

分析aes_encrypt:

image-20260210133924040

可以得出这是AES-128-CBC加密

结合sender_task函数的fread来看,每次读取长度为0xc0(192=16x12),长度不够时PKCS#7 填充

image-20260210134225532

现在还差aes的key,但是ghrida中无法看到key的值:

image-20260210134343018

需要使用ida再次分析,拿到key

image-20260210134833115

结合espnow的数据帧格式

image-20260210134927302

选取其中一段数据进行解析:

image-20260210135037861

写出解密脚本,恢复图像:

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
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import struct

AES_KEY = b"uV9vG6mZ7mS8eC8b"
MAGIC = b"\xC7\xF0\x0D\x1E"
STATIC_HEADER = b"\xD0\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF"
INPUT_FILE = "capture.raw"
OUTPUT_FILE = "decrypted.png"

with open(INPUT_FILE, 'rb') as f:
raw_data = f.read()

frames = []
offset = 0
while True:
idx = raw_data.find(STATIC_HEADER, offset)
if idx == -1:
break
next_idx = raw_data.find(STATIC_HEADER, idx + 10)
if next_idx == -1:
next_idx = len(raw_data)
frames.append(raw_data[idx:next_idx])
offset = next_idx

chunks_dict = {}
total_chunks = 0

for frame in frames:
magic_idx = frame.find(MAGIC)
if magic_idx == -1:
continue
if magic_idx + 28 > len(frame):
continue

seq, total = struct.unpack('<II', frame[magic_idx+4:magic_idx+12])
iv = frame[magic_idx+12:magic_idx+28]
total_chunks = total

if len(frame) < magic_idx + 28 + 4:
continue
encrypted = frame[magic_idx+28:-4]

if len(encrypted) % 16 != 0:
continue

cipher = AES.new(AES_KEY, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(encrypted)
chunks_dict[seq] = decrypted

chunks = sorted(chunks_dict.items())
with open(OUTPUT_FILE, 'wb') as f:
for seq, data in chunks:
if seq == total_chunks - 1:
try:
data = unpad(data, 16)
except:
pass
f.write(data)

image-20260210135257958