首页
社区
课程
招聘
看雪 CTF 2026 第二题「巳时·绿光幽语」Writeup by lzq2000
发表于: 8小时前 216

看雪 CTF 2026 第二题「巳时·绿光幽语」Writeup by lzq2000

8小时前
216

题目:巳时·绿光幽语(第二题)
提交地址https://ctf.kanxue.com/game-season_fight-268.htmgaid=268
目标:找出 6 位注册码(flag),使程序"还原代码"后打印 恭喜成功!
注册码 / flagyb6tcr
解题环境:Python 3.13.12、Windows、pycdc.exe、自写定位/反序列化脚本

程序把一段被 XOR 加密的 code object 字节码内嵌在 python313.dll 里,运行时要求你输入一个 6 字符 key,用 key 逐字节 XOR 解密出一段合法的 co_code,再把它塞回 my_function.__code__.co_code;同时用同一 key 解密出成功提示串 恭喜成功! 塞进 co_consts只要 XOR 解密出来的 co_code 与原先那段合法字节码完全一致(即 key 正确),my_function() 就能正常执行并打印"恭喜成功!";key 错误则字节码变成乱码,执行时 SystemError: unknown opcode 崩溃。

对加密常量 c 与原始合法 co_codekey[i % 6] = c[i] ^ co_code[i],26 字节全部自洽于同一个 6 字节 → yb6tcr,且 succ XOR key == b'恭喜成功!' 二次验证通过。

此前的分析在几个方向上反复碰壁,记录如下,供复盘:

真正的突破口在第 3 节。

题目.exe 提取出 python313.dll(约 7.1MB)。对它的 .rdata 节直接做字符串/字节搜索,发现明文内嵌了 crackme 的 marshal 代码(未加密):

也就是说,出题人把整个 crackme 当成一个"frozen 模块"塞进了解释器 DLL。这是本题最关键的 trick。

main.pyc 反序列化对照 marshal.dumps 实测,Python 3.13 的 code object 头布局如下:

注意:整数字段前面没有 TYPE_INT (0x69),这就是第 2 节第 3 点坑的来源。

由此得到模块级 code object(co_argcount==0 && co_posonlyargcount==0)的精确预筛特征

另一个坑:程序的中文输出是 GBK 编码,不是 UTF-8/UTF-16,这导致早期只按 UTF-8/UTF-16 扫内存时漏掉了 prompt 串。

my_function 锚点(0x4dace9)为起点向前回扫,用第 4 节的模块级特征做预筛,对每个 0x63/0xe3 候选放到崩溃隔离子进程里 marshal.loads 试解。

命中:文件偏移 0x4d00e0 处有一个 <module> code object,其 co_filename = <frozen os>——它伪装成 frozen 的 os 模块,但结尾被追加了 crackme 主循环与 my_function 嵌套函数。

该模块里能 marshal.loads 干净加载,说明载荷完全明文、未被二次加密。

0x4d00e0 处的模块 dis.dis 后(见 crackme_disasm.txt 第 1040–1230 行),提取出 crackme 主循环。相关常量取自模块 co_consts

主循环逻辑(伪代码 / 反汇编直译):

关键点:嵌入的 my_function 原始 co_code 是合法的 print(...) 字节码(原始 co_consts = (None, 666))。程序用 buf 覆盖 co_code、用 (None, '恭喜成功!') 覆盖 co_consts。因此"成功"的本质是:解密出的 buf 必须和原始合法 co_code 一字节不差,否则就是非法操作码 → 崩溃。

正确 key 的充要条件:buf == 原合法 co_code,即

0x4d00e0 处 marshal 流取出原始 my_function.__code__.co_code(26 字节):

逐字节做 c[i] ^ co_code[i](注意 i % 6 周期):

26 个位置全部收敛到同一个 6 字节周期 → key = "yb6tcr"(唯一解)

二次验证:用同一 key 解密成功串,应得 恭喜成功!(UTF-8):

推导脚本见 solve.py,核心两行:

完整复刻第 6 节逻辑,sim.py 实测输出:

按题目页面 JS($.xpost,即带 X-Requested-With: XMLHttpRequest 的 AJAX)向 https://ctf.kanxue.com/game-team_fight.htm POST:

结果返回 JSON:

说明提供的 Cookie 不是有效登录态(可能过期或只是游客 token),flag 实际未被服务端接收。需在自己浏览器登录看雪账号后到该题页面手动填入 yb6tcr 提交,或提供有效登录 Cookie 后再代提交。

Flag:yb6tcr

字段 内容
类型字节 0xe3TYPE_CODE | FLAG_REF,带引用索引)或 0x63(裸定义)
引用索引 0xe3 后出现 4 字节小端引用序号
整数字段 紧跟 4 字节 ×N:co_argcountco_posonlyargcountco_kwonlyargcountco_nlocalsco_stacksizeco_flags……
i c[i] (hex) co_code[i] key[i%6] 字符
0 ec 95 79 y
1 62 00 62 b
2 6d 5b 36 6
3 75 01 74 t
4 63 00 63 c
5 72 00 72 r
6 79 00 79 y
7 62 00 62 b
8 36 00 36 6
9 74 00 74 t
10 63 00 63 c
11 72 00 72 r
12 2a 00 2a * (仅影响 buf,不参与 key 周期)
(12..25 全部自洽于 yb6tcr 周期)
文件 说明
crackme_disasm.txt 0x4d00e0 模块完整 dis 反汇编(1040–1230 行为 crackme 主循环)
my_function_disasm.txt 嵌入 my_function 的反汇编(print(666) 桩)
solve.py 从 marshal 流取原始 co_codekey = c XOR co_code 推导
sim.py 复刻程序逻辑,验证 yb6tcr 成功、错误 key 崩溃
crk_run.py / crk_one.py / crk_worker.py / crk_driver.py python313.dll 中做模块级预筛 + 崩溃隔离 marshal.loads 定位顶层模块
dis_*.py 反汇编 / 查找 my_function 辅助脚本
python313.dll 题目.exe 提取,含明文 crackme 载荷
.workbuddy/memory/2026-08-11.md 解题笔记
# main.pyc
import sys
sys.exit(0)
0x63 后跟 8 字节 0x00  →  模块级裸 code object
0xe3 后跟 4 字节 ref,再跟 8 字节 0x00  →  模块级带引用 code object
c    = b'\xecbmucryb6tcr*c\x03ucryb6tCr\x1eb'          # 26 字节,加密后的 co_code
succ = b'\x9f\xe3\x9b\x91\xf5\xee\x9f\xea\xa6\x91\xe9\xed\x96\xde\xb7'  # 13 字节,加密后的成功串
# 1) 一段 UI 提示:8 个经 XOR 85 编码的 GBK 字节串,逐元素 ^85 后拼接、print(与校验无关)
buffer = b''.join(i ^ 85 for i in [
    b'\xb3\xda...', b'\xea\xcd...', b'103u8,\n3 ;6!<:;}',
    b'|oX_\\%\'<;!}w...', b'\xc3\xc9...', b'\xfa\xe2...',
    b'\xca\xb1...', b'\xb0\xd0...'
])
print(buffer.decode('utf-8'))

# 2) 读 key,必须是 6 字符
key = input()
while len(key) != 6:
    key = input()          # 不等长就回到循环起点重输
b_key = key.encode()

# 3) 解密 co_code
c    = b'\xecbmucryb6tcr*c\x03ucryb6tCr\x1eb'
buf  = bytes(c[i] ^ b_key[i % 6] for i in range(len(c)))
my_function.__code__ = my_function.__code__.replace(co_code=buf)

# 4) 解密成功提示串,塞进 co_consts 的 const[1]
succ = b'\x9f\xe3\x9b\x91\xf5\xee\x9f\xea\xa6\x91\xe9\xed\x96\xde\xb7'
buf2 = bytes(succ[i] ^ b_key[i % 6] for i in range(len(succ)))
my_function.__code__ = my_function.__code__.replace(co_consts=(None, buf2.decode('utf-8')))

# 5) 调用——正确 key → co_code 合法 + const[1]='恭喜成功!' → 打印"恭喜成功!"
my_function()

冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回