2026KCTF|第三题《午时·永数囚笼》设计思路及解析"

admin 2026-10-17 09:26:00 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

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

候选分别经过三套实现:

  1. 从反汇编重写的整数模型;
  2. 运行原 VM 指令的 Unicorn 环境;
  3. 未处于调试状态的 Windows 原生进程。

三者都给出:

6f 6b 0d 0a

即 ok\\r\。

8. Flag

flag{v4_opaque_trace_2026}

球分享

球点赞

球在看


2026KCTF|第三题《午时·永数囚笼》设计思路及解析" 网络安全文章

2026KCTF|第三题《午时·永数囚笼》设计思路及解析"

2026KCTF第三题《午时·永数囚笼》是一道Windows逆向题,通过加密字节码、状态相关控制流、多世界诱饵路径和完整性校验,将最终结果与完整执行轨迹绑定,提高分析成本。解题需还原VM语义、状态机和数据流,结合排列约束、终态固定值、可逆变换及专用求解器完成反演。正确答案为flag{v4_opaque_trace\\_2026}。
利用方法混淆绕过CSRF防护,导致账户完全被劫持" 网络安全文章

利用方法混淆绕过CSRF防护,导致账户完全被劫持"

本文分析了一个CSRF防护绕过漏洞,攻击者通过将POST请求转换为GET请求,利用SameSite=LaxCookie在顶级GET导航中发送的特性,绕过防护并实现账户完全接管。漏洞影响tvs._redacted_.com子域名,与主域名共享会话Cookie。建议修复包括强制执行POST方法验证、实施CSRF令牌、升级Cookie策略及验证TV代码归属。
opencode自动化渗透测试,给银手铐加一层限制" 网络安全文章

opencode自动化渗透测试,给银手铐加一层限制"

本文介绍了opencode的二开版本opencode-safe-judge,为自动化渗透测试引入LLM-as-judge守门员机制。通过小模型拦截并审查大模型的工具调用,能有效防止AI在测试中执行删库等高危操作。作者提供了多平台的部署教程,推荐使用硅基流动的14B小模型充当裁判以实现零成本监管。该方案虽无法彻底消除AI幻觉,但为安全使用自动化CLI工具提供了实用的工程化缓解思路。
评论:0   参与:  0