2026DesCTF_wp(Misc部分)

DesCTF的misc部分wp

Real sign in?

根据题意,我们需要找到secret之后去找天命团队的公众号交流得到flag(像公众号推广题目)

首先给了一个challenge.zip的压缩包,在解开它之前,我们先进行binwalk查看

发现文件最后存在有7-zip数据,我们可以将文件放入Winhex搜索是否存在7z文件文件头

1
7z文件头:37 7A BC AF 27 1C

发现确实是在zip文件后存在多余7zip数据,提取出来,命名为7z文件

打开发现有一个markdown文件

可以看到这是一个矩阵的ZigZag扫描重排的操作

这边第一个矩阵在通过标准的之字形扫描顺序之后变成了一维序列,之后行优先排为了第二个44矩阵

1
(0,0)→(0,1)→(1,0)→(2,0)→(1,1)→(0,2)→(0,3)→(1,2)→(2,1)→(3,0)→(3,1)→(2,2)→(1,3)→(2,3)→(3,2)→(3,3)

这种Zigzag一般用在图像或者视频编码,我们继续往下看,打开压缩包看看

很好,里边是一个png文件,这很有可能是一张zigzag处理之后的,不急,我们至少得先打开这个

可以看到很关键的点在于这边的加密方式是ZipCrypto,压缩方式是Store

这意味着这个加密的png文件可以被明文爆破出来,由于png的前十六字节基本上都是一样的,我们可以由此进行爆破

1
2
3
89504e470d0a1a0a  #PNG文件头
0000000d #文件数据块IHDR长度
49484452 #IHDR标识

由此我们可以直接在这边进行明文爆破,利用bkcrack即可

1
bkcrack -C "challenge.zip" -c challenge.png -x 0 89504e470d0a1a0a0000000d49484452

成功爆破得到key

1
bkcrack -C 1.zip -k 5eb34ede c49019bf 815834b9 -U new.zip easy

成功解锁

打开是这样子一张扭曲的照片,根据前边的分析,这是被zigzag变化之后的,它的像素值已经按照Zigzag顺序被展开成了一维数组,我们需要将其重组为二维

我们知道了原始图片是通过DCT变化得到的系数矩阵,而后经过Zigzag扫描变成了一维序列

所以逆向的话就是按照Zigzag顺序填充到矩阵,然后逆DCT变化就好了

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

def zigzag_indices(h, w):
"""
生成 h x w 矩阵的二维 zigzag 顺序坐标
"""
result = []
for s in range(h + w - 1):
diag = []
r_start = max(0, s - (w - 1))
r_end = min(h - 1, s)
for r in range(r_start, r_end + 1):
c = s - r
diag.append((r, c))

# 偶数对角线反向,奇数对角线正向
if s % 2 == 0:
diag.reverse()

result.extend(diag)
return result

def inverse_whole_image_zigzag(img_path, out_path):
img = Image.open(img_path).convert("L")
arr = np.array(img)
h, w = arr.shape

flat = arr.flatten()
coords = zigzag_indices(h, w)

restored = np.zeros((h, w), dtype=np.uint8)

for i, (r, c) in enumerate(coords):
restored[r, c] = flat[i]

Image.fromarray(restored).save(out_path)
return restored

if __name__ == "__main__":
restored = inverse_whole_image_zigzag("challenge.png", "whole_invzig.png")
print("done")

运行后成功得到二维码

扫码得到内容为I_love_DesCTF

发给公众号就好了

Neural Secrets

可以看到这边给了一个pth文件,也就是PyTorch Model File,这是一个典型的pytorch模型的打包文件

本质上说这就是一个python的pickle文件,通常使用zip进行压缩打包,主要装模型参数和模型对象

阅读一下题目,题目说了”他一直在利用神经网络模型作为‘死信箱’来传递哈机密”

根据题目,很有可能这题是静态分析的,我们需要查看结构字段等来找flag,由此我们很容易想到先去查看一下这一个pth文件的checkpoint

1
2
3
4
5
import torch
checkpoint = torch.load('model.pth', map_location='cpu')
print("Checkpoint keys:", checkpoint.keys())

# Checkpoint keys: dict_keys(['model_state_dict', 'optimizer_state_dict', 'epoch', 'train_loss', 'val_loss', 'best_val_loss', 'vocab', 'model_config', 'eval_cache'])

可以看到输出了很多字段,在这些字段里大都常见,只有eval_cache不是特别标准的那一条字段,还叫缓存,很可疑

我们查看其属性,利用type可以直接确定位<class ‘torch.Tensor’>

1
2
3
4
5
6
7
import torch
checkpoint = torch.load('model.pth', map_location='cpu')

cache = checkpoint['eval_cache']
print(f"eval_cache 类型: {type(cache)}")
print(f"eval_cache 形状: {cache.shape}")
print(f"eval_cache 数据类型: {cache.dtype}")
1
2
3
eval_cache 类型: <class 'torch.Tensor'>
eval_cache 形状: torch.Size([39, 64])
eval_cache 数据类型: torch.float32

暂时只能看到说形状是[39, 64],即它有39行每行64维,那我们再看看别的参数

继续查看关键参数,发现了model_state_dict有重要信息

1
2
3
4
5
6
7
import torch
ckpt = torch.load("model.pth", map_location="cpu")

state = ckpt["model_state_dict"]

for k, v in state.items():
print(k, tuple(v.shape))

发现了embedding.weight 形状是 [95, 64]

意思是一共有95个字符。每一个字符都有个64维的embedding向量

这和上边那一个eval_cache的形状似乎十分相近

不难推测出研究院应该是把文本里的每个字符都替换成了对应的embedding向量之后存进的eval_cache,所以我们只需要将其中的每一行重新拿回去比对找到原来的即可

1
2
3
4
5
6
result = []

for vec in cache:
dists = torch.norm(emb - vec.unsqueeze(0), dim=1)
idx = torch.argmin(dists).item()
result.append(idx2char[idx])

写得解题脚本

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

ckpt = torch.load("model.pth", map_location="cpu")

state = ckpt["model_state_dict"]
emb = state["embedding.weight"] # [95, 64]
cache = ckpt["eval_cache"] # [39, 64]
vocab = ckpt["vocab"]


if isinstance(vocab, list):
idx2char = {i: ch for i, ch in enumerate(vocab)}
elif isinstance(vocab, dict):
if all(isinstance(k, int) for k in vocab.keys()):
idx2char = vocab
else:
idx2char = {v: k for k, v in vocab.items()}
else:
raise TypeError("unknown vocab format")

result = []

for vec in cache:
# 计算与所有 embedding 向量的距离
dists = torch.norm(emb - vec.unsqueeze(0), dim=1)
idx = torch.argmin(dists).item()
result.append(idx2char[idx])

flag = "".join(result)
print(flag)

解得最后的flag为DesCTF{n3ur4l_st3g0_1n_emb3dd1ng_sp4c3}

infrared_code

根据题意,遥控操作中隐藏着某种暗号信息,我们打开文件看看

打开ir_challenge.txt,我们可以看到里边是很多的地址和命令,这个文件包含了捕获的红外信息数据,也即是NEC扩展协议,在pdf可以看到这台电视配的是 IR / EN2B36H 遥控器

仔细看看可以看到每个命令包含了一个命令字节,command的第一字节是按键码,对应着遥控器上按下的特定按钮,也就是给的那一个png图片

统计之后发现一共有1000条记录,15种按键码,其中15/16/17/18/19出现次数很低,很像是确认和方向键

结合上边给的这个png图片以及知道的FLAG开头,我们可以尝试将这些做出映射

1
2
3
4
5
15 -> OK
16 -> UP
17 -> DOWN
18 -> RIGHT
19 -> LEFT

之后再利用题目这个6*6键盘来模拟布局即可

1
2
3
4
5
6
A B C D E F
G H I J K L
M N O P Q R
S T U V W X
Y Z 1 2 3 4
5 6 7 8 9 0

方向移动按“环绕”处理,越界会从另一边回来,把 OK 时当前位置字符记下来

从9/0的位置起步,注意这边还有一个删除的按钮,第12次实际上是这个删除

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
import re
from collections import Counter

with open("ir_challenge.txt", "r", encoding="utf-8") as f:
data = f.read()

# 提取命令字节
cmds = [a for a, _ in re.findall(r"command:\s*([0-9A-F]{2})\s+([0-9A-F]{2})", data)]

# 低频导航按钮映射
mapping = {
"15": "OK",
"16": "U", # 向上
"17": "D", # 向下
"18": "R", # 向右
"19": "L", # 向左
}

nav = "".join(mapping[c] for c in cmds if c in mapping)
segments = nav.split("OK")
if segments and segments[-1] == "":
segments.pop()

grid = [
list("ABCDEF"),
list("GHIJKL"),
list("MNOPQR"),
list("STUVWX"),
list("YZ1234"),
list("567890"),
]

# 从底部右侧开始
r, c = 5, 4
out = []

for idx, seg in enumerate(segments, 1):
for ch in seg:
if ch == "U":
r = (r - 1) % 6 # 纵向环绕
elif ch == "D":
r = (r + 1) % 6
elif ch == "L":
c = max(0, c - 1) # 水平不环绕
elif ch == "R":
c = min(5, c + 1)

cur = grid[r][c]

# 第12次确认实际上是“删除”操作,删掉上一个字符
if idx == 12:
if out:
out.pop()
else:
out.append(cur)

result = "".join(out)
print(f"Flag: flag{{{result[4:].lower()}}}")

得到最后的文本为 FLAG1NFR4R3DISFUN

所以flag为:flag{1nfr4r3disfun}

wireshark

给了一个流量包,先翻译一下题目:

“流量显示正常的Modbus流量和“额外”的通信——所有这些都符合协议,规避了防火墙警报。攻击者了解该协议。”

意思很明确了,是个Modbus工控流量,攻击者应该是在这些正常协议里用的异常用法

常见的Modbus功能码是:

1
2
3
4
5
6
01 Read Coils
03 Read Holding Registers
04 Read Input Registers
05 Write Single Coil
06 Write Single Register
10 Write Multiple Registers

我们打开压缩包,先过滤tcp.port == 502

可以发现192.168.100.10像正常的主站

而192.168.100.101,192.168.100.102和192.168.100.103这几个像是PLC

跟着我们刚刚的思路,找找异常请求,发现大部分都是正常通信,只有功能码是8的不正常,过滤一下看看

1
modbus.func_code == 8

功能码8是Diagnostics,其中一大功能就是这边的Return Query Data

这个合法而且可以塞任意请求,很适合做数据隐藏

过滤之后我们继续探查,发现了很多ASCII内容

像LINK,PING,ECHO,有很多

直到中间有一部分发现不像刚刚那种的,长度固定的字符串,很像是密文块

1
2
3
4
5
6
ded7825ede4fd19c
9f37371c37c6fa2d
54e6fe2801f0df1d
763175a586db1c62
9efa82d0f8eacb41
7b4419392b4a6aa8

考虑到是八字节每个,怀疑是DES,而且既然有密文,很有可能有密钥

继续寻找密钥,猜测是8字节的key

不断找,还能找到假flag

一直到这里发现一个突兀的八字节字符串S7COMM01

发现成功解出flag

flag{a3f8e2d1-7c19-4a6b-b5e8-9d2f0c4a7e31}

张三的秘密


先看题目,题目说什么沙米尔,秘密

很有可能是让我们收集Shamir分片,但不急,继续往下看

附件给了一个E01文件,明显是一道取证的题目,我们可以先进行仿真

在仿真过程中可以跑一下取证软件,可以先看看张三浏览过的文件,发现了个很可疑的叫hint.dll的

仿真好了,这个壁纸左上角还有个假flag,或许壁纸也不一般,还是先找hint.dll

就在快速访问,更快了

发现还真是个hint啊,只要我们只要5个密钥就能解开秘密,应该也就是flag了,还给了我们一个

继续寻找,发现桌面有个二维码

一扫就获得了第二个shares

继续寻找,我们没思路的时候可以继续看看浏览记录

发现在看壁纸,看看壁纸

发现明看看不出啥

但是放入WinHex后找到了第三串shares,只差最后两个了

继续找,发现这个批注不简单

上边直接就写了第四条,至此我们得到了四条,也就是最简单的四条,就是第五条怎么找都找不到

于是我们选择了爆搜

根据题目提示搜索沙米尔,发现了一大堆的这样子的类似shares的字符串,至此我们就集齐了五条,解密即可

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
from itertools import permutations


# From the recovered hint.dll / screenshots.
# `p` is the Shamir field modulus.
P = int(
"666c61677b3431e120579912cdf6831aed2476b0f3fab7c37b86a5c7b847e226a97f72f45783",
16,
)

# These are the corrected values that appeared in the earlier recovery session.
# Two shares were copied from evidence with the same truncation that the prior solve used.
SHARES = [
int("4e769b2cb222e299d33ea4b89e2831e12399a6b0117336e981a567371726b3368c73f3488e18", 16),
int("47bb1ac5f6a422e8b4d483334b5d7fe2f8bae6ae665322ff30b2cade7f03e434a2e849d08599", 16),
int("3f961ff18045be09c0ef92b6a5813cdfe8dc365f613b130ed430095e657c8391a1c03ac5ace5", 16),
int("f0fe0b8be939ecd598f774ca043352f43dfb9fa3b74678aa9f9c9f68ab385f071d84376e64e", 16),
int("4a37c5fad9d060d12f2cf65650fdd718d18d7e0a777276e85e1dd70a4a3c5b842d1feb896c42", 16),
]


def lagrange_at_zero(xs, ys, mod):
total = 0
for i, xi in enumerate(xs):
num = 1
den = 1
for j, xj in enumerate(xs):
if i == j:
continue
num = (num * (-xj)) % mod
den = (den * (xi - xj)) % mod
total = (total + ys[i] * num * pow(den, -1, mod)) % mod
return total


def to_bytes(value):
width = max(1, (value.bit_length() + 7) // 8)
return value.to_bytes(width, "big")


def main():
hits = []
for xs in permutations(range(1, 10), len(SHARES)):
secret = lagrange_at_zero(xs, SHARES, P)
blob = to_bytes(secret)
if blob.startswith(b"flag{") and blob.endswith(b"}"):
hits.append((xs, blob.decode("ascii")))

if not hits:
print("No flag-looking result found.")
return

print("Candidate hits:")
for xs, flag in hits:
print(f"x mapping = {xs}")
print(flag)


if __name__ == "__main__":
main()

得到flag{37af8b400737929ad29ad6876d283e92}


2026DesCTF_wp(Misc部分)
https://mei-you-qian.github.io/2026/03/12/2026DesCTF_wp(Misc部分)/
作者
Meiyouqian
发布于
2026年3月12日
许可协议