首页
社区
课程
招聘
[原创]KCTF 2026 · 第二题「巳时·绿光幽语」Writeup
发表于: 2026-8-17 13:51 22

[原创]KCTF 2026 · 第二题「巳时·绿光幽语」Writeup

2026-8-17 13:51
22

答案 / KEY:yb6tcr


0. 结论速览

程序要求输入 6 个字符的 key,用来"还原"一段被加密的 Python 字节码。
真正的逻辑不在 PyInstaller 打包的用户脚本里,而是被塞进了
python313.dll 内部的 frozen os 模块尾部——解释器启动 import os 时就会执行。

破解点:程序自己把答案打印在提示里。成功时必然输出 恭喜成功!,
而这段字符串是 succ XOR key(key 循环,周期 6,密文 15 字节),
所以 key 可以直接反解,并且能被交叉验证 3 次(15 = 6+6+3)。


1. 初步识别

题目.exe: PE32+ console, x86-64, 7 sections, 7,438,179 bytes

搜到 PyInstaller / MEIPASS 特征 → PyInstaller onefile 打包,python313.dll(Python 3.13)。

2. 解包 PyInstaller(pyiextract.py)

定位尾部 cookie MEI\x0c\x0b\x0a\x0b\x0e(0x717f0b),算出归档基址 0x54600,解析 TOC 得到 21 项:

类型 名称
s pyiboot01_bootstrap / pyi_rth_inspect / main
m struct / pyimod01~04
b python313.dll / libcrypto-3.dll / *.pyd / base_library.zip
z PYZ.pyz

坑点 1:main 反汇编出来只有

import sys
sys.exit(0)

PYZ 里 112 个模块全是标准库,pyimod0x、runtime hook、base_library.zip 也都干净。
但程序实际会不停打印中文提示 —— 说明真正的代码另有藏身之处。

3. 找到藏匿点:frozen os 模块

在整个 exe 里搜 GBK / UTF-8 / UTF-16 的 提示、恭喜成功 全部落空(说明字符串是运行时算出来的),
但输出的 \r\r\n 和 GBK 编码明显是 Python 的 print 干的。

于是逐文件搜可见片段 my_function,命中 python313.dll 偏移 0x4dace9。
其附近能看到 co_code、co_consts、print、utf-8 以及若干 16 字节乱码块 —— 典型的 marshal 常量表。

CPython 3.11+ 会把 os 等启动模块以 frozen module(marshal 字节流) 形式编进 python313.dll。
搜到 <frozen os> 在 0x4d144e,向前暴力扫描 0xe3/0x63(TYPE_CODE)并试 marshal.load:

START 0x4d00e0  len 47420   name=<module>  file=<frozen os>

marshal.loads 后遍历子 code object,末尾多出一个 my_function(line 1188) —— 正常 CPython 的 os.py 到 process_cpu_count(1173)就结束了。

出题人把挑战代码追加到了 os 模块末尾,重新 freeze 进 DLL。
所以 PyInstaller 的 main 是空壳,逻辑在 import os 阶段就跑完了。

4. 还原隐藏逻辑

dis 模块级字节码(line 1185~1229),还原出的源码见 hidden_code.py,要点:

x = 0
while x < 20:
    def my_function():
        print(...)                       # 占位,co_code / co_consts 是密文

    try:
        x += 1
        buffer = b''.join([... 8 段 16 字节 ...])
        w = b''.join((i ^ 85).to_bytes(1) for i in buffer)
        print(w.decode('utf-8'))         # 提示文本 = 密文 XOR 0x55

        key = input()
        if len(key) != 6: continue
        b_key = key.encode()

        c = b'\xecbmucryb6tcr*c\x03ucryb6tCr\x1eb'          # co_code 密文,26 字节
        buf = bytes(c[i] ^ b_key[i % 6] for i in range(len(c)))
        my = my_function.__code__.replace(co_code=buf)

        succ = bytes.fromhex('9fe39b91f5ee9feaa691e9ed96deb7') # co_consts 密文,15 字节
        buf = bytes(succ[i] ^ b_key[i % 6] for i in range(len(succ)))
        my = my.replace(co_consts=(None, buf.decode('utf-8')))

        my_function.__code__ = my
        my_function()
    except Exception:
        continue

while x < 20 + except: continue 就是"倒计时 / 自毁"的实现——最多 20 次机会。

5. 求解 key(已知明文攻击)

提示文本本身就把答案送上门了:目标函数是

def my_function():
    print("恭喜成功!")

"恭喜成功!" 的 UTF-8 编码正好 15 字节,与 succ 密文等长:

succ  : 9f e3 9b 91 f5 ee | 9f ea a6 91 e9 ed | 96 de b7
plain : e6 81 ad e5 96 9c | e6 88 90 e5 8a 9f | ef bc 81
xor   : 79 62 36 74 63 72 | 79 62 36 74 63 72 | 79 62 36
        y  b  6  t  c  r  |  ← 周期完全一致,三段互相验证

KEY = yb6tcr

反过来验证 co_code:

95 00 5b 01 00 00 00 00 00 00 00 00 53 01 35 01 00 00 00 00 00 00 20 00 67 00
RESUME 0
LOAD_GLOBAL 1 (print + NULL)
LOAD_CONST  1 ('恭喜成功!')
CALL 1
POP_TOP
RETURN_CONST 0 (None)

完全合法的 Python 3.13 字节码,与 DLL 中原始 my_function 的 co_code 逐字节一致。

6. 实机验证

$ printf 'yb6tcr\n' | ./题目.exe
提示,需要还原的代码
def my_function():
        print("恭喜成功!")
请输入key还原代码(请输入6个字符)
恭喜成功!

传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

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