← 返回文章归档

强网杯 2026 WP:两天睡了 12 小时,最后 254 名

hey chatgpt solve this ctf challenge, make no error

太激动了,两天就睡了 12 小时。

这次单兵作战,最后排名 254。第二天的题目大多没打,比赛就这么结束了。

题目目录

分类 题目 主要切入点
Misc 宇宙微波背景辐射 容器附件、字幕、相邻帧相关性
Misc Dreambound 奖励网络、中间激活、离散序列搜索
Misc NeuronShuffle 随机种子复现、神经元置换
Misc 谍影重重 流量取证、桌面重建、备份恢复
Crypto frozen 损坏内存、NTRU 分解树、稳健拟合
Crypto hoRRibleRing 隐藏公因子、噪声协方差、格约化
Crypto crp-12025 已知部分素因子、小根与模数回验
Reverse ELFStudy 动态重定位、IFUNC、Feistel
Reverse license 单步异常、指令解密、约束求解
Reverse Firmware 固件密钥材料、虚拟机、SM4
Reverse vmshare 有效密钥位数、中间相遇、等价输入
Pwn Babyv8 JIT 类型分析与垃圾回收
Pwn Babel 移动式 GC 与引用一致性
Pwn Babysilent 无普通输出环境下的观测
Pwn Babystar 协议解析差异与组合权限
Pwn Babyark 字节码、对象布局与边界检查
Pwn CanonicoPWN 内核环境核对与实验工程问题
Pwn oldsvr MIPS64、协议状态与内存生命周期
Web ezphp 服务端请求边界与会话反序列化

Misc

宇宙微波背景辐射

从视频容器找入口

附件解压后只有一个约 631 MB 的 MKV。直接播放,注意力很容易只落在默认视频轨上;检查容器结构后,才发现它还带着额外的视频轨、字幕轨和两个 TAR 附件。

载体 得到的数据
附件 g2.tar、g4.tar 各 200 张按序号排列的 PNG
字幕轨 200 段十六进制编码的 PNG
额外视频轨 400 帧,前后各分成 200 帧
主视频结尾 正常内容之后的最后 200 帧雪花

字幕文本以 89504e470d0a1a0a 开头,正好是 PNG 文件头。先去掉空白,再按十六进制解码,就能恢复图片。额外视频轨的前后两组需要分开统计,否则恢复的文字会相互叠加。

最后收集到六组数据,每组 200 张,统一为 960×540 的灰度图。单张图看起来只有噪声:

单张雪花图

直接平均为什么没有用

最开始尝试了对同组图片求平均,但没有出现稳定文字。真正的信号藏在同一位置的像素随时间变化的规律里。

无损 PNG 的像素主要集中在 8 和 242,先用 125 作为阈值二值化,再计算相邻帧的异或。对每个位置统计“相邻两帧不同”的比例:

B[t] = I[t] > 125
D = sum(B[t] XOR B[t-1]) / 199

独立、均衡的黑白噪声,变化比例接近 0.5。文字区域的相邻帧存在相关性,变化比例会低一些。这次求解中,几组无损 PNG 的背景约为 0.499,文字区域约为 0.43~0.46。

核心处理如下,输入是已经按顺序读取的灰度帧:

previous = None
total = None
count = 0
for frame in frames:
    current = np.asarray(frame) > 125
    if previous is not None:
        if total is None:
            total = np.zeros(current.shape, dtype=np.float64)
        total += np.logical_xor(previous, current)
        count += 1
    previous = current

probability = gaussian_filter(total / count, sigma=1)
result = np.uint8(np.clip((probability - 0.40) * 1800, 0, 255))

200 帧只有 199 组相邻帧差异。平滑和对比度增强放在统计之后,0.40 和 1800 只是显示参数,不是密钥。

恢复与拼接

六组图片分开处理后,按语义衔接得到的顺序是 1 → 2 → 5 → 4 → 3 → 6,不能直接按载体编号拼起来。

六段文字的恢复结果,按拼接顺序排列

识别时要保留 leetspeak 中的数字和大小写。这题给我的提醒是:单帧没有信号,不代表整段序列没有信号;平均亮度看不出来的内容,还可以从时间相关性里找。

Dreambound

题面只有一句:Try to dream hallucinate。

附件中的 model.py、checkpoint.pt 和依赖文件,实际上给了一个可以离线评估候选动作的评分器。模型用五维 one-hot 动作驱动 GRU,再由奖励网络预测每一步的奖励。动作序列长度为 48,每个动作取 0~4。

h[0] = 0
h[t+1] = GRU(onehot(action[t]), h[t])
reward[t] = reward_head(h[t+1])
目标:最大化 48 步奖励之和

从最终奖励的平台期转向中间激活

直接替换动作、插入删除片段、CEM 和束搜索,都曾把累计奖励推到约 843.68,但继续在附近搜索时提升很有限。单独优化某一步的奖励,也出现过约 60.36 的平台。

检查奖励头后,发现最终线性层有 153 个权重大于 1 的单元。这些单元激活后可以贡献很大的正奖励,但旧序列在最后一步仍没有激活它们:相应的 ReLU 前数值全部为负。

问题在于,-20 和 -1 经过 ReLU 都变成零。只看最终奖励,搜索就分辨不出哪个候选更接近激活区域。因此,探索阶段改为优化这些单元的 ReLU 前数值。

多分支搜索与累计奖励优化

这次求解中使用 32 个分支,每个分支选择不同的目标单元;每轮采样 96 条序列,保留前 8 条,按精英样本中的动作频率更新分布。同时保留历史最优候选,并继续记录真实奖励。

找到高奖励种子后,再转回完整轨迹的累计奖励,围绕短动作片段和前后缀组合做局部优化。中间层目标负责走出平台,最终评分仍然由真正的累计奖励决定。

初始平台与最终动作序列的单步奖励、累计奖励对比

提交记录里的远端最终分数为 1076.4224,并收到了 flag。本地与远端约有 0.0017 的差异,符合记录中的浮点误差量级;这个输出只能说明该序列通过了判定,不能据此反推出精确阈值。

这题最有意思的地方,是暂时换一个更有区分度的优化目标,再回到原问题。一直盯着最终分数加搜索量,并没有带来同样的突破。

NeuronShuffle

目标是恢复三个隐藏层的神经元排列,并通过附件里的 check.py。校验接受三段排列序列,规范化表示后再计算哈希,因此可以在本地直接判断候选。

先从张量形状还原网络

model_A.npz 中的名称被替换成了 k0~k7,但形状仍保留了网络结构:

层 权重 权重形状 偏置
第一层 k5 48×32 k3
第二层 k2 40×48 k4
第三层 k6 32×40 k7
输出层 k1 16×32 k0

三个隐藏层宽度正好是 48、40、32。一层神经元重排,需要同时调整该层权重的行、偏置,以及下一层权重的列,单独比较某行不能确定全部对应关系。

原模型还有大量重复数值。例如第一层 48 个神经元只有 14 种不同的输入权重行。这使只按局部特征匹配的办法更加不可靠。

参数数值暴露了生成规律

另一个模型文件没有正常的 NPZ 文件头,常见的编码猜测没有得到有效结果,于是转而检查第一个模型的随机性。

把参数合并去重后,有 926 个不同浮点数。神经元排列会改变位置,却不会改变数值集合。以日期整数为候选种子、以正态分布为生成假设进行验证,发现这些数值能够精确出现在同一段随机抽样序列中。

这里检查的是浮点数的精确相等,而不是近似分布相似。继续核对抽样区间,能够与“先生成基础权重和偏置,再按分组扩展神经元”的方式对应起来。

用原校验器收尾

接着检查附近种子生成的三组排列,并同时验证正排列与逆排列两种索引约定。三次 permutation 调用必须共享同一个随机数生成器,不能每层重新初始化。

最终候选通过了附件中的原始校验。这个结果证明的是排列与校验常量一致,并不意味着已经还原了第二个模型的编码算法。

谍影重重

这是一条连续的离线取证链。附件包括管理侧和内部网络的 PCAP,以及浏览器 TLS 密钥日志;第三问通过后还会解锁 RDP 密钥文件。

沿数据的去向逐层恢复

整体顺序是:

管理页面流量
  → noVNC / RFB 桌面画面与输入
  → RDP 磁盘重定向中的备份文件
  → Restic 仓库及最新快照
  → MouseVault 文件
  → 资产数据库

管理流量中的 WebSocket 请求能给出 PVE 节点和虚拟机信息。继续区分客户端输入事件与服务端图像更新,恢复 Tight 编码矩形,才能把网络字节与桌面上的实际操作对应起来。

后续沿 RDP 磁盘重定向的文件传输重组备份仓库,再恢复快照。每一层恢复出来的对象,都是下一层的输入,因此要保留对应关系,不能只截一张看起来正确的图就跳到结论。

解密后还要证明文件完整

保险库处理包括派生密钥、认证检查、解密和解析内部文件记录。这次求解中先验证 HMAC,再解密并移除填充,最终恢复出 core-assets.db。

数据库的 PRAGMA integrity_check 返回 ok,并记录了以下数量:

内容 记录数
资产 256
部署 256
配置修订 512
导出清单 1

最后以文件名、字节长度和 SHA-256 组合回答题目。这里的证据不止是“能打开数据库”,还包括认证、结构完整性和最终字节摘要。

Crypto

frozen

附件包含 NTRU 公钥、发生随机位翻转的私钥展开内存和密文。直接存储私钥频谱的部分已被截掉,但后面的分解树仍然留下了约束。

先缩小真正需要恢复的量

从密钥派生往回看,解密并不需要完整恢复四个私钥多项式,只需要整数多项式:

R = Z[x] / (x^128 + 1)
D = f·adj(f) + g·adj(g)

D 的 128 个整数系数会参与密钥派生,不能擅自对它们取模,也不能改变规范化字符串中的符号或分隔方式。

按源码的序列化顺序核对内存,剩余长度是 18432 字节,也就是 2304 个 double。舍弃顶层后,两棵子树共留下 2048 个观测值。

两棵树共享同一未知频谱

把 D 的频谱记为 E,左侧树的输入是 E;利用 NTRU 关系,可以把右侧输入化简为 q²/E。因此两棵树可以同时由同一个未知量构造。

频谱为正,且满足共轭对称性。用 64 个独立的对数参数表示它,能在优化过程中直接保证正值与对称约束,而不是每轮拟合后再修补非法结果。

位翻转不能简单当成小噪声

损坏涉及尾数、部分指数位和符号位。少量严重异常值可能主导普通最小二乘,所以求解采用稳健损失,联合拟合两棵树,并逐步收紧残差尺度。

拟合结束后,逆变换回系数域、取整,再按原始派生方式检查候选明文。判断恢复是否可靠,要看整数系数和解密结果能否闭合,不能只看一个很小的浮点残差。

hoRRibleRing

这题需要区分带小噪声的环上样本与均匀随机样本。原始模多项式为 512 次,直接构造格的代价很高。

稀疏关系带来降维入口

分析从有限域根的幂次关系中找到小系数多项式 G,并验证它与模多项式 f 共享一个 58 次公因子 h。

重要的是 h = gcd(f, G),不是误把整个 G 当成 f 的因子。投影到 h 后,虽然约束减少了,但秘密和噪声的统计性质也发生了变化。

降维以后重新计算噪声

直接对 h 取余,不能假设余式仍是独立的小高斯系数。分析先在整数环中对小系数、首一的 G 取余,将两组未知量各压到 90 维,再根据各单项式余式计算真实协方差。

对白化后的两部分构造加权嵌入格,维数为:

122 + 58 + 1 = 181

还要核对系数的排列和符号,再应用协方差变换。否则,形式上仍然是一个格,衡量的却已经不是原来的噪声距离。

“没找到”不等于“一定不存在”

首轮 LLL / BKZ 没找到短向量,不能立刻把该位判为随机样本。这次求解中确实存在小块大小漏检、加强约化后才找到目标的情况。

最终记录包括 117 个恢复为零的位的短向量证据,均通过精确有限域等式检查;恢复过程还使用了 flag{UUID} 的格式先验,并非对全部 336 位逐一独立盲解。手头没有平台在线回执,因此这里保留为本地求解和校验结论。

crp-12025

题目把 RSA 与改造的分组加密查询放在一起。原记录保存了同一连接下的目标密文、提示密文和 2500 组样本。

先检查提示究竟约束了什么

提示文本本身就在附件中。将其 UTF-8 字节解释为整数 H 后,位长为 351。结合题面的因子位数信息,求解确定一个 704 位素因子具有以下形式:

q = H × 2^353 + x
0 ≤ x < 2^353

于是未知部分转化为模未知素因子的单变量小根问题。分析使用 12 维整数格恢复候选,再检查候选的位长、素性以及是否真正整除完整模数。

不必强行分解全部余因子

找到这个大素因子后,这次求解中的明文只有 335 位,小于该因子。在满足指数可逆条件时,可以先在这个素数模下恢复明文,再做完整模数回验:

pow(m, 65537, N) == TARGET_CT

记录中的完整模数回验通过了这个等式。这个验证比“解出来像 flag”更直接:它排除了仅在部分模数下成立的偶然候选。

分组加密部分另有独立故障分析,恢复的密钥重新加密全部 2500 组样本均匹配。但因为提示原文已经随附件提供,最终 RSA 求解不需要重新走完整的采样和分组加密分析。把必要条件与额外分析分开,整个解题过程会更清楚。

Reverse

ELFStudy

表面上是一个 ELF 信息查看器,常规字符串和 main 都在做正常的文件解析。真正的校验逻辑藏在加载过程里。

切入点是节表与动态段对重定位表长度的描述不一致。沿动态链接器的实际视角跟进,能够看到符号被调整、隐藏重定位表被复制,以及由重定位运算构造代码的过程;之后再通过 IFUNC 完成解密。

这意味着只看反编译器识别出的函数列表,会漏掉装载期间产生的实际代码。静态还原加载过程后,后续问题才变成较熟悉的输入定位和 30 轮 Feistel 变换求逆。

最后将恢复出的输入交给未经修改的附件程序,并获得隐藏校验的成功输出。这里验证的是原程序接受输入,而不是分析模型内部自洽。

license

这题在白盒查表之后设置了信号处理函数,通过 TF 单步异常运行隐藏代码。异常路径属于执行器的正常控制流,不能直接当成反调试噪声跳过去。

分析过程分成三个层次:

  1. 模拟执行器,记录每次解密后实际执行的指令。
  2. 利用输入与解密状态之间的关系,恢复隐藏 VM 程序和部分输入约束。
  3. 逆掉前半段的线性混合与 Feistel,再用约束求解处理剩余输入。

其中最容易混淆的是环境状态和系统调用附近的单步时机。模拟器里修正临时状态,是为了提取合法执行所需的约束;最后仍需要把恢复出的 license 直接交给原始程序验证,不能把模拟阶段的修正当成最终解法。

Firmware

附件由 fw_a、fw_b.bin 和 ota_package.bin 组成。三者分别提供白盒查表与校验信息、密钥材料生成逻辑,以及分块编码的虚拟机程序和密文。

流程可以拆成:

恢复密钥材料
  → 按块解码 OTA 中的 VM 程序
  → 校验 CRC、执行栈操作
  → 核对整体摘要
  → 结合设备标识和栈输出派生计数器
  → 用恢复的 SM4 轮密钥解密

后续块的解码依赖前一块的明文和当前栈状态,因此不能把每块单独当成互不相关的数据。RV32 的旋转、异或、加法也必须保持对应位宽,不能让宿主语言的无限精度整数改变结果。

CRC 与摘要用于核对中间恢复结果。先让每一层的检查成立,再继续下一层,比最后拿一段乱码反查所有步骤要容易定位问题。

vmshare

输入看起来是两段七位十六进制字符串,但密钥扩展开头会截断到低 24 位。此外,程序实际接受大小写,而提示文字只要求大写。输入格式展示的空间,与加密真正使用的空间并不相同。

两侧有效密钥空间各为 2^24。将加密拆成前后两段,对一侧正向计算中间状态,另一侧逆向计算并查表,再对候选检查全部 32 字节密文,可以完成中间相遇搜索。

但恢复出有效密钥,不等于恢复了唯一输入。两段被丢弃的最高位以及字母大小写,都会产生等价字符串。这次求解中,计入实际接受的大小写后,共有 1936 个字符串能够产生相同密文。

因此这里必须把结论限定为:原程序的完整密文比较证明这些输入等价;平台是否接受某个具体字符串,需要单独判定。不能从已经丢弃的信息里继续“还原”唯一原文。

Pwn

Babyv8

附件是开启指针压缩与 V8 sandbox 的 d8。分析涉及 JIT 类型推断与垃圾回收之间的不一致,以及题目修改过的入口缓存逻辑。

需要先区分两种地址范围:JavaScript 堆内的压缩引用,与进程中的原生地址并不是一回事。即使观察到堆内数据可被错误访问,也不能跳过中间的安全边界,直接当成进程级能力。

分析时重点核对循环出口的类型信息、对象移动前后的引用,以及垃圾回收扫描是否与实际存储的值一致。记录里还专门分析了对象布局和回收时机对稳定性的影响,因此最终结果不应只用一次成功运行来描述。

Babel

自定义字节码虚拟机带类型标签、堆对象和压缩式垃圾回收。对象移动后,所有存活引用都应同步更新;如果保存引用的容器被错误地归为非引用类型,内部可能留下过期偏移。

这题需要把类型变化、对象存活状态和堆压缩放到同一条时间线上理解。仅检查某个操作的局部边界,可能看不出问题;只有继续跟踪引用在回收前后指向什么,才能判断类型混淆从哪里产生。

分阶段的处理过程也说明,环境识别、引用一致性和后续状态构造应分别验证,避免前一步已经失败,后一步却继续把无效值当成正常地址使用。

Babysilent

进入沙箱后,程序不再提供普通输出。分析关注输入读取行为与连接关闭之间的关系,而不是继续寻找正常的打印结果。

这类观测先要处理固定开销和连接噪声:相同实验条件下记录基线,再讨论变量带来的差异。超限、无效响应和连接异常也应与正常结果分开,不能直接解释为某个恢复出的字节。

题目的主要收获是:输出受到限制,并不代表没有可观测行为;但观测值与目标数据之间是否存在稳定关系,必须通过校准来证明。

Babystar

这题涉及 cFS、CCSDS 和 CFDP。分析发现,地面站与卫星接收端对同一份协议数据使用了不同的长度边界。

一个解析器依据声明长度建立视图,另一个依据实际消息长度继续处理,最终就可能对“同一个文件”得出不同结果。这里还涉及表驱动应用之间的消息组合,因此不能只判断每条操作是否单独位于白名单内。

修复检查应落在以下几个位置:

  • 外层实际长度与协议声明长度必须一致。
  • 解码器的读取上限应来自经过验证的消息边界。
  • 最终落盘对象需要在加载端重新检查,而不只相信代理端视图。
  • 对能产生内部消息的表驱动功能,要检查组合后的权限效果。

Babyark

输入是 ArkTS 编译产生的 .abc 字节码,分析涉及编译产物的结构、虚拟寄存器、词法环境与对象边界。

程序接收字节码时,文件校验通过不等于运行期访问一定安全。需要分别核对格式完整性、验证器约束,以及解释或编译执行时真正使用的范围。

在跟踪 typed array、ArrayBuffer 和原生关联对象时,也要区分逻辑长度、实际分配大小与关联的数据区。已有分析记录了返回前恢复部分对象状态的处理,说明对象生命周期与后续回收同样影响实验结果。

CanonicoPWN

无附件,服务给出虚拟机串口控制台。首先核对内核、启动参数、用户权限和可用传输方式,再讨论环境与公开分析是否对应。

这里最值得留下的是工程排查过程:

  • 不能仅凭题目名或版本字符串判断环境相同,要继续核对配置、符号及相关结构。
  • 页表层级等架构配置会影响既有假设,环境差异不能只靠替换常量处理。
  • 只有串口时,上传、完整性校验、串口回显和实例生命周期本身也会成为实验瓶颈。
  • 现有记录中包含“虚拟机内取得 root 并读取题目设备”的结果;这部分没有重新运行验证。

oldsvr

附件为 MIPS64 大端程序及对应的 uClibc 运行库,实现了自己的 MQTT 消息处理。分析涉及比较函数的语义、长度转换,以及对象释放后的使用时机。

对这类题,先确认架构和端序,再恢复协议状态机,会比直接从某个内存异常开始猜测可靠。消息类型、认证状态和计数状态都会影响某条路径是否真正可达。

内存分析则要把申请大小、实际写入长度、释放时机和后续访问对应起来。手头记录引用了一个最终求解脚本,但目录没有单独提供该文件,因此这里不把它写成一份已经重新验证的完整复现包。

Web

ezphp

分析围绕 URL 请求入口、内部服务和 PHP 会话处理之间的信任关系展开。问题跨了不止一个组件,单独看某个请求返回正常,并不足以说明整个链路安全。

可以拆成三个检查层次:

  1. 请求边界:用户提供的 URL 最终能访问哪些协议与目标,跳转后的目的地是否仍受约束。
  2. 会话存储边界:内部存储是否被当成天然可信的数据源,读写权限与会话数据来源是否有明确约束。
  3. 反序列化边界:读取会话时是否可能把不可信内容解释成可执行对象结构。

分析时还区分了“读取不到目标内容”和“后续处理没有触发”两类结果。这个区分在分析中很有必要:没有最终输出时,要先确定失败发生在哪一层,而不是把所有异常都归为同一个原因。

赛后

254 名,第二天还有大半题没打,这次先把结果记在这里。

整理材料时,有几处比“最后输出了什么”更值得留下:雪花图要看时间相关性,奖励搜索要走出最终分数的平台,模型参数的随机生成方式可能比编码格式更容易突破;逆向题里,原程序接受输入与自写模型算出相同结果,也不是一回事。

下次打完,附件、脚本、输出和提交记录也得及时整理,不然赛后还要重新找一遍。

作者 IE · CC BY 4.0转载时请注明出处

搜索文章

输入关键词搜索文章。

搜索标题、标签与正文ESC 关闭