写给谁: 刚接触逆向 / CTF 的同学,自己也学习一波。
阅读方式: 少黑话;出现专业词会先用大白话解释。
目标: 不依赖「死记 IDA 地址」,也能顺着思路得到 key,并理解「凭什么」。
AI: grok 4.5
若平台要求 flag{...} 格式,可提交:
程序本身只校验 6 字符 yb6tcr,是否再包一层以平台说明为准。
程序提示:有一段「被藏起来的代码」,需要你找到一个 6 字符钥匙(key) 正确还原,才会成功。
一句话:
找出加密方式 + 算出钥匙。
不是改程序、也不是攻击他人电脑,只是分析「程序如何检查答案」。
在工程根目录运行:
正常编码下大致显示:
含义:
这是 Windows 控制台编码问题,不是样本损坏。可先执行:
936 表示按简体中文(GBK)显示。
读后面步骤前,先建立这 5 个直觉。
有的 exe 是 C/C++ 直接编译;也有的是 把 Python 程序打包进 exe(方便双击运行)。本题属于后者,打包工具是 PyInstaller。
生活类比:同一把钥匙拧一下变密文,再拧一下变明文。
本题用了两种 XOR:
key 为 yb6tcr 时,密文较长则钥匙循环使用:
每个位置:明文 = 密文 XOR key[对应位置]。
都是「文字如何变成字节」:
本题中:
记住:同是中文,编码不同字节就不同,分析时要对上编码。
一句话: marshal 是 Python 把「代码对象、常量、字符串、元组」写成一串字节 / 再读回来的官方格式。.pyc、frozen 模块、嵌在 DLL 数据区里的大段,都可能是 marshal。
本题只需会认几个「路牌」:
例子 A:73 10 00 00 00 + 16 字节
→ 一段长度 16 的串(本题「展示密文」每块)。
例子 B:29 08 + 8 次「73 10 + 16 字节」
→ 8 元组,每个元素 16 字节 → 8 段展示密文的摆放方式。
例子 C:73 0F 00 00 00 + 15 字节
→ 长度 15 的串 → 本题「用户密文」。
不必手写完整解析器;会「读标签拆货」即可。
在 exe 的字符串中可看到类似:
这些是 PyInstaller 特征;python313 表示 Python 3.13。
结论:
题目逻辑不在壳里,在壳释放出来的内容里。
初学者可先把 IDA 当「确认是 PyInstaller」的工具;解题主路径是 拆包 + 分析 DLL 数据。
推荐使用 pyinstxtractor-ng(可执行文件或 Python 脚本均可)。
优点:一键拆包、兼容较好、目录干净,适合初学者。
在工程根目录(与 题目.exe 同级)执行:
成功后生成类似目录:
其中约有二十来个文件,例如:main.pyc、PYZ.pyz、python313.dll 等。
做题与写证据时,请只用 pyinstxtractor-ng 生成的拆包目录。
不要把运行日志、分析笔记 txt 混进搜索目录,以免「在文件中搜索」出现假命中。
相对路径示例:
反编译 / 分析 main.pyc 后,逻辑大约只有:
即:一运行就正常退出,没有输入 key、没有打印中文提示。
但实际运行 exe 时明明有提示和输入。因此:
提示与校验 不在 main 里,而在 Python 启动过程中更早执行 的地方。
Python 启动会自动导入内置模块,其中包括 os。出题人把挑战逻辑嵌进 被修改过的 python313.dll 内 frozen 的 os 数据。一旦启动并 import os,题目就先跑起来——即使 main 是空的。
易错点: 只反编译 main,会以为「题是空的」。必须结合运行现象,去找真正打印中文的位置。
本节回答三个问题:
运行时屏幕上明文出现了英文标识符:
特点:
分析顺序:
不是先验「一定在 dll」,而是用运行提示反查文件。
本题中,标识符 my_function 未加密,可直接搜到。
注意区分:
其它题可能混淆名字或全串加密;本题没有,因此「搜运行时看见的英文函数名」有效。
在干净目录 题目.exe_extracted/ 中搜索 my_function:
本题实测:仅 python313.dll 命中 1 次。
这是 全目录检索 + 排除法,不是猜。
操作(Notepad++ 示例):
Notepad++ 只回答「在哪个文件」;打开后还要往下找 8 段密文与 15 字节(见下)。
在 python313.dll 中,my_function 约在文件偏移 0x4DACE9(十六进制编辑器 / 脚本查找可用)。
附近还能看到:print、input、b_key、co_code / co_consts、__code__、replace、Exception,以及 os 模块常见名字。
拼起来的逻辑大致是:
下列 VA(虚拟地址) 针对本题该 DLL:ImageBase = 0x180000000。
你本地若 ImageBase 一致,可用 G 跳转核对;不一致则以 字符串搜索 + 结构阅读 为准,不必死记地址。
不要在 .text 里找名为 my_function 的 C 函数。
本题逻辑是 marshal 嵌在 .rdata 的 Python 字节码与常量,不是原生导出函数。
此时还不知道 key
从 my_function 往下滚,会看到类似:
「展示密文块 0~7」是事后笔记名:结构来自 marshal 标签;内容是否为展示提示,要靠 XOR 实验与运行输出比对。
从第一段 16 字节(本题块 0 起址约 0x1804DC71C)抄出数据,试单字节 XOR。已知明文攻击后锁定钥匙为 0x55(过程见第十节):
按同样方法解全部 8 段并拼接,得到与运行窗口一致的整段提示 → 确认这就是「提示,需要.」密文。
不是 8 段展示密文里的某一段,而是 继续往下滚,过 co_code 等名字后:
二者地址、长度、密钥都不同,不要混。
文件偏移与 VA(本题该 .rdata 片区粗算):
最稳妥仍是:搜字符串 → 按结构往下读,不必死记。
这是数据区中的 Python marshal 串,不是 C 函数入口。静态做题以 字符串/字节模式定位 + 算法还原 即可,不必反编译整份 CPython。
业务逻辑(Python)主要在 .rdata 的 marshal 数据;.text 是解释器「厨师」。在 .rdata 看不到清晰的 xor al, 55h 很正常——算法靠 已知明文 + 与运行输出比对 断定。
算 key 只走线 B。
线 A 由程序自动解密并打印;你静态推出 0x55 是为了看懂题面从哪来;过关仍要线 B 的 yb6tcr。
对应 = 等长 + 按下标对齐 + 同一种 XOR 能互变 + 与程序行为一致。
DLL 中有 8 段 × 16 字节。对每个字节:
拼起来即完整 UTF-8 提示(与运行时一致)。
注意: 0x1804DC6E9 处的明文名字 my_function 不能当这段密文;反推 0x55 用的是 块 2。
展示文字用固定钥匙 0x55,不是你要猜的 6 字符 key。
依据:长度正好 15;贴在 co_code / b_key / input 旁;仍在挑战植入区。不是在整个 .rdata 里随便抓 15 字节。
因此 6 字符 key 为:
输入:yb6tcr
看到 恭喜成功! 即通过。
下面不是 100% 从编译器抠出的原文件,而是根据证据还原、与真题行为一致的逻辑:
在 DLL 中约 0x1804DC7DB 附近,数据里夹着可读碎片:
于是有人问:密钥都在文件里了,为何还要输入?
一句话:
程序只认你键入的 6 字节当钥匙。
文件里能搜到 yb6tcr = 出题不严谨带来的作弊通道,不是官方玩法。
正确用法: 用第十节 XOR 推出 key 之后,再回搜印证。
错误用法: 一打开 DLL 就搜 yb6tcr 当定位手段,跳过算法。
能。关键路径:确认 PyInstaller → pyinstxtractor-ng 拆包 → 在 python313.dll 找字符串与密文 → XOR 计算。IDA 用于核对,不是唯一路径。
Python 源码编译后的字节码文件,可用工具反编译成接近源码的形式。
题藏在启动必加载的 frozen os(在 python313.dll)里,main 是掩护。
程序只认 6 字符;平台若要求 flag{...} 再包装。以平台说明为准。
使用工具:CyberChef。
见 第十节「凭什么」。延伸:2_wp/6_零基础_如何算出key.md。
不要盲扫整段 .rdata。更负责的优先级:
本题能快,是因为有运行提示 + 可搜 my_function(捷径)。通用时不要把「只搜字符串」当唯一方法。
本题够用两个路牌:73+长度 = 串;29+个数 = 元组。见概念 5。
本题 IDA 路标(VA,ImageBase 0x180000000):
这题拆开后核心只有两步:
跟着 运行 → 拆包 → 定位 → 算异或 → 验证,每一步问「凭什么」,就是完整的 CTF 逆向。
Key:yb6tcr
| 项目 |
内容 |
| Key(6 个字符) |
yb6tcr |
| 样本 |
工程根目录下的 题目.exe |
| 验证方式 |
运行样本,输入 yb6tcr,应打印 「恭喜成功!」 |
| 用途 |
钥匙 |
作用 |
| 显示提示文字 |
固定值 0x55(十进制 85) |
把展示密文变成你看到的中文和源码 |
| 校验你的输入 |
你输入的 6 字符,循环使用 |
把 15 字节密文变成 UTF-8「恭喜成功!」 |
| 字节 |
人话 |
后面跟什么 |
73(s) |
字符串 / 字节串 |
4 字节长度(小端)+ 内容 |
29()) |
小元组 |
1 字节元素个数 + 各元素 |
da / z 等 |
短 ASCII 名字 |
名字字符(如 my_function) |
e3 / 63(c) |
code 对象 |
字段多,初学可先跳过 |
| 项目 |
值 |
| 文件名 |
题目.exe |
| 大小 |
约 7.4 MB(偏大,像带了运行环境) |
| MD5 |
d2fc58976cac7403cee3a8b8f2560081 |
| 类型 |
Windows 64 位 PE |
| 文件 |
是什么 |
对本题的意义 |
main.pyc |
程序入口字节码 |
几乎是空的(烟雾弹) |
PYZ.pyz |
打包的标准库等 |
无自定义题目模块 |
python313.dll |
Python 解释器 |
真题数据在这里 |
其它 .pyd / 运行库 |
依赖 |
一般不必深挖 |
| 路径 |
说明 |
题目.exe |
原始样本 |
题目.exe_extracted/ |
pyinstxtractor-ng 拆包结果(推荐) |
题目.exe_extracted/python313.dll |
真题所在 DLL |
| 内容 |
是否加密 |
说明 |
| 屏幕展示的整段提示 |
是(固定 XOR 0x55) |
密文在 DLL,运行时解密再打印 |
| 校验用 15 字节 |
是(6 字符 key 循环 XOR) |
验证 key |
名字 my_function |
否(本题) |
ASCII 明文,可直接搜 |
| 项目 |
值(本题) |
| 模块 |
python313.dll |
| 架构 |
64 位 |
| ImageBase |
0x180000000 |
| 阶段 |
可以搜 |
不要搜 |
| 刚定位 |
运行时见过的 my_function |
yb6tcr(key 尚未算出) |
| 算完 key 后 |
可用 yb6tcr 回搜印证 |
把结论当定位手段提前抄 |
| 名称 |
作用 |
钥匙 |
| 展示密文块 |
给玩家看的提示(8×16) |
固定 0x55 |
| 用户密文 |
校验你输入的 key(15 字节) |
6 字符循环 |
| 含义 |
VA |
如何自己定位 |
明文名 my_function |
0x1804DC6E9 |
先 Shift+F12 搜 |
| 展示密文块 0 |
0x1804DC71C |
my_function 下方 29 08 后第一段 73 10+16B |
| 展示密文块 2 |
0x1804DC746 |
第 3 段 16 字节(31 30 33 75...) |
| 用户密文 15 字节 |
0x1804DC7FF |
再往下 73 0F 00 00 00 后 15 字节 |
ASCII yb6tcr(泄漏) |
0x1804DC7DB |
仅算出 key 后回搜印证 |
| 做题核心区(约) |
0x1804DC693~0x1804DCBCA |
名字、两段密文、相关 names |
| 打开对象 |
你看到什么 |
题目.exe |
PyInstaller 壳:解压、加载 DLL、跑 main |
python313.dll |
解释器 + 本题真正植入点(frozen os 数据) |
| 线 |
做什么 |
与输入的 key 有关吗 |
| A 展示提示 |
8 段数据 XOR 固定 0x55 → 屏幕中文提示 |
无关 |
| B 检查 key |
15 字节 XOR 你输入的 6 字符 →「恭喜成功!」 |
有关(答案在这里) |
| 东西 |
作用 |
屏幕上的 def my_function(): print("恭喜成功!") |
题面——告诉你还原后长什么样 |
| 6 字符 key |
你要交的答案 |
| 位置 |
密文 |
明文 (UTF-8) |
XOR |
字符 |
| 0 |
0x9f |
0xe6 |
0x79 |
y |
| 1 |
0xe3 |
0x81 |
0x62 |
b |
| 2 |
0x9b |
0xad |
0x36 |
6 |
| 3 |
0x91 |
0xe5 |
0x74 |
t |
| 4 |
0xf5 |
0x96 |
0x63 |
c |
| 5 |
0xee |
0x9c |
0x72 |
r |
| 6 |
0x9f |
0xe6 |
0x79 |
y(开始重复) |
|
程序运行时(设计意图) |
你在二进制里搜到 yb6tcr |
| key 从哪来 |
input() 读键盘 |
数据布局泄漏出的 6 字符 |
| 干什么 |
XOR 15 字节,还原成功串 |
不是程序从该地址自动读密码 |
| 输错 |
解密失败,不进成功分支 |
与是否搜到无关 |
| VA |
内容 |
0x1804DC6E9 |
明文名字 my_function(路标) |
0x1804DC746 |
展示密文块 2(用来反推 0x55) |
| 知识点 |
本题中的样子 |
| PyInstaller |
exe 内嵌 Python;用 pyinstxtractor-ng 拆包 |
| 烟雾弹 |
main 为空 |
| 藏逻辑 |
改 python313.dll 内 frozen os;数据在 .rdata |
| 业务代码形态 |
Python 字节码 + 常量被 marshal,不是 .text 里显眼的 C 函数 |
| marshal 路牌 |
73+长度=串;29+个数=元组 |
| 线 A / 线 B |
展示用 0x55;校验用 6 字符 key(算 flag 只走 B) |
| 固定 XOR |
块 2 ⊕ def my_function( → 0x55 |
| 循环 XOR |
15 字节 ⊕ UTF-8「恭喜成功!」→ yb6tcr |
| 无明文时 |
动态断输出 + DLL diff,禁止盲扫整段 .rdata |
| 验证 |
动态输入看到「恭喜成功!」 |
| 内容 |
VA |
| 核心区 |
0x1804DC693~0x1804DCBCA |
名字 my_function |
0x1804DC6E9 |
| 展示块 2 |
0x1804DC746 |
| 用户密文 15B |
0x1804DC7FF |
| key 泄漏串 |
0x1804DC7DB(仅回搜印证) |
flag{yb6tcr}
题目.exe
提示,需要还原的代码
def my_function():
print("恭喜成功!")
请输入key还原代码(请输入6个字符)
chcp 936
题目.exe
┌─────────────────────────────┐
│ 题目.exe(快递箱) │
│ ┌───────────────────────┐ │
│ │ Python 运行环境 │ │
│ │ + 真正的 Python 逻辑 │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
密文字节: 第0 第1 第2 第3 第4 第5 第6 第7 第8 ...
key 字节: y b 6 t c r y b 6 ...
源码 .py
→ 编译成 code 对象(字节码 + 常量表 + 名字表)
→ marshal.dumps → 磁盘/DLL 里的 0x73 0x29 ...
→ 运行时 marshal.loads → 解释器执行
第 1 步 确认是 PyInstaller 打包的 Python
↓
第 2 步 用 pyinstxtractor-ng 拆包
↓
第 3 步 发现 main 几乎是空的(烟雾弹)
↓
第 4 步 在 python313.dll 中定位真题数据
↓
第 4.5 步(推荐)用 IDA 打开 DLL,按结构往下读
↓
第 5 步 两条线:展示用 0x55;校验用 6 字符 key
↓
第 6 步 得到 key = yb6tcr,运行验证
:: 独立 exe 工具
pyinstxtractor-ng.exe 题目.exe
题目.exe_extracted/
题目.exe_extracted/python313.dll
题目.exe_extracted/main.pyc
import sys
sys.exit(0)
1. 双击 题目.exe
2. PyInstaller 解压并启动 Python 3.13
3. 自动 import os → 【题目逻辑在这里执行】
4. 最后才执行 main → sys.exit(0) (几乎无作用)
my_function
运行现象 → 记下独特英文名 my_function
→ 在干净拆包目录全局搜索
→ 谁命中,谁更可疑
√ python313.dll (匹配 1 次) ← 唯一真源
× main.pyc
× PYZ.pyz
× 启动相关 pyc / 各 .pyd / 运行库 dll
29 08 ← 小元组,8 个成员
73 10 00 00 00 ← 串,长 16
[16 字节] ← 展示密文块 0
73 10 00 00 00
[16 字节] ← 展示密文块 1
... 共 8 次 ...
b = bytes.fromhex("b3dac5b2f1efbae9d9bcc9d5bdf3d4bd")
print(bytes(x ^ 0x55 for x in b))
73 0F 00 00 00 ← 长度 = 15
[15 字节] ← 用户密文(本题起址约 0x1804DC7FF)
文件偏移 0x4DACE9 ≈ VA 0x1804DC6E9
VA ≈ 0x180000000 + 文件偏移 + 0x1A00
明文字节 = 密文字节 XOR 0x55
密文 = bytes.fromhex("31303375382c0a33203b36213c3a3b7d")
明文 = b"def my_function("
print(bytes(密文[i] ^ 明文[i] for i in range(16)))
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于 1天前
被教教我吧~编辑
,原因: 上传附件