首页
社区
课程
招聘
[原创]KCTF 2026 第二题 "绿光幽语" WriteUp
发表于: 2天前 94

[原创]KCTF 2026 第二题 "绿光幽语" WriteUp

2天前
94

目标文件:题目.exe(PyInstaller onefile 打包,x64,Python 3.13.1)

分析工具:GPT 5.6 + IDA Pro + 官方 CPython 3.13.1 解释器(marshal/dis 还原字节码)

出题人在简介里埋了三条技术线索,破解后可一一对应:

整个解题过程遵循"由外到内、由静到动"的顺序:外壳 → 解包 → 诱饵识别 → 动态运行 → 定位真实逻辑 → 还原算法 → 恢复密钥。

在 IDA 中对入口相关函数做 F5,很快能看到大量 Py_* C-API 被运行时动态解析。例如 sub_140001C80(Python DLL 符号加载器)的 F5 伪代码节选:

解读:这段一次性解析了 50+ 个 Py_* 符号并存入结构体偏移,是 PyInstaller 官方 bootloader 的标准行为pyi_pylib_load_python_dll / _pyi_pylib_set_runtime_paths 那套)。没有任何自定义算法。

在 IDA 里搜索 flag / KCTF / serial

结论 1:原生 EXE 层是标准 PyInstaller 壳,flag 逻辑不在原生代码里。 这正是"工牌上的空白照片"——外壳是障眼法。

用脚本解析 PyInstaller 的 CArchive(parse_toc.py / full_toc.py),定位文件尾部的 MEI\x0c\x0b\x0a\x0b\x0e cookie,读出 21 个 TOC 条目:

关键观察:

从 CArchive 直接抽出 main 条目并 marshal.loads 反汇编:

main 只做了一件事:import sys; sys.exit(0) 这就是"化为灰烬的终端机"——入口脚本是空壳。

同时把 PYZ.pyz 里的 112 个模块全部与官方 3.13.1/3.13.7 源码做字节码级 diff,扣除"点版本差异 + -OO 去 docstring"的噪声后,没有任何一个标准库模块被真正篡改

结论 2:Python 脚本层(main + PYZ + base_library)全是干净的标准库和诱饵,真实逻辑既不在原生层,也不在常规 pyc 里。

至此静态分析陷入僵局——这是本题设计最"阴"的地方。破局点必须来自动态运行。

既然 main 号称直接退出,那就实际跑一遍看看:

GBK 解码后惊现真实交互逻辑

重大发现:程序明明"应该"在 mainsys.exit(0),却打印出了 keygen 提示!说明真正的代码在 main 之前就执行了。

在 Python 启动流程中,能早于用户入口脚本运行的,只有解释器自带的 frozen module(如 osimportlib._bootstrap 等,它们被冻结进解释器,import 时无需读磁盘)。而这些 frozen module 就编译进了 python313.dll——回想第 2 节里那个体积异常的 DLL。

对提示中出现的函数名 my_function 在所有文件做裸字节搜索

命中 python313.dll\xda\x0b 是 marshal 的 SHORT_ASCII_INTERNED(len=11) + my_function,紧跟着一串 s\x10\x00\x00\x00(marshal TYPE_STRING, len=0x10=16 字节)——正是"十六个碎片"

向前扫描定位到 marshal code object 的起点,用官方 3.13.1 解释器成功 marshal.loads 出一个 co_name='<module>'、内含 my_function 的完整模块——这是一个被出题人魔改过的 frozen os 模块(起始偏移 0x4d00e0)。

对应线索:"工业路由器用的就是这套逻辑"——把校验逻辑冻结进运行时 DLL,是嵌入式/路由器固件里常见的反逆向手法。

递归遍历该 code object 的 co_consts,找到出题人注入的载荷:

对魔改 os 模块的模块级字节码(第 1185–1226 行)反汇编,逐段翻译成等价 Python。核心逻辑如下:

这就是"古典密码 + 时间戳算法"里最简单的单表替换层:XOR 85

对应 F5 之外的字节码证据(行 1213):

校验原理(精妙之处):程序不做任何 if key == ... 的显式比较。密钥正确时,c 被异或还原成一段合法的 CPython 字节码(正好是 print("恭喜成功!")),my_function() 正常输出;密钥错误时还原出非法字节码,调用即抛异常/崩溃——这正是简介里"三段必须同步解密,否则自毁"。

无需爆破。第 5.4 节告诉我们:解密后的 succ 必然是成功文案。而提示里已经明明白白写着成功文案是 恭喜成功!,其 UTF-8 编码恰好 15 字节,与 succ 长度完全一致:

输出结尾:

✅ 注册码 yb6tcr 经真机验证正确。

综合难度:中上。 对只会"pyinstxtractor + 反编译 pyc"流水线的选手是硬门槛;对有动态调试意识、了解 frozen module 机制的老手是中等。

一句话总结:外壳干净、诱饵到位、逻辑藏得妙,是一道"考动态意识 + frozen module 冷知识"的好题;核心思路值得沉淀成模板。

Flag / 注册码 / 序列号:yb6tcr

简介中的隐喻 对应的真实技术点
"文件分段加密:古典密码、时间戳算法、硬件密钥" 重复密钥异或(Vigenère 型古典密码)+ 单字节 XOR + 密文分段
"硬件密钥被拆成十六个碎片,分散在十六个存储扇区" 提示明文被拆成 8×16=128 字节碎片,藏在重编译的 python313.dll
"三年前 KCTF 上那款工业路由器,用的就是这套逻辑" 自定义/固化 Python 运行时(把逻辑冻结进 DLL 的 frozen module),路由器固件常见套路
"终端机化为灰烬" / "空白照片的工牌" main.pyc 是空壳诱饵(sys.exit(0)),真正的"人"(逻辑)藏在别处
常量索引 内容 用途
const[145] 8 个 16 字节 bytes 元组(共 128 字节) 提示明文密文("16 碎片")
const[146] 85 单字节 XOR key
const[147] 'utf-8' 解码
const[148] 6 密钥长度校验
const[149] b'\xecbmucryb6tcr*c\x03ucryb6tCr\x1eb' my_function 的加密 co_code
const[151] b'\x9f\xe3\x9b\x91\xf5\xee\x9f\xea\xa6\x91\xe9\xed\x96\xde\xb7'(15 字节) 加密的成功字符串
const[143] <code my_function> 待还原的函数(占位版)
__int64 __fastcall sub_140001C80(__int64 a1)
{
  // a1 是一张"函数指针表",逐个用 GetProcAddress 从 python 主 DLL 拿地址
  Py_DecRef = GetProcAddress(*(HMODULE *)a1, "Py_DecRef");        // 0x140001c93
  *(_QWORD *)(a1 + 16) = Py_DecRef;
  if ( !Py_DecRef ) { /* 报错 "Failed to import symbol %hs from Python DLL." */ }

  Py_DecodeLocale             = GetProcAddress(*(HMODULE*)a1, "Py_DecodeLocale");
  Py_Finalize                 = GetProcAddress(*(HMODULE*)a1, "Py_Finalize");
  ...
  PyMarshal_ReadObjectFromString = GetProcAddress(*(HMODULE*)a1, "PyMarshal_ReadObjectFromString"); // 反序列化 pyc
  PyEval_EvalCode                = GetProcAddress(*(HMODULE*)a1, "PyEval_EvalCode");                 // 执行 code object
  PyImport_ExecCodeModule        = GetProcAddress(*(HMODULE*)a1, "PyImport_ExecCodeModule");
  PyRun_SimpleStringFlags        = GetProcAddress(*(HMODULE*)a1, "PyRun_SimpleStringFlags");
  ...
  return 0;
}
typ  flag      dlen      ulen   name
m    1          235       289   struct
m    1         2831      4914   pyimod01_archive
m    1        13630     31786   pyimod02_importers
s    1         1027      1881   pyiboot01_bootstrap
s    1         1470      2779   pyi_rth_inspect
s    1          121       159   main            ← 入口脚本,仅 159 字节
b    1       ...                 _decimal.pyd / _hashlib.pyd / _socket.pyd ...
b    1      2676210   7127552   python313.dll   ← 注意这个体积
z    0      1372950   1372950   PYZ.pyz         ← 112 个标准库模块

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

最后于 2天前 被BenBenWen编辑 ,原因:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回