首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
1
看雪 CTF 2026 第二题「巳时·绿光幽语」Writeup by lzq2000
发表于: 2026-8-12 18:01
1399
看雪 CTF 2026 第二题「巳时·绿光幽语」Writeup by lzq2000
lzq2000
2026-8-12 18:01
1399
# 看雪 CTF 2026 第二题「巳时·绿光幽语」Writeup > **题目**:巳时·绿光幽语(第二题) > **提交地址**:`https://ctf.kanxue.com/game-season_fight-268.htm`(`gaid=268`) > **目标**:找出 6 位注册码(flag),使程序"还原代码"后打印 `恭喜成功!` > **注册码 / flag**:**`yb6tcr`** > **解题环境**:Python 3.13.12、Windows、pycdc.exe、自写定位/反序列化脚本 --- ## 0. 一句话结论 程序把一段被 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_code` 做 `key[i % 6] = c[i] ^ co_code[i]`,26 字节全部自洽于同一个 6 字节 → **`yb6tcr`**,且 `succ XOR key == b'恭喜成功!'` 二次验证通过。 --- ## 1. 程序静态特征 - `题目.exe` 由 **PyInstaller onefile + Python 3.13** 打包(pyc 魔数 `f3 0d 0d 0a`)。 - bootloader 经过魔改,运行时反射式加载 Python,**没有把 pyz / PYZ 里明文藏着脚本**,而是把核心逻辑混进了 `python313.dll`。 - 运行表现:循环打印一段中文提示,要求输入 **6 字符的 key(注册码)** 来"还原代码";输入正确后 `my_function` 会打印 `恭喜成功!`。 - 目录里有个 `main.pyc`(175 字节),反编译后是诱饵桩: ```python # main.pyc import sys sys.exit(0) ``` `main.py` 为空——真正的逻辑根本不在 `main` 里。 --- ## 2. 走过的弯路(避坑) 此前的分析在几个方向上反复碰壁,记录如下,供复盘: 1. **内存扫描(frida)全部落空**。进程打印循环有硬上限(约 20 次),约 1.5s 就退出,后期内存扫描(>700ms)时进程早已不在;且 `my_function` 这个字符串在运行时内存里**从未作为 Python 字符串出现**(提示由保护侧/外壳打印)。 2. **`codeobj_dump.bin` 误判**。导出的一段 196KB 数据头是 `_Py_CODEUNIT` 字节码表,里面大量"中文串"只是二进制噪声被 UTF-8 偶然解码出来的,根本不是 code object,marshal 扫描 0 命中。 3. **`0x69` marshal 特征扫描是根本性错误**。动手前想当然用 `TYPE_INT (0x69)` 当 marshal 特征去扫,结果 0 命中。实测 Python 3.13 的 code object 整数字段**不再带 `TYPE_INT` 前缀**,详见第 4 节。 4. **`marshal.loads` 会崩**。对损坏数据调用 `marshal.loads` 会让整个进程 **SIGSEGV**,所以所有的 `marshal.loads` 尝试都必须放在**崩溃隔离子进程**里。 真正的突破口在第 3 节。 --- ## 3. 突破口:crackme 载荷明文内嵌 `python313.dll` 从 `题目.exe` 提取出 `python313.dll`(约 7.1MB)。对它的 `.rdata` 节直接做字符串/字节搜索,发现**明文内嵌了 crackme 的 marshal 代码**(未加密): - 字符串 `my_function` 出现在文件偏移 **`0x4dace9`**; - 紧跟其后的 `my_function` **code object 定义**从 **`0x4dac93`** 开始(`63 00 00 00 00 ...`); - 其后紧跟着 GBK 编码的中文提示串,如 `b3 da c5 b2 f1 ef` = "请输入"、`ea cd b0 db` = "key" 等。 也就是说,出题人把整个 crackme 当成一个"frozen 模块"塞进了解释器 DLL。这是本题最关键的 trick。 --- ## 4. Python 3.13 marshal `code object` 布局(实测确认) 用 `main.pyc` 反序列化对照 `marshal.dumps` 实测,Python 3.13 的 code object 头布局如下: | 字段 | 内容 | | ---- | -------------------------------------------------------------------------------------------------------------- | | 类型字节 | `0xe3`(`TYPE_CODE \| FLAG_REF`,带引用索引)或 `0x63`(裸定义) | | 引用索引 | 仅 `0xe3` 后出现 **4 字节**小端引用序号 | | 整数字段 | 紧跟 **4 字节** ×N:`co_argcount`、`co_posonlyargcount`、`co_kwonlyargcount`、`co_nlocals`、`co_stacksize`、`co_flags`…… | 注意:**整数字段前面没有 `TYPE_INT (0x69)`**,这就是第 2 节第 3 点坑的来源。 由此得到**模块级 code object(`co_argcount==0 && co_posonlyargcount==0`)的精确预筛特征**: ``` 0x63 后跟 8 字节 0x00 → 模块级裸 code object 0xe3 后跟 4 字节 ref,再跟 8 字节 0x00 → 模块级带引用 code object ``` 另一个坑:程序的中文输出是 **GBK 编码**,不是 UTF-8/UTF-16,这导致早期只按 UTF-8/UTF-16 扫内存时漏掉了 prompt 串。 --- ## 5. 定位顶层模块 以 `my_function` 锚点(`0x4dace9`)为起点向前回扫,用第 4 节的模块级特征做预筛,对每个 `0x63`/`0xe3` 候选放到崩溃隔离子进程里 `marshal.loads` 试解。 命中:文件偏移 **`0x4d00e0`** 处有一个 `<module>` code object,其 `co_filename = <frozen os>`——它伪装成 frozen 的 `os` 模块,但结尾被追加了 crackme 主循环与 `my_function` 嵌套函数。 > 该模块里能 `marshal.loads` 干净加载,说明载荷完全明文、未被二次加密。 --- ## 6. 还原算法(核心) 对 `0x4d00e0` 处的模块 `dis.dis` 后(见 `crackme_disasm.txt` 第 1040–1230 行),提取出 crackme 主循环。相关常量取自模块 `co_consts`: ```python 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 字节,加密后的成功串 ``` 主循环逻辑(伪代码 / 反汇编直译): ```python # 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() ``` 关键点:嵌入的 `my_function` 原始 `co_code` 是合法的 `print(...)` 字节码(原始 `co_consts = (None, 666)`)。程序用 `buf` 覆盖 `co_code`、用 `(None, '恭喜成功!')` 覆盖 `co_consts`。因此**"成功"的本质是:解密出的 `buf` 必须和原始合法 `co_code` 一字节不差**,否则就是非法操作码 → 崩溃。 --- ## 7. 求解 正确 key 的充要条件:`buf == 原合法 co_code`,即 ``` key[i % 6] = c[i] ^ co_code[i] (i = 0..25) ``` 从 `0x4d00e0` 处 marshal 流取出原始 `my_function.__code__.co_code`(26 字节): ``` co_code hex = 95005b0100000000000000005301350100000000000020006700 ``` 逐字节做 `c[i] ^ co_code[i]`(注意 `i % 6` 周期): | 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 周期) | 26 个位置全部收敛到同一个 6 字节周期 → **`key = "yb6tcr"`(唯一解)**。 **二次验证**:用同一 key 解密成功串,应得 `恭喜成功!`(UTF-8): ``` succ ^ b'yb6tcr' → b'\xe5\xb7\xa5\xe5\x96\x9c\xe6\x88\x90\xe5\x8a\x9f\xef\xbc\x81' → '恭喜成功!' ✓ ``` 推导脚本见 `solve.py`,核心两行: ```python co = marshal.loads(open(r"C:\Users\Administrator\python313.dll","rb").read()[0x4d00e0:]) key = bytes(c[i] ^ co_code[i] for i in range(len(c))) # 取 i%6 周期即 yb6tcr ``` --- ## 8. 验证(`sim.py`) 完整复刻第 6 节逻辑,`sim.py` 实测输出: ``` my_function co_names: ('print',) my_function co_consts: (None, 666) === CORRECT key 'yb6tcr' (derived from co_code XOR c) === key='yb6tcr' buf==orig_co_code? True buf2.decode='恭喜成功!' my_function() OK, returned=None, printed='恭喜成功!\n' === wrong key 'ob6tcr' === key='ob6tcr' buf==orig_co_code? False buf2.decode="<decode-error: ...>" my_function() CRASH: SystemError('<frozen os>:1188: unknown opcode 131') === wrong key 'xxxxxx' === key='xxxxxx' buf==orig_co_code? False buf2.decode="<decode-error: ...>" my_function() CRASH: SystemError('<frozen os>:1188: unknown opcode 148') === wrong key 'abcdef' === key='abcdef' buf==orig_co_code? False buf2.decode="<decode-error: ...>" my_function() CRASH: SystemError('<frozen os>:1188: unknown opcode 141') ``` - 正确 key:`buf` 与原始 `co_code` **完全一致**,`buf2.decode` 得到 `恭喜成功!`,`my_function()` 正常执行并打印 `恭喜成功!`。 - 任意错误 key:`buf` 与原始 `co_code` 不符,还原出的字节码出现非法操作码 → `SystemError: unknown opcode` 崩溃(与题目"还原代码"判定完全对应)。 --- ## 9. 提交 按题目页面 JS(`$.xpost`,即带 `X-Requested-With: XMLHttpRequest` 的 AJAX)向 `https://ctf.kanxue.com/game-team_fight.htm` POST: ``` gaid=268 serialkey=yb6tcr ``` 结果返回 JSON: ```json {"code": "-1", "message": "请先登录"} ``` 说明提供的 Cookie **不是有效登录态**(可能过期或只是游客 token),flag 实际未被服务端接收。需在自己浏览器登录看雪账号后到该题页面手动填入 `yb6tcr` 提交,或提供有效登录 Cookie 后再代提交。 --- ## 10. 复盘 / 防御视角 - **难点在于"藏"**:出题人把 crackme 作为一个伪装成 `os` 的 frozen 模块塞进 `python313.dll` 的 `.rdata`,绕过了所有常规的 PyInstaller 提取路径(`pyinstxtractor` / `pycdc` 在 `main` 上扑空)。解题关键是直接对 DLL 做明文 marshal 载荷搜索。 - **难点在于"版本"**:Python 3.13 的 marshal 布局与旧版不同(整数字段去 `TYPE_INT`),凭老经验用 `0x69` 特征扫描会彻底走偏——必须以实测为准。 - **难点在于"编码"**:GBK 输出导致字符串搜索遗漏;GBK 串的偶然 UTF-8 解码又会造成"假中文"噪声。 - **技巧点**:`code.replace(co_code=...)` + `co_consts=...` 的动态"还原代码"是 Python 反射能力的经典玩法,校验等价于"加密字节码 XOR key == 合法字节码",因此可直接反解出唯一 key。 --- ## 附:关键文件清单 | 文件 | 说明 | | --------------------------------------------------------------- | ------------------------------------------------------- | | `crackme_disasm.txt` | `0x4d00e0` 模块完整 `dis` 反汇编(1040–1230 行为 crackme 主循环) | | `my_function_disasm.txt` | 嵌入 `my_function` 的反汇编(`print(666)` 桩) | | `solve.py` | 从 marshal 流取原始 `co_code`,`key = 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` | 解题笔记 | **Flag:`yb6tcr`**
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
1
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
njtoplc
你的帖子非常有用,感谢分享!
2026-8-13 11:20
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
2
)
njtoplc
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
1
粉丝
0
关注
私信
njtoplc
2
楼
加油!!!
2026-8-13 11:19
0
anywhere杨
雪 币:
206
活跃值:
(1308)
能力值:
( LV4,RANK:40 )
在线值:
发帖
58
回帖
448
粉丝
3
关注
私信
anywhere杨
3
楼
999
2026-8-13 11:34
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
lzq2000
6
发帖
6
回帖
47
RANK
关注
私信
他的文章
[原创] 第十题:卯时·曦光初现 wp by lzq2000
1038
[原创] 第九题:丑寅同墟·星海抉择 wp by lzq2000
41
[原创] 第八题:亥子合辰·塔影迷楼 wp by lzq2000
57
第七题:戌时·暗能潜流
21
看雪 CTF 2026 第二题「巳时·绿光幽语」Writeup by lzq2000
1399
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部