2026KCTF|第二题《巳时·绿光幽语》设计思路及解析

admin 2026-08-14 08:41:22 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 2026KCTF第二题《巳时·绿光幽语》是一道逆向分析题,核心设计是将校验逻辑隐藏在重新编译的python313.dll中,并植入frozenos模块,使代码在main.py执行前触发。解题需通过对比二进制文件定位被修改的DLL,分析字符串找到植入的frozen模块,并利用异或算法还原字节码。题目考验选手对程序运行链路和Python字节码的敏感度,而非单纯依赖工具。 综合评分: 88 文章分类: 逆向分析,二进制安全,CTF,安全工具,实战经验


cover_image

2026 KCTF | 第二题《巳时·绿光幽语》设计思路及解析

2026 KCTF 2026 KCTF

看雪学苑

2026年8月12日 18:06 上海

在小说阅读器读本章

去阅读

8月10日12:00,2026 KCTF攻防对抗赛正式开赛!本届赛事沿用多维度积分体系,难度、火力、精致度三重积分同步核算,攻防双向计分机制兼顾趣味性与竞技性。

*签到题《辰时·钟鸣破晓》赛事周期内持续开放,比赛全程都可以提交 flag 获取对应攻击积分。

值得关注:

本题的解题数据也折射出 AI 对 CTF 竞赛正在产生的结构性影响。

截止今天中午12点,第二题《巳时·绿光幽语》关卡已关闭。本题共有119支战队成功提交flag,『RST』战队“一血”仅用时9分58秒。这样的破解速度和覆盖面,在过去同等难度的 Reverse 题中并不常见。

去年我们更多看到的是少数选手开始尝试借助大模型辅助分析,可以说还是“由点到面”的扩散阶段;而到了今年,变化已经明显不只是“更多人开始使用 AI”,而是AI 正在实质性改变逆向题的解题效率、技术门槛和竞赛节奏。过去需要选手花较长时间完成的 PyInstaller 解包、Python 字节码分析、异常代码定位、算法还原等工作,如今在合适的工具链和提示引导下,AI 已经能够在很短时间内协助完成其中相当一部分。

对 CTF 出题者而言,这也意味着一个新的命题正在出现:当 AI 可以快速完成传统意义上的静态分析和模式识别后,未来的赛题究竟应该考什么,才能继续有效区分选手真正的分析、判断与创新能力?

我们带着这些问题和好奇

一起来看看本题的设计思路&解题思路

KCTF评委团队点评:

本题的亮点不在于复杂的密码算法,而在于对Python 打包与运行机制的巧妙利用。

题目以 PyInstaller 作为表层入口,却将真正的校验逻辑隐藏在重新编译的 python313.dll 中,并进一步植入 frozen os 模块,使代码在 main.py 执行之前即被触发,有效避开了常规“解包—反编译主脚本”的分析路径。

在最终校验阶段,题目没有采用直接比较字符串的方式,而是将用户输入作为循环 XOR 密钥,动态还原 my_function 的字节码与常量;而题目给出的目标输出又恰好构成已知明文,为选手留下了清晰但不直白的突破口。

整体来看,这是一道藏得深、算法简、思路巧的 Reverse 题,真正考验的是选手对程序运行链路、Python 字节码以及异常现象的敏感度,而非单纯依赖工具完成机械式反编译。

一起来看看本题的设计思路&解题思路

一、

设计思路

出题战队:one team

战队成员ID:穿甲葡萄籽

题目需要6个字符串,字符范围在小写大写数字中组成。成功后控制台会输出“恭喜成功!”,如果输出其他乱码字符或程序崩溃不算成功。

文件pyinstaller打包。将代码编写到os.py中,通过python源码编译把os.py编译到python.dll中。

用户可从python.dll中还原出os.py的代码。

根据提示的代码可获取到“print(“恭喜成功!”)”编译后的python字节马。

根据os.py中的分组异或代码可分析出异或的key。

x``=``0

while``x<``20``:

def``my_function():

print``(``666``)

try``:

x``+``=``1

buffer``=``b''.join([

b``'\xB3\xDA\xC5\xB2\xF1\xEF\xBA\xE9\xD9\xBC\xC9\xD5\xBD\xF3\xD4\xBD'``,

b``'\xEA\xCD\xB0\xDB\xCA\xB2\xCF\xD1\xB1\xEE\xF6\xB2\xF5\xD4\x58\x5F'``,

b``'\x31\x30\x33\x75\x38\x2C\x0A\x33\x20\x3B\x36\x21\x3C\x3A\x3B\x7D'``,

b``'\x7C\x6F\x58\x5F\x5C\x25\x27\x3C\x3B\x21\x7D\x77\xB3\xD4\xF8\xB0'``,

b``'\xC3\xC9\xB3\xDD\xC5\xB0\xDF\xCA\xBA\xE9\xD4\x77\x7C\x58\x5F\xBD'``,

b``'\xFA\xE2\xBD\xEB\xC6\xB0\xD0\xF0\x3E\x30\x2C\xBD\xEA\xCD\xB0\xDB'``,

b``'\xCA\xB1\xEE\xF6\xB2\xF5\xD4\xBA\xE9\xDD\xBD\xFA\xE2\xBD\xEB\xC6'``,

b``'\xB0\xD0\xF0\x63\xB1\xED\xFF\xB0\xF8\xC2\xB2\xF9\xF3\xBA\xE9\xDC'``])

w``=``b""

for``i``in``buffer``:

b``=``i^``0x55

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'

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)

buf``=``b""

succ``=``b``'\x9f\xe3\x9b\x91\xf5\xee\x9f\xea\xa6\x91\xe9\xed\x96\xde\xb7'

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"``)))

my_function.__code__``=``my

my_function()

break

except``Exception as e:

continue

二、

赛题解析

该解题思路由论坛会员【yuzhouheike】提供

样本信息

题目.rar &nbsp;&nbsp;7,255,261&nbsp;bytes&nbsp; sha256 ce817ebd7499be0ba081bbe1572dced64ac1b6a9391fc2955237c189860fb531
题目.exe &nbsp;&nbsp;7,438,179&nbsp;bytes&nbsp; sha256 f6b192f9ee87c4ad2bc8681291ca428cf8e98fec26fa5d7b82c6e791a7a90ead

PE32+ console x86-64,编译时间 2026-03-06。7 MB 的体积里,真正的题眼只有几百字节,而且藏在一个几乎不会有人去看的地方。

整题的结构可以概括成一句话:把逻辑塞进被重新编译过的 CPython 解释器里,而不是塞进脚本。

1. 表层:PyInstaller onefile

rabin2 -S 显示节区只到 0x54600,后面 7 MB 全是 overlay。overlay 开头是 78 da(zlib),文件末尾是 python313.dll + 一串 0 —— 典型的 PyInstaller cookie 结构:

struct&nbsp;COOKIE {
&nbsp; &nbsp; char &nbsp;magic[8]; &nbsp; &nbsp; &nbsp;&nbsp;// MEI\014\013\012\013\016
uint32&nbsp;pkgLen; &nbsp; &nbsp; &nbsp; &nbsp;// 7092579
uint32&nbsp;toc; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 7091627
uint32&nbsp;tocLen; &nbsp; &nbsp; &nbsp; &nbsp;// 864
uint32&nbsp;pyvers; &nbsp; &nbsp; &nbsp; &nbsp;// 313
&nbsp; &nbsp; char &nbsp;pylibname[64]; &nbsp;// "python313.dll"
};

cookie 位于 0x717f0bpkgStart = cookiePos + 88 - pkgLen = 0x54600,正好接在最后一个节区之后。

TOC 只有 21 项:

m &nbsp;struct &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; s &nbsp;pyiboot01_bootstrap &nbsp; &nbsp;b&nbsp; _decimal.pydb&nbsp; python313.dll
m &nbsp;pyimod01_archive &nbsp; &nbsp; &nbsp; s &nbsp;pyi_rth_inspect &nbsp; &nbsp; &nbsp; &nbsp;b&nbsp; _hashlib.pydb&nbsp; select.pyd
m &nbsp;pyimod02_importers &nbsp; &nbsp; s &nbsp;mainb&nbsp; _lzma.pydb&nbsp; unicodedata.pyd
m &nbsp;pyimod03_ctypes &nbsp; &nbsp; &nbsp; &nbsp;b&nbsp; VCRUNTIME140.dllb&nbsp; _socket.pyd&nbsp; &nbsp; &nbsp; &nbsp;o &nbsp;pyi-contents-directory _internal
m &nbsp;pyimod04_pywin32 &nbsp; &nbsp; &nbsp;&nbsp;b&nbsp; _bz2.pydb&nbsp; base_library.zip&nbsp; z &nbsp;PYZ.pyz
b&nbsp; libcrypto-3.dll

入口脚本 main 只有 159 字节,unmarshal 后 co_names = ('sys','exit')co_consts = (0,&nbsp;None),还原出来就是:

import&nbsp;sys
sys.exit(0)

PYZ.pyz 里 112 个模块全是标准库,base_library.zip 154 项也全是标准库。Python 层什么都没干。

到这里第一个岔路口出现了:很容易以为解错方向或者拿错文件。

#

2. 排除法:证明原生代码没被动过

与其在 190 KB 的 bootloader 里大海捞针,不如直接和上游二进制对比。

PyInstaller 的 Windows wheel 里带着预编译的 run.exe,从 PyPI 把 6.11 ~ 6.22 全撸下来逐个比 .text

pyi-6.18.0 &nbsp; .text=186880&nbsp; match=35561&nbsp; &nbsp; exact=False
pyi-6.19.0 &nbsp; .text=186880&nbsp; match=35561&nbsp; &nbsp; exact=False
pyi-6.20.0 &nbsp; .text=186880&nbsp; match=186880&nbsp; &nbsp;exact=True&nbsp; &nbsp; &nbsp;← 逐字节相同
pyi-6.21.0 &nbsp; .text=187392&nbsp; match=6873&nbsp; &nbsp; &nbsp;exact=False

命中 PyInstaller 6.20.0

再逐段 diff:

.rsrc 里只有 7 个图标 + 1 个 manifest,其中 256×256 的那张 PNG(37019 字节)与 wheel 里 PyInstaller/bootloader/images/icon-console.ico 内嵌的 PNG 完全一致 —— PyInstaller 默认图标,没夹带。

再验证归档本身有没有藏东西:把 21 个条目的 [entryPos, entryPos+csz) 加上 TOC 区间排序求并集,从 0 到 0x6c390b 无任何空洞

结论:bootloader、资源、归档布局全部干净。剩下唯一可能被做手脚的,就是归档里那些二进制。

3. 定位:被换掉的 python313.dll

#

对归档内所有 PE 做体检(节区末尾 vs 文件大小):

_bz2.pyd &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;overlay=14168&nbsp; &nbsp;ts=2026-04-07
_decimal.pyd &nbsp; &nbsp; &nbsp;overlay=14168&nbsp; &nbsp;ts=2026-04-07
_hashlib.pyd &nbsp; &nbsp; &nbsp;overlay=14168&nbsp; &nbsp;ts=2026-04-07
_socket.pyd &nbsp; &nbsp; &nbsp;&nbsp;overlay=14168&nbsp; &nbsp;ts=2026-04-07
select.pyd &nbsp; &nbsp; &nbsp; &nbsp;overlay=14168&nbsp; &nbsp;ts=2026-04-07
unicodedata.pyd &nbsp;&nbsp;overlay=14168&nbsp; &nbsp;ts=2026-04-07
libcrypto-3.dll &nbsp;&nbsp;overlay=14192&nbsp; &nbsp;ts=2026-02-13
python313.dll &nbsp; &nbsp;&nbsp;overlay=0&nbsp; &nbsp;ts=2026-05-19&nbsp; &nbsp; ← 唯一没有 Authenticode 签名的

那 14168 字节的 overlay 是 Authenticode 数字签名。同一套 Python 发行版里所有 .pyd 都签了名,唯独主 DLL 没有 —— 这是最硬的一个信号。

再和官方比大小:

官方&nbsp;python-3.13.1-embed-amd64&nbsp;里的&nbsp;python313.dll :6,093,816
题目里的&nbsp;python313.dll &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;:7,127,552&nbsp; &nbsp; &nbsp;(+1&nbsp;MB)

字符串 diff 直接把作者的构建环境暴露了:

H:\样本分析\789456\Python-3.13.1\PCbuild\amd64\python313.pdb
H:\样本分析\789456\Python-3.13.1\Objects\codeobject.c
H:\样本分析\789456\Python-3.13.1\Python\import.c
...
[MSC v.1941 64 bit (AMD64)]

作者从源码自己编了一份 CPython 3.13.1(非 PGO,所以比官方大 1 MB),题目逻辑在解释器里。

4. 题眼:植入 frozen os 模块

自编 CPython 的改动无法用字节 diff 定位(整个二进制布局都变了),得换思路 —— 从字符串里找不属于 CPython 的东西。过滤掉双方共有的字符串后,几个刺眼的残片浮出来:

my_function
bmucryb6tcr*c
co_consts)

co_consts) 这种带右括号的形态,是 marshal 里 )\x01\xda\tco_consts(长度 1 的元组 + interned 短字符串)—— 说明这附近是一段被 marshal 进 DLL 的 code object

Python 3.13 的 frozen 模块在 python313.dll 里就是一段段裸 marshal 数据,一个挨一个排布。

顺序 unmarshal 可以把整张表走出来:

0x4bc520 &nbsp;<frozenimportlib._bootstrap>&nbsp; &nbsp;47420 → 56860
0x4ca340 &nbsp;<frozenimportlib.machinery>&nbsp; &nbsp; &nbsp;1069
0x4ca770 &nbsp;<frozenposixpath>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 17899
0x4ced60 &nbsp;<frozen__phello__.ham>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;104
0x4cedd0 &nbsp;<frozen_sitebuiltins>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;4872
0x4d00e0 &nbsp;<frozenos>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;47420 &nbsp; &nbsp; &nbsp;← 就是它
0x4dba20 &nbsp;<frozenimportlib.util>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;11840
0x4de860 &nbsp;<frozen__phello__>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;332

把 <frozen os> 反出来,函数列表最后多了一个 CPython 里根本不存在的 my_function

[...,&nbsp;'add_dll_directory',&nbsp;'process_cpu_count',&nbsp;'my_function']

选 os 下手很讲究:解释器启动阶段必然 import os,所以模块级代码一定会跑,main.py 干不干活完全无所谓。

4.1 被注入的模块级代码

反汇编 <frozen os> 模块级字节码的尾部(dis 需要用真正的 CPython 3.13,否则 opcode 表对不上),还原出来是:

x =&nbsp;0
while&nbsp;x <&nbsp;20: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 最多 20 次机会
def&nbsp;my_function():
print(666)

&nbsp; &nbsp; x +=&nbsp;1
&nbsp; &nbsp; buffer =&nbsp;b''.join([<8&nbsp;个&nbsp;16&nbsp;字节的密文块>])
&nbsp; &nbsp; w =&nbsp;b''
for&nbsp;i&nbsp;in&nbsp;buffer:
&nbsp; &nbsp; &nbsp; &nbsp; w += (i ^&nbsp;85).to_bytes(1)
print(w.decode('utf-8')) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 打印提示

&nbsp; &nbsp; key =&nbsp;input()
if&nbsp;len(key) !=&nbsp;6: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 长度必须是 6
continue
&nbsp; &nbsp; b_key = key.encode()

&nbsp; &nbsp; c =&nbsp;b'\xecbmucryb6tcr*c\x03ucryb6tCr\x1eb'&nbsp;&nbsp;# 26 字节
&nbsp; &nbsp; buf =&nbsp;b''
for&nbsp;i&nbsp;in&nbsp;range(len(c)):
&nbsp; &nbsp; &nbsp; &nbsp; buf += (c[i] ^ b_key[i %&nbsp;len(b_key)]).to_bytes(1)

&nbsp; &nbsp; my = my_function.__code__
&nbsp; &nbsp; my = my.replace(co_code=buf) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# ← 用解密结果当新字节码

&nbsp; &nbsp; succ =&nbsp;b'\x9f\xe3\x9b\x91\xf5\xee\x9f\xea\xa6\x91\xe9\xed\x96\xde\xb7'&nbsp;&nbsp;# 15 字节
&nbsp; &nbsp; buf =&nbsp;b''
for&nbsp;i&nbsp;in&nbsp;range(len(succ)):
&nbsp; &nbsp; &nbsp; &nbsp; buf += (succ[i] ^ b_key[i %&nbsp;len(b_key)]).to_bytes(1)

&nbsp; &nbsp; my = my.replace(co_consts=(None, buf.decode('utf-8')))
&nbsp; &nbsp; my_function.__code__ = my &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 把函数整个换掉
&nbsp; &nbsp; my_function()

对应的模块常量:

先把提示解出来(XOR 85):

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

题目把要求说得很清楚了:找到 6 字符的 key,让 my_function 被还原成能打印「恭喜成功!」的样子。

5. 已知明文攻击

重复密钥异或本身不难,难点是”正确的明文长什么样”。这里作者留了一个致命的巧合:

>>>len(my_function.__code__.co_code)
26
>>>len(c)
26

密文 c 和 my_function 原始字节码等长。 而 co_consts 是被单独替换的((None, 解密出的字符串)),也就是说 LOAD_CONST 1 这条指令本身不用变,只是它取到的常量从 666 变成了那句中文 —— 所以还原目标就是原始字节码本身,一个字节都不用改

my_function 原始字节码:

95&nbsp;00&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; RESUME &nbsp; &nbsp; &nbsp;&nbsp;0
5b&nbsp;01&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp; &nbsp; &nbsp; &nbsp; LOAD_GLOBAL &nbsp;1&nbsp;(print + NULL)
53&nbsp;01&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; LOAD_CONST &nbsp;&nbsp;1&nbsp;(666)
35&nbsp;01&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; CALL &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;1
20&nbsp;00&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; POP_TOP
67&nbsp;00&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; RETURN_CONST&nbsp;0&nbsp;(None)

直接异或:

c &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ec&nbsp;62&nbsp;6d&nbsp;75&nbsp;63&nbsp;72&nbsp;79&nbsp;62&nbsp;36&nbsp;74&nbsp;63&nbsp;72&nbsp;2a&nbsp;63&nbsp;03&nbsp;75&nbsp;63&nbsp;72&nbsp;79&nbsp;62&nbsp;36&nbsp;74&nbsp;43&nbsp;72&nbsp;1e&nbsp;62
co_code &nbsp; &nbsp; &nbsp;95&nbsp;00&nbsp;5b&nbsp;01&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;53&nbsp;01&nbsp;35&nbsp;01&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;00&nbsp;20&nbsp;00&nbsp;67&nbsp;00
------------ XOR ----------------------------------------------------------------------
79&nbsp;62&nbsp;36&nbsp;74&nbsp;63&nbsp;72&nbsp;79&nbsp;62&nbsp;36&nbsp;74&nbsp;63&nbsp;72&nbsp;79&nbsp;62&nbsp;36&nbsp;74&nbsp;63&nbsp;72&nbsp;79&nbsp;62&nbsp;36&nbsp;74&nbsp;63&nbsp;72&nbsp;79&nbsp;62
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;y &nbsp;b6&nbsp; t &nbsp;c &nbsp;r &nbsp;y &nbsp;b6&nbsp; t &nbsp;c &nbsp;r &nbsp;y &nbsp;b6&nbsp; t &nbsp;c &nbsp;r &nbsp;y &nbsp;b6&nbsp; t &nbsp;c &nbsp;r &nbsp;y &nbsp;b

完美的 6 字节周期,key = yb6tcr,白送。

双向验证

#

用这个 key 解另一段密文 succ

succ ^ yb6tcr =&nbsp;'恭喜成功!'

正好就是提示里承诺的那句输出。两条独立约束(字节码必须合法且等于原码、成功串必须是合法 UTF-8 且内容对得上)同时满足,密钥唯一。

#

6. 一键 solver

solve_q2.py:从原始 exe 一路自动解到 key,全程静态,不需要运行样本、不需要 Windows

python3.13 solve_q2.py 题目.exe
[*] PyInstaller archive: python=313&nbsp;lib=python313.dll entries@0x6c35ab
[*] python313.dll:&nbsp;7127552&nbsp;bytes
[*] <frozen os> @&nbsp;0x4d00e0

=== 程序提示(8×16&nbsp;字节&nbsp;XOR&nbsp;85)===
提示,需要还原的代码
def my_function():
&nbsp; &nbsp; print("恭喜成功!")
请输入key还原代码(请输入6个字符)
=== 恢复的&nbsp;key(周期&nbsp;6)===
yb6tcr
=== 校验 ===
new&nbsp;co_code == 原始 co_code :&nbsp;True
&nbsp; succ ^&nbsp;key&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;'恭喜成功!'

[+] 注册码 = yb6tcr

脚本做四件事:解 PyInstaller CArchive → 取出 python313.dll → 用 marshal 里 \xda\x0b<frozen os> 这个 interned 字符串定位、回扫找 code object 头 → 取常量做已知明文异或并自动求周期。

注意必须用 CPython 3.13 跑:3.14 的 marshal 虽然能把 code object 读出来(co_names/co_consts 是对的),但 dis 的 opcode 表已经变了,反汇编结果全是错的。分析过程中如果用 3.14 看 main.pyc,会看到 RETURN_GENERATOR 之类完全不着调的指令。

7. 答案

yb6tcr

提交返回:

{ "code":&nbsp;"0",&nbsp;"message":&nbsp;"哟, 不错哦,答对了!!"&nbsp;}

#

8. 小结

这题的难度不在密码学(重复密钥异或 + 已知明文,两行代码的事),而在信息藏得够深,且前面铺了一条完整的死路

  • PyInstaller 是幌子。

    入口脚本 import sys; sys.exit(0),PYZ 和 base_library 全是标准库。老老实实用 pyinstxtractor + 反编译走到底,会得到”这文件什么都没有”的结论。

  • bootloader 也是幌子。

    与 PyInstaller 6.20.0 官方 run.exe 的 .text 逐字节相同。这一步反而是最有价值的——用上游二进制做对照,能把 190 KB 的搜索空间一次性清零,比在 IDA 里瞎翻高效得多。

  • 真正的载体是被重编的解释器。

    判定依据有三条,任意一条都够:主 DLL 缺 Authenticode 签名而同套 .pyd 都有签名、体积比官方大 1 MB、字符串里带着 H:\样本分析\789456\Python-3.13.1\ 的构建路径。

  • 落点是 frozen os

    选 os 是因为解释器启动必定 import 它,天然拿到执行权,完全绕开了脚本层。

  • 自修改字节码。

    用 code.replace(co_code=...) + func.__code__ = ... 在运行期换掉函数体,动态调试时看到的和静态反编译看到的不是一个东西。

而作者留下的破绽也很干净:替换后的字节码和原始字节码等长且完全相同(因为常量是单独换的)。这等于把明文直接放在了旁边,重复密钥异或在已知明文面前一击即溃。

题面里那句”文件分段加密:古典密码、时间戳算法、硬件密钥”和”十六个碎片分散在十六个存储扇区”纯属剧情包装 —— 实际只有一层重复密钥异或,和 8 个 16 字节的提示文本块。

球分享

球点赞

球在看


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 2026 KCTF 2026 KCTF《2026 KCTF | 第二题《巳时·绿光幽语》设计思路及解析》

    提示词的本质是? 网络安全文章

    提示词的本质是?

    文章总结: 本文记录红队成员在缺乏C2源码时,通过逆向分析Vshell前端及通信协议,借助AI工具反推后端的过程。核心难点在于WebSocket交互命令调试,最
    评论:0   参与:  0