首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
1
2
[原创][原创]KCTF2026第四题:未时·车流困城(AI总结)
发表于: 2026-8-17 21:14
860
[原创][原创]KCTF2026第四题:未时·车流困城(AI总结)
秋白
2026-8-17 21:14
860
拿到这道题的前 10 小时,我一度以为自己已经破解了核心算法 —— 我脱了壳、逆向了 AES 结构、恢复了 S 盒、算出了多项式根,生成的 Serial 在自己补丁后的程序里稳稳输出Successful!。 但一拿到原版程序上跑,直接红底白字跳出Failed!。 反复核对了三遍算法,数学上完全自洽,每一步逆推都能正向复现,但原版就是不认。 后来我才明白:这道题最难的地方从来不是 “算法有多复杂”,而是它布下了三层递进的欺骗陷阱。你以为自己在走正门,其实从一开始就掉进了出题人精心设计的诱饵迷宫里。 这篇手记会按照我实际踩坑、破局的顺序,完整还原从 “拿到程序一头雾水” 到 “跑出正确 Serial” 的全过程,所有结论均经过原版程序实测验证。 一、撕开第一道伪装:32 位 PE 下的 64 位暗门 程序是个再普通不过的 32 位控制台 CrackMe,拖进 IDA 里能看到完整的导入表和导出函数名:DayDayUp、MengXinQiuFangGuo、GoodGoodStudy、Warning,乍一看结构非常清晰。 主函数骨架很短,核心就是 5 次调用同一个跳板函数sub_4010E0,每次传入不同的函数指针和参数。 sub_4010E0这个函数很短,但关键指令一眼就懂: asm pusha pushf mov eax, [esp+28h] ; 第4个参数:函数指针 mov ebx, [esp+24h+arg_0] mov ecx, [esp+24h+arg_4] mov edx, [esp+24h+arg_8] push 33h ; 64位代码段选择子 call $+5 add [esp+2Ch+var_2C], 5 retf ; 远返回切入x64长模式 这是经典的Heaven's Gate(天堂之门)技术:32 位程序通过远返回切换 CPU 到 64 位长模式,去执行藏在内存里的 64 位代码。 IDA 默认按 32 位解析,所以核心逻辑所在的.data段全被识别成了数据,静态分析看到的 32 位代码,只是个负责调度的 “壳子”。 2. 异常驱动的宏指令 ISA 跟进 64 位代码后,我第一个疑惑是:里面充斥着大量in、out、int3这类特权指令,正常 ring3 程序根本不会这么写。 很快我反应过来:这些指令不是真的要执行 IO 操作,而是故意触发异常,程序提前注册了 VEH(向量异常处理)函数Warning,异常触发后由Warning根据触发指令的 opcode 分发执行对应的业务逻辑。 换句话说,出题人用异常机制自己造了一套指令集: 0xED(in eax, dx):反调试自检 0xEE(out dx, al):格式长度校验 0xEF(out dx, eax):派生加密密钥 0xEC(in al, dx):计算混淆跳转地址 这里第一个大坑就来了:不能直接把异常指令 NOP 掉。很多人(包括我一开始)觉得异常就是反调试,直接 patch 掉就能正常跑。结果跳过异常后,程序确实还能运行,但跑的已经是另一套假算法了 —— 异常本身承担了状态传递、密钥累加、控制流跳转的功能,它是算法的一部分,不是单纯的阻碍。 二、最隐蔽的陷阱:环境决定算法真假 这是我卡最久的一个坑,也是区分 “假解” 和 “真解” 的核心门槛。 1. 诡异的 “环境玄学” 我最早用 PowerShell 7 拉起程序调试,无论怎么输入公开的合法样例,永远输出Failed!。我一度以为公开样例是错的,或者我漏了什么校验。 直到偶然双击直接运行程序,手动粘贴公开样例,居然一次就过了。 同一个程序、同一个输入、同一个系统,只是启动方式不同,结果完全不一样。 2. 定位父进程白名单 顺着这个线索排查,很快定位到程序启动后会调用NtQuerySystemInformation枚举进程,拿到自己的父进程名,做一次大小写不敏感的 djb2 哈希,和内置的三个哈希值对比。 命中白名单(explorer.exe、cmd.exe、powershell.exe)才会进入真实算法分支;不命中就静默进入诱饵分支。 PowerShell 7 的进程名是pwsh.exe,不在白名单里,所以永远跑的是假算法。这也是为什么很多人用 Python subprocess 拉起程序永远不对的原因。 3. PEB 里的隐蔽信道 光过了父进程检测还不够。程序在执行流程中,分两次往PEB->BeingDebugged这个字节里写值: 执行 AES 解密(MengXin)时,加0x1A 执行格式校验(gate2)时,再加0x40 两次加起来正好是0x5A,这个值就是最终多项式校验层 VM 字节码的解密密钥。 如果你跳过了中间任何一步校验,或者调试器修改了 PEB 字段,这个值就不对,最终校验层会直接返回失败,没有任何报错提示。 我把它叫做 “两次盖章机制”:你必须完整走完所有正常流程,才能凑齐打开最后一扇门的钥匙。任何投机取巧的跳过,都会悄无声息地滑进错误路径。 快速判断自己是否在真路径的方法 不用等到最后看结果,做一个简单的差分实验: 修改 Serial 的第 1 个字符,运行后对比解密后的第 1 个 16 字节块。 如果输出和公开样例的对应块完全对不上,不用往下看了,你百分百在诱饵路径里,算法再完美也没用。 三、拆解第一层:Serial 的自定义编码还原 搞定环境问题后,正式开始从外到内逐层逆向。 1. Serial 结构初探 合法 Serial 固定 9226 字符,首尾各 5 个字符是固定的标识头和尾,中间 9216 个字符是正文。 统计正文字符集,正好 64 个可打印字符,第一反应就是变种 Base64。 2. 还原仿射偏移 如果只是换了字母表,那直接映射就能解码。但我做了个单字符修改实验:只改 Serial 第 i 位的字符,看解码后对应字节的变化。 结果发现:每个位置的字符,对应的 6 位值不是固定的,而是随位置线性变化。 通过多个位置采样计算,最终还原出变换公式: 设字符在字母表中的下标为a_i,对应的真实 6 位值为v_i 解码方向:v_i = (a_i + 51 + 27 * i) mod 64 编码方向:a_i = (v_i - 51 - 27 * i) mod 64 9216 个 6 位值正好打包成 6912 字节,没有填充位。这一步是一一映射,只要拿到 6912 字节的目标密文,就能唯一反推出 Serial 正文。 四、拆解第二层:从诱饵 S 盒到真实 AES 结构 6912 字节正好是 432 个 16 字节块,很自然想到是分组密码。 1. 确认 ECB 模式 还是用差分法:修改第 0 块的输入,看其他块的输出有没有变化。 结果是完全没有,说明是 ECB 模式,块与块之间独立,没有链式依赖。这意味着只要恢复出一个 16 字节块的逆变换,就能批量处理全部 432 块。 2. 状态转置的坑 一开始我按行优先的常规 AES 布局去套,怎么都对不上行移位和列混合的规律。 直到我把输入输出按列优先重排了一遍,结构瞬间清晰了:输入缓冲区按列主序存储,但内部轮函数按行主序运算。 漏掉这个转置,S 盒看起来可能还是个置换,但行移位、列混合永远对不上,这也是很多人卡在这一层的原因。 3. 还原真实 S 盒与轮密钥 前面说过,调试器附加会得到假 S 盒,所以不能用普通断点 dump。 我用的是无侵入的内存轮询法: 异常触发时,Windows 会把完整的 64 位寄存器上下文(包括 XMM 寄存器)存在线程栈上。我找到这份 CONTEXT 结构的固定位置,只读轮询内存,记录每一轮状态的变化。 整个过程不打断程序执行,不修改任何内存,程序全程以为自己在干净环境里运行,输出的自然就是真实算法的结果。 采集到相邻轮的状态后,通过差分计算: 单字节变化只扩散到一列,确认是 AES 的列混合结构,约简多项式是标准的0x11B 反向消去线性层,得到字节代换关系,拼出完整的 256 字节逆 S 盒 多块求交消去噪声,恢复出全部轮密钥和首轮输入异或值 最终确认:这是一个魔改的 AES-128 解密结构,正向执行用的是 AES 的逆列混合 + 右移行,不能直接调用标准 AES 库。 另外有个细节:每轮线性层之后,都会异或一遍全0x40的常量,漏掉这个偏移,单轮看没问题,多轮累计下来结果就会完全偏移。 五、拆解第三层:多项式根校验与长度暗桩 AES 解密出来是一段 ASCII 字符串,用-分隔的十进制数字,一共 1000 个整数,分成 100 组,每组 10 个。 1. 多项式系数的解密 用户名KCTF会经过两次哈希映射,生成 100 个 14 位的组号。每个组号对应 11 条系数记录,存在.rdata段的大表里。 这些系数不是明文,是加密过的压缩 BCD 码。解密密钥由组号和系数序号共同派生: plaintext base = (组号 * 0x9E3779B9) XOR (系数序号 * 0x517CC1B7) 解密后的格式是:首字节高 1 位是符号,低 7 位是十进制位数,后面的字节按半字节存储十进制数字。 11 个系数正好对应一个 10 次首一整系数多项式(最高次项系数为 1)。 2. 整数根求解 校验逻辑很直接:每组的 10 个数,代入对应多项式,结果必须全为 0。 换句话说,这 10 个数就是这个 10 次多项式的全部正整数根。 求解不用浮点运算,利用数论里的基本定理:首一整系数多项式的整数根,一定能整除常数项。 常规组:直接在整数环内做精确因式分解,得到 10 个互异正整数根 异常组(第 84 组):一次项系数被故意篡改,无法直接分解。我采用的方法是枚举常数项的所有正因子(共一万多个,计算量很小),逐一代入验证,筛选出唯一一组 10 个互异根。 3. 最后一个暗桩:文本长度 算完 1000 个根的时候我特别兴奋,直接编码成 Serial 拿去跑,结果又失败了。 数学上完全正确,为什么程序不认? 对比公开样例的明文长度,我发现了最后一个坑: 程序对解密后的明文长度有严格要求,必须落在特定区间内。原始根拼接起来的文本长度是 6891 字节,不满足条件。 而十进制解析是允许前导零的 —— 数值不变,但文本长度可以增加。 我在第一个整数前面补了 6 个前导零,总长度变成 6897 字节,尾部补 NUL 对齐到 6912 字节,再重新加密编码,这次一次通过。 这是出题人非常精妙的设计:你数学全对,但只要没注意到格式约束,照样拿不到分。 六、完整逆推:从多项式根到最终 Serial 把所有环节反过来串起来,就是完整的解题流程: 计算组号:输入用户名KCTF,通过哈希 + 雪崩变换,生成 100 个去重的 14 位组号 提取多项式:根据组号索引系数表,解密得到 100 个 10 次首一整系数多项式 求解整数根:对每个多项式因式分解,得到 10 个正整数根,组内升序排列 构造明文:将 1000 个根用-连接,通过补前导零调整文本长度,尾部补零到 6912 字节 AES 加密:用还原出的自定义 AES 算法,正向加密 6912 字节明文 编码 Serial:按自定义 Base64 规则编码密文,加上固定首尾,得到最终 9226 位 Serial 七、最终验证与结果 将生成的 Serial 输入未打任何补丁的原版程序,控制台稳定输出: plaintext Successful! 全程无爆破、无遍历、无内存补丁,完全通过代数逆推得到正确结果。 关于 “多解” 的补充 我测试发现,同一组数值可以生成很多个合法 Serial: 前导零可以补在不同位置,只要总长度合规就行 末尾多加一个-分隔符,程序也会接受 甚至因为程序实现有越界 bug,有 51 个位置的数值可以替换成任意大值,依然能通过校验 但从数学设计上来说,干净的正确解是唯一的。那些额外的解,要么是格式冗余,要么是实现缺陷。 八、复盘:这道题的精髓在哪里 做完这道题最大的感受是,它考的不是 “你会不会逆向 AES”、“你会不会解多项式”,而是你能不能跳出思维定式,识别出出题人的欺骗。 第一层欺骗:给你看 32 位代码,真实逻辑藏在 64 位里 第二层欺骗:给你一套完整的假算法,让你在调试器里自以为破解成功 第三层欺骗:数学全对也没用,格式暗桩卡在最后一步 它不是一道 “算法难题”,而是一道 “逆向工程素养题”—— 真正的逆向,从来不是对着代码死磕,而是时刻保持怀疑,验证每一步的前提,确认自己走的每一步都在真实路径上。 附:核心解题脚本(关键片段) 这里给出最核心的多项式求根与 AES 逆推逻辑框架,完整脚本可自行补全环境验证: python 运行 # 核心思路:整数环内精确分解首一多项式 def integer_roots(poly): """poly: 系数列表,从常数项到最高次项,首一多项式""" assert poly[-1] == 1 constant = abs(poly[0]) if constant == 0: return [0] + integer_roots(poly[1:]) # 枚举常数项的所有正因子 factors = get_all_positive_factors(constant) roots = [] remaining = poly.copy() for f in factors: # Horner法则验证 val = 0 for coef in reversed(remaining): val = val * f + coef if val == 0: roots.append(f) # 多项式降次 remaining = poly_deflate(remaining, f) if len(remaining) == 1: break return sorted(roots) # 逆推链路 # 1. 生成100组多项式根 all_roots = [] for state in derive_states("KCTF"): poly = decrypt_polynomial(state) roots = integer_roots(poly) all_roots.extend(roots) # 2. 调整长度并构造明文 plain_text = "-".join(str(r) for r in all_roots) # 补前导零使长度符合C4校验 plain_text = "0" * 6 + plain_text plain_bytes = plain_text.encode("ascii") + b"\x00" * (6912 - len(plain_text)) # 3. AES加密 + 自定义Base64编码 cipher = custom_aes_encrypt(plain_bytes, round_keys, sbox_inv) serial = custom_base64_encode(cipher) final_serial = HEAD + serial + TAIL
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
1
点赞
・
2
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
星野安全
感谢你分享这么好的资源!
2026-8-20 14:50
backer
感谢你的积极参与,期待更多精彩内容!
2026-8-20 00:00
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
RascallyDog
雪 币:
138
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
4
粉丝
3
关注
私信
RascallyDog
2
楼
认真看完了, 我觉得你是真的懂这个题, 也精准揣摩出了我的心思. 致敬~
2026-8-18 21:33
1
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
秋白
6
发帖
12
回帖
10
RANK
关注
私信
他的文章
[原创]第九题:丑寅同墟·星海抉择 (解题思路)
12
[原创]第七题:戌时·暗能潜流(AI解析)
13
[原创][原创]KCTF2026第四题:未时·车流困城(AI总结)
860
[建议]气死我了 KCTF解开了为什么服务器暴了??
91
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部