首页
社区
课程
招聘
KCTF 2026第二题「巳时·绿光幽语」Writeup
发表于: 2026-8-25 18:16 223

KCTF 2026第二题「巳时·绿光幽语」Writeup

2026-8-25 18:16
223

一、题目信息

  • 题目文件题目.exe(7,438,179 字节)
  • 题目简介:故事讲述了金融分析师发现一段被拆成 16 片的硬件密钥,需要还原旧终端机上的分段加密文件。程序提示"请输入 key 还原代码(请输入 6 个字符)"。
  • 目标:求出序列号(6 字符的 key)
  • 最终答案yb6tcr

二、初步分析

file / PE 头检查可知 题目.exePyInstaller 打包的 Python 3.13 程序。PyInstaller 程序通常把 Python 解释器(python313.dll)和打包的 .pyc 一起内置。使用 pyinstxtractor.py 解包:

题目.exe_extracted/
├── py313_mod.dll        # 注意:被修改的 python313.dll(重命名)
├── base_library.zip
└── ...

关键点:py313_mod.dll被作者修改过的解释器。正常 PyInstaller 用的是原版 python313.dll,这里重命名为 py313_mod.dll,说明作者对解释器动了手脚。

三、修改后的解释器分析

3.1 字符串全部被加密

用 IDA 打开 py313_mod.dll(ImageBase 0x180000000),发现程序要打印的提示文本在文件中不是明文,而是被 XOR 加密的数据块:

  • 8 个加密提示字符串块位于 0x4dad10 - 0x4dadde,均 XOR 0x55(85)
  • 后面紧跟着一系列小数据:INT 85'utf-8'INT 6、块 9、('co_code',)INT 15、块 10、('co_consts',)

这说明作者在解释器层面(DLL 内部)把字符串 marshal 数据加密了,运行时由解释器自行解密,标准反编译工具直接看是乱码。

3.2 marshal 格式被修改

对比本机标准 Python 3.13.14 与 py313_mod.dll 的 marshal tag 编码:

对象 标准 3.13.14 作者 DLL
INT 0xe9 0x69
SMALL_TUPLE(1 字节 count) 0xa9 0x29
bytes(4 字节 len) 0xf3 0x73
SMALL_STRING(1 字节 len) 0xfa 0x7a
CODE 0xe3 0x63

规律:标准 tag = 作者 tag + 0x80FLAG_REF 位)。作者 DLL 的 marshal 变体大多不带 FLAG_REF 位。而 0x63 CODE tag 和 0x81/0xa4 qualname tag 则是作者完全自定义的。

好消息:标准 3.13.14 的 marshal.loads 可以解析所有作者 DLL 的无 FLAG 变体(0x290x730x690x7a 均可正常 loads),只有 code/qualname tag 是自定义的。

四、动态分析:加载作者解释器

IDA 静态分析该 DLL 流程繁琐且不稳定,改为动态加载:用 ctypes 加载 py313_mod.dll,调用它自己的 Py_InitializePyMarshal_ReadObjectFromString,让作者自己的解析器去解析 marshal 数据——它支持自定义 tag,并能自动 XOR 解密字符串。

load_marshal.py / probe.py / scan.py

一个坑:Py_Initialize 会立刻把解密后的提示文本打印 20 次(因为作者代码注入在 os 模块里,import os 时自动执行)。解决:加载 DLL 前把 fd 1/2 重定向到 devnull,初始化后再恢复。

4.1 作者代码注入位置

Py_Initializesys.modules 里找到自定义函数 my_function,它位于 <frozen os> 模块中(origin='frozen')。说明作者把自定义代码追加到了标准 os 模块的字节码末尾,import os 时即执行。

反汇编 os 模块主代码尾部(约 1185-1235 行)得到完整作者逻辑:

x = 0
while x < 20:
    def my_function(): print(666)      # 占位函数,co_consts = (None, 666)
    x += 1
    buffer = b''.join([8 个加密块])      # 拼接 8 段 XOR 0x55 的密文
    w = b''
    for i in buffer:
        b = i ^ 85
        w += b.to_bytes(1)
    print(w.decode('utf-8'))            # 打印解密后的提示
    key = input()
    if len(key) != 6:
        continue                        # 长度不对就重来
    b_key = key.encode()
    c = b'\xecbmucryb6tcr*c\x03ucryb6tCr\x1eb'      # 块 9(26 字节)
    buf = b''
    for i in range(len(c)):
        buf += (c[i] ^ b_key[i % len(b_key)]).to_bytes(1)
    my = my_function.__code__
    my = my.replace(co_code=buf)        # 块 9 解密结果 -> 新字节码
    buf = b''
    succ = b'\x9f\xe3\x9b\x91\xf5\xee\x9f\xea\xa6\x91\xe9\xed\x96\xde\xb7'  # 块 10(15 字节)
    for i in range(len(succ)):
        buf += (succ[i] ^ b_key[i % len(b_key)]).to_bytes(1)
    my = my.replace(co_consts=(None, buf.decode('utf-8')))  # 块 10 解密结果 -> consts[1]
    my_function.__code__ = my
    my_function()                       # 调用还原后的函数

4.2 提示文本

8 个加密块 XOR 0x55 拼接后是:

提示,需要还原的代码
def my_function():
    print("恭喜成功!")
请输入key还原代码(请输入6个字符)

即提示文本本身就明示了还原目标:让 my_function 打印 恭喜成功!

五、破解密钥:已知明文攻击

整体算法非常清晰:

  1. 块 9(26 字节)XOR key(UTF-8,循环重复)→ 还原成 my_function新字节码(co_code)
  2. 块 10(15 字节)XOR 同一 key → 还原成字符串,替换 co_consts[1]

co_consts[1] 最终要被 my_function() 打印出来。结合提示文本,"恭喜成功!" 就是要还原的目标明文!

"恭喜成功!" 的 UTF-8 编码(15 字节):

e6 81 ad e5 96 9c e6 88 90 e5 8a 9f ef bc 81

块 10 密文(15 字节):

9f e3 9b 91 f5 ee 9f ea a6 91 e9 ed 96 de b7

因为密钥是循环重复的(key[i % 6]),取前 6 字节做 XOR:

k[0] = 9f ^ e6 = 79 = 'y'
k[1] = e3 ^ 81 = 62 = 'b'
k[2] = 9b ^ ad = 36 = '6'
k[3] = 91 ^ e5 = 74 = 't'
k[4] = f5 ^ 96 = 63 = 'c'
k[5] = ee ^ 9c = 72 = 'r'

key = yb6tcr。其余 9 字节用同一 6 字节 key 循环验证,完全吻合。

六、验证

6.1 块 9 解密成合法字节码

9 ^ 'yb6tcr' =
95 00 5b 01 00 00 00 00 00 00 00 00 53 01 35 01 00 00 00 00 00 00 3a 10 28 16

用 Python dis 反汇编(与原始 my_function 占位代码结构一致):

RESUME                   0
LOAD_GLOBAL              1          (print)
LOAD_CONST               1
CALL                     1
POP_TOP
RETURN_CONST             0

即"打印 consts[1]"的合法代码。

6.2 块 10 解密成目标字符串

10 ^ 'yb6tcr' = e6 81 ad e5 96 9c e6 88 90 e5 8a 9f ef bc 81 = "恭喜成功!"

6.3 真实运行验证

a)加载作者 DLL,手动还原并调用 my_function(),实际打印 恭喜成功!

b)向原 题目.exe 管道输入 yb6tcr,程序输出:

提示,需要还原的代码
def my_function():
    print("恭喜成功!")
请输入key还原代码(请输入6个字符)
恭喜成功!

验证通过 ✅


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

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