2026复现文档

2026复现文档

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

DesCTF 2026

张三的秘密

使用FTK imager image mount…

image-20260313162113840

挂载成功:

image-20260313162144825

先浏览一般主要的文件夹:

Users\张三\Pictures\Screenshots有两张批注,

其中一个没有内容,左上角有一个fakeflag:

image-20260313162327955

image-20260313162341173

第二张发现了hint,结合题目信息,知道了这是Shamir’s Secret Sharing,需要五个密钥,在上图中已经发现两个。

继续查找,桌面上有一张二维码,扫描得到第三个key:

image-20260313162542804

image-20260313162650642

第四个key是在桌面的壁纸中,需要找到壁纸文件夹,在png的末尾找到key:

\Users\张三\AppData\Roaming\Microsoft\Windows\Themes\CachedFiles

image-20260313163042692

image-20260313163100520

第五个暂时不知道如何出的…

又是羡慕火眼的一天

楚慧杯2026

Time_and_chaos

给了1-8个png,flag.png,flag.txt。

可以发现的是这些png有很多的噪点,不过可以隐约看到右上角有内容:

image-20260313123431558

那么我们要做的其实是对图片进行降噪处理。

可以利用多张相似图像的统计特性进行图像降噪和恢复,通过合并多张带噪图像,使用统计方法去除噪声,最终恢复出清晰的flag图像:

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
import os
from pathlib import Path
import numpy as np
from PIL import Image


def load_images(paths):
"""读取图片并转换为 numpy 数组"""
return np.stack([
np.asarray(Image.open(p).convert("RGB"), dtype=np.float32)
for p in paths
])


def denoise(images):
"""
使用 median + mean 混合降噪
median 去除离群噪声
mean 平滑随机噪声
"""
median_img = np.median(images, axis=0)
mean_img = np.mean(images, axis=0)

# 混合权重
result = 0.7 * median_img + 0.3 * mean_img

return np.clip(result, 0, 255).astype(np.uint8)


def save_image(array, path):
Image.fromarray(array).save(path)


def recover(paths, output):
print(f"[+] 载入 {len(paths)} 张图片")

images = load_images(paths)

print("[+] 开始降噪处理")
clean = denoise(images)

print("[+] 水平翻转图像")
clean = np.fliplr(clean)

save_image(clean, output)

print(f"[✓] 完成 -> {output}")


if __name__ == "__main__":

candidates = [f"{i}.png" for i in range(1, 9)] + ["flag.png"]

paths = [p for p in candidates if Path(p).exists()]

if not paths:
print("[-] 未找到任何图片")
exit()

recover(paths, "recovered_flag.png")

得到恢复图像:

image-20260313123717593

可以看到一部分flag: DASCTF{Logistic_and
下一部分flag显然是flag.txt了,可以很明显看出零宽字符的隐写特征:

image-20260313123844845

image-20260313123903838

得到第二部分flag

SAM_and_Steg

给了sam和system文件。

使用samdump2提取hash:

image-20260313125001557

476b4dddbbffde29e739b618580adb1e

使用hashcat爆破:

image-20260313125300202

得到密码:!checkerboard1

binwalk检测system可以发现文件尾部嵌入了一张jpg图片:

image-20260313125618696

提取图片:

image-20260313125810153

image-20260313125822454

使用密码解密,可以得到一个aes文件:

image-20260313125927952

根据图片上的提示知道是 openssl,进行解密:

密码在sam的尾部:

image-20260313130716445

openssl enc -d -aes-256-cbc -md sha256 -k p@s4w0rd -in AES256 -out 1.gztar -xvf 1.gz

DASCTF{aa28f51d-0f54-4286-af3c-86a14fbab4a4}

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