2026 KCTF | 第三题《午时·永数囚笼》设计思路及解析
2026 KCTF
2026 KCTF
看雪学苑
2026年8月15日 17:59
上海
在小说阅读器读本章
去阅读
8月10日12:00,2026 KCTF攻防对抗赛正式开赛!本届赛事沿用多维度积分体系,难度、火力、精致度三重积分同步核算,攻防双向计分机制兼顾趣味性与竞技性。
\*签到题《辰时·钟鸣破晓》赛事周期内持续开放,比赛全程都可以提交 flag 获取对应攻击积分。
截止今天中午12点,第三题《午时·永数囚笼》答题通道已关闭。本题仅6支战队攻破,他们分别是:
本题KCTF组委会点评:
AntiAi 是一道完成度较高、设计思路鲜明的 Windows 逆向题。它没有停留在传统 VM 混淆层面,而是通过加密字节码、状态相关控制流、多世界诱饵路径和完整性校验,把最终结果与完整执行轨迹绑定,有效提高了字符串检索、局部 Patch 和单点跟踪等常规方法的分析成本。
本题更值得肯定的是,复杂度并非单纯依赖“堆混淆”。解题过程中仍可逐步还原 VM 语义、状态机和数据流,并结合排列约束、终态固定值、可逆变换以及专用求解器完成反演。WP 中多次推翻错误假设并重新建模,也很好体现了逆向分析中“假设—验证—修正”的核心方法。
从 AntiAi 的命题角度看,它并非让 AI 无法解题,而是显著提高 AI 自动化分析所需的上下文维护、建模、验证和工程实现成本。这种“提高理解和建模门槛,而不是简单增加垃圾代码”的设计思路,对 AI 时代的 CTF 逆向题具有较好的参考价值。
一起来看看本题的设计思路&解题思路
一、
设计思路
出题战队:天外星系
战队成员ID:geekfire
AntiAi 是一题 Windows 本地逆向题。它把 26 字节输入校验置于自定义 VM 中,以加密字节码、状态相关控制流、动态分派、完整诱饵路径和运行时完整性检查,避免选手仅凭字符串检索、单点比较或修改一个条件跳转完成题目。
AntiAi 的关键设计是“四世界执行记录承诺”(four-world transcript commitment):
每个候选输入均执行 4 条物理打包入口:1 条真实世界和 3 条完整诱饵世界。
每个世界拥有独立而自洽的状态、路由、流值、终止 PC 和 8 字摘要。孤立地跟踪诱饵时,它会表现为可信的本地有效轨迹。
最终成功不取决于任一世界的局部 bad 值,而是把四条完整 trace(包含累计故障/失配字)混合成 4 个承诺 DWORD,并与加密目标比较。
因而强制进入诱饵入口、跳过局部比较、篡改一段 VM 密文或在调试中造成迟到故障,都无法得到真实输入的全局承诺。
预期解法是还原解释器、解密并理解路径/状态依赖,然后重建约束恢复真实输入。
stdin
│ 读取、零初始化、去掉最多一个行结束符
▼
原始输入 ─► 26 字节规范化 ─► 8 轮扩散 ─► 32 字节 VM 值与 12 位 tag
│ │
│ └► 派生世界运行顺序
▼
VM 密文 seal + 当前 PE 的 .text seal
│
├─► 世界 0:入口解封 → VM 解释 → 本地 trace
├─► 世界 1:入口解封 → VM 解释 → 本地 trace
├─► 世界 2:入口解封 → VM 解释 → 本地 trace
└─► 世界 3:入口解封 → VM 解释 → 本地 trace
│ ▼ state / flow / route / PC / 步数 / digest / fault │ ▼ 4 个 trace 混合为 4 字跨世界承诺 │ 原始长度失配在此阶段并入 ▼ 比对加密目标并输出响应字
正确答案为:
flag{v4\_opaque\_trace\_2026}
二、
赛题解析
该解题思路由论坛会员 F1ux\_Team 提供
1. 先固定可观测事实
分析中使用的关键地址如下:
输入读入后会去除 CR/LF。0x14000306C 内部最多复制 0x1A 字节,同时在返回前把长度与 0x1A 的差写入最终差异量。
因此通过条件包含:
length == 26
对 VMCode 计算出的两个常量为:
seed0 = B0ADEFF3
seed1 = 03DB2DD2
在正常桌面环境中,调试探测与完整性检查均走未触发分支。为了排除仿真误差,低级指令模型、Unicorn 和 Windows 原生进程分别对多组 26 字节样本进行了比较;每份 240 字节 worker 记录和 16 字节汇总值均一致。
这一步得到的结论很具体:后续求解可以使用已经抽出的整数模型,候选仍需交还原程序确认。
2. 32 字节内部状态的来源
设用户数据为 M[0..25]。进入 0x140002BA0 前,程序构造:
M || 5a a5 f0 e1 d2 c3
随后按小端解释为 A[0..7] 八个 DWORD。初始加白为:
A[i] ^= 6FECC3EE - 61C88647*i (mod 2^32)
第 r 轮先更新两个位置:
A[r] += (6D2B79F5*r) XOR 7F4A7C15
A[(r+3)&7] ^= ROL32(3C6EF372*r + A5A55A5A, r+5)
然后调用一次固定的八字 ARX 置换。这样的轮次共八次,输出记作 X[0..31]。
所有算子都存在明确逆操作:加法用减法撤销,循环左移用循环右移撤销,异或再次执行即可。八字置换也可按中间量逐项回代。逆回初始数组后,合法状态必须满足:
A[7] = C3D2E1F0
A[6] & FFFF0000 = A55A0000
这 48 个固定位用于淘汰无法来自 26 字节输入的 X。
本题最终恢复的内部状态为:
f1b4801d0d18245b437c05da5b354020
262485099321b50a6615bbd15e601d3f
将它逆回去,八个原始 DWORD 为:
67616c66 5f34767b 7161706f 745f6575
65636172 3230325f a55a7d36 c3d2e1f0
去掉固定尾部后正好留下 26 个答案字节。
3. 四份 diagnostic record 的真实语义
每个 worker 写出 15 个 DWORD:
[0] 末态 r
[1] 末态 s
[2] 末态 code key
[3] 末态 VM PC
[4] 输出字节数
[5] 执行指令数
[6] error bitmap
[7..14] 32 字节 raw output 经四轮混合后的八个 DWORD
其中 error 的精确计算式为:
duplicate_primary
OR complement(used_primary)
OR (r_final XOR r_reference)
OR (s_final XOR s_reference)
OR reduce_or(post_words XOR reference_post_words)
VM 内部的停止标志只负责离开循环。它与记录中的 error 字段分开保存。外层函数不会看到 error != 0 就立即返回;整个 DWORD 会作为第七个字段继续参与 0x140002940 的运算。
答案对应的实际记录状态是:
因此只有编号 2 的 worker 符合自身参考记录,其余三份记录依然是外层计算所需的数据。
4. 从总比较值定位 worker 2
最终选择器要求 0x140002940 的四个输出 DWORD 等于:
892A48AF B45DCD47 006C5AC3 B0B1062A
汇总函数尾部是 128 位线性变换。构造它对 128 个基向量的响应矩阵,得到秩 128,所以末态唯一。再把前面的四段 32 位递推按顺序消元,只剩一个 32 位闭合方程。枚举该方程的根并逐个阻断后,只找到下列一组 worker 摘要:
D0 = 1F1CC1BC
D1 = 1B7D21A2
D2 = 47D3D751
D3 = 27FF56E7
把二进制内置的四份 worker 参考记录分别送入摘要函数,编号 2 的参考记录恰好产生 47D3D751。这给出了一个可直接求原像的单路条件:
PC_final = 00000400
codekey_final = 408CF867
r_final = E6A68D04
s_final = 2E8C989C
error = 00000000
从参考 post words 撤销末尾四轮 ARX 与状态注入后,得到 worker 2 的原始 32 字节目标:
40 e0 86 d0 ce ff 2d 27 0d e6 e2 8b d4 9f 57 af
be b5 31 6b d8 32 29 83 82 09 99 bc cb 16 ab 1d
这里没有要求另外三个 worker 也套用各自的参考输出。解出一个满足 worker 2 的 X 后,再运行四 worker 的总验证即可判断它是否同时生成另外三个所需摘要。
5. 把 worker 2 改写成 32 次记录更新
worker 2 的一次完整运行包含 257 条 VM 指令。逐条记录 opcode、PC、code key 和跨轮寄存器后,可以看到:
前 256 条 = 32 组 × 每组 8 条
最后 1 条 = 终止指令
每组只有一次 primary byte 读取和一次 raw byte 提交。把固定的 8 条指令折叠成一项参数后,第 p 组可以表示成以下过程。
先由当前两个 32 位状态计算索引:
j = I_p(r, s) & 31
primary = X[j]
I_p 的常量全部来自该组解码后的 load 指令。primary 经过本组专属的 8 位双射:
u = U_p(primary, r)
普通组满足:
raw[p] = u
带交叉读取的组满足:
raw[p] = u XOR ROL8(X[k], shift)
交叉组只出现在这些记录号:
2, 3
12, 13, 14, 15
22, 23
24, 25, 26, 27
其中 2/3 与 22/23 分别互相引用;12..15 和 24..27 各自在一个四字节窗口内切换次序。
提交输出后,两个跨组状态更新为:
r_next = ROL32(r + primary*045D9F3B + (p XOR A7), (p mod 13)+5)
XOR 9E3779B9
s_next = ROL32(s XOR r_next XOR K_p(raw[p], r_next), rotation_p)
+ 6D2B79F5
第二式中的 K_p 由 code key 指令、记录号和 raw byte 组成;所有量在进入下一组前已经确定。
抽出的 32 组模型用随机 X 与低级 VM 逐组比较,检查项包括 primary index、raw byte、r_next、s_next、PC 和 code key。它们全部相等后才用于恢复答案。
6. 专用枚举器
搜索状态只保存:
p, r, s
primary 占用位图
X 的已赋值位图
X[32]
两个四字节交叉组的选择位
12-bit 桶号
6.1 普通记录不产生 256 路分支
U_p 由 8 位加减、旋转、异或和奇数乘法构成。奇数在模 256 下有乘法逆元,所以:
primary = inverse_U_p(raw[p], r)
然后检查 X[j] 是否已经被赋成不同的值。普通记录通常只产生一个后继状态。
6.2 交叉记录的分支规则
令 secondary 位置为 k。
X[k]
已赋值时,raw 可唯一确定 primary;
* X[j]
已赋值时,raw 可唯一确定 secondary;
* 两处都空时,枚举 256 个 primary,每个 primary 反推出唯一 secondary。
赋值冲突当场丢弃,无需等到 32 组结束。
6.3 primary 占用位图
worker 2 的 error 必须为零,因此:
32 次 primary index 互不重复
索引域也是 32 个元素,所以最终自然覆盖全部 0..31。每次得到 j 后先查占用位图,重复索引直接终止该分支。
答案分支的索引顺序为:
13,10,17,15,4,7,6,24,
0,28,23,31,19,16,3,20,
27,25,14,1,22,18,21,11,
12,8,2,9,30,29,26,5
6.4 用组内赋值数量拆任务
进入记录 12 时,统计 X[12..15] 已经填入几个位置。答案路径的计数为 2。枚举器仅保留该计数等于 2 的任务;记录 22 和 24 处不额外限制。
另一个任务维度是 X 的 12 位桶号。桶计算为:
d = C3D2E1F0
for i = 0..7:
z = LE32(X[4*i:4*i+4]) - 61C88647*(i+1)
z ^= d
n = ((z >> 27) & 7) + 5
d = C2B2AE35 - ROL32(z,n)*7A143595
h = ((((d >> 9) XOR d) >> 7) XOR d) & FFF
worker 的 s0 由 h 计算,因而每个桶可以独立执行。本次复现取:
h mod 8 = 0
依次处理 0, 8, 16, ...,在 h=0x110 时找到唯一完整叶子。
6.5 叶子检查
记录 31 完成后依次检查:
r == E6A68D04
s == 2E8C989C
primary bitmap == FFFFFFFF
assigned bitmap == FFFFFFFF
hash12(X) == h
inverse_preprocess(X) 的固定 48 位正确
编译参数为:
--edition 2021 -C opt-level=3 -C target-cpu=native
本机复现耗时 8.59 秒,关键输出为:
FLAG=flag{v4_opaque_trace_2026}
FLAG_HEX=666c61677b76345f6f70617175655f74726163655f323032367d
HASH=110
STATE=f1b4801d0d18245b437c05da5b354020262485099321b50a6615bbd15e601d3f
7. 候选的完整运行记录
候选的 worker 2 raw output 与目标逐字节相等:
40e086d0ceff2d270de6e28bd49f57af
beb5316bd8322983820999bccb16ab1d
四份 60 字节记录经汇总后得到:
worker digests:
1F1CC1BC 1B7D21A2 47D3D751 27FF56E7
aggregator output:
892A48AF B45DCD47 006C5AC3 B0B1062A
候选分别经过三套实现:
- 从反汇编重写的整数模型;
- 运行原 VM 指令的 Unicorn 环境;
- 未处于调试状态的 Windows 原生进程。
三者都给出:
6f 6b 0d 0a
即 ok\\r\。
8. Flag
flag{v4_opaque_trace_2026}
球分享
球点赞
球在看
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论