re-python について
re-pythonは、PyInstaller、PyArmor、pycファイルなどを解析するリバースエンジニアリング向けスキルです。パッカーの識別から抽出、逆コンパイル、主要ロジックの特定までを段階的に進めます。
主な機能
- 01Pythonパッカーの識別
- 02PyInstallerアーカイブの抽出
- 03PyArmorサンプルの展開
- 04pycのバージョン識別と逆コンパイル
- 05難読化サンプルの主要ロジック特定
活用シーン
- 01不審なPyInstaller実行ファイルの調査
- 02PyArmor保護サンプルの復元
- 03単独のpycファイルの解析
- 04コンパイル済みPython成果物の調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dslsdzc/ rev-skills --skill re-pythonインストール後、Claude Code で/re-pythonを入力して読み込まれたか確認します。
同梱ファイルreferences/examples.mdreferences/layout.md
詳細
Python パッケージ化サンプルの解析(PyInstaller / PyArmor / pyc)
使用する場合/使用しない場合
- 使用する場合:PyInstaller の単一ファイル/ディレクトリ形式の exe、PyArmor で保護されたサンプル、.pyc ファイル、Nuitka/Cython のコンパイル成果物、Python マルウェアのパッケージ化サンプル
- 使用しない場合:純粋な .py ソースコードの難読化(base64/エンコードによるラッピング)→ [[re-script-deob]] に移行;Python のモデルウェイト(pkl/onnx)→ [[re-ai-model]]
ツールの準備
[[re-analyze/platform-tips]] を参照してください。解凍/逆コンパイルは静的な手順のため、サンドボックスは不要です。平文を動的に取得する場合([[re-sandbox]])は、原則としてサンドボックスを使用してください。
python3(基本ランタイム、必須)
- Linux:
apt install python3/dnf install python3/pacman -S python - macOS:
brew install python(システムに付属) - Windows:公式インストーラーまたは
choco install python - 確認:
python3 --version
pyinstxtractor(PyInstaller アーカイブの抽出)
- マルチプラットフォーム:
pip install pyinstxtractor-ngまたは GitHub スクリプトpython pyinstxtractor.py - 確認:
pyinstxtractor-ng <対象.exe>をエラーなく実行できること(独立した --help コマンドはありません)
PyArmor-Unpacker(PyArmor で保護されたファイルの解凍)
- マルチプラットフォーム:
git clone https://github.com/Svenskithesource/PyArmor-Unpackerを実行し、README に従って PyArmor のバージョンに応じた 3 つの方法から 1 つを選択します - 確認:リポジトリ内の Python スクリプトが実行できること
pycdc / pycdas(pyc の逆コンパイル)
- Linux/macOS:
git clone https://github.com/zrax/pycdc && cd pycdc && cmake . && make - Windows:プリコンパイル済みバイナリを使用するか、WSL でコンパイルします
- 確認:
./pycdc --help
file(パッカーの識別補助、汎用)
- 各システムに付属(Linux は binutils、macOS は標準搭載。Windows では別途インストールするか、省略してください)
- 確認:
file --version
作業手順
順番に実行し、各手順の成果物を保存してください(パス + sha256。[[re-triage]] を参照)。
-
パッカーを識別する:
file sample.exe strings sample.exe | grep -iE 'PyInstaller|_MEIPASS|pyi-|PyArmor|pyarmor' | head- PyInstaller の特徴:
PyInstallerのバージョン文字列、_MEIの一時ディレクトリ名、pyi-プレフィックスのブートローダー - PyArmor の特徴:
pyarmorの runtime 文字列 - Nuitka/Cython:pyc がなく、純粋なコンパイル成果物(
fileは通常の実行可能ファイルと表示し、Python runtime のパッケージ化を示す特徴はありません) - 識別できなくても Python 関連であることが確認できた場合は、疑わしい PyInstaller として扱います
- PyInstaller の特徴:
-
PyInstaller を解凍する:
- pip でインストールした場合:
pyinstxtractor-ng sample.exe;GitHub スクリプトを使用する場合:python pyinstxtractor.py sample.exe
python pyinstxtractor.py sample.exe # 输出 sample.exe_extracted/ 目录,含 PYZ-00.pyz(依赖归档)与主脚本 .pyc- メインの .pyc を特定します(名前がエントリースクリプトと対応)。PYZ 内のモジュールは自動的に
PYZ-00.pyz_extracted/に展開されます。モジュール一覧は archive_viewer.py で確認できます
- pip でインストールした場合:
-
PyArmor を解凍する(手順 1 で PyArmor が検出された場合):
- PyArmor のバージョンに応じて PyArmor-Unpacker の 3 つの方法から 1 つを選択します(README でバージョンを確認し、対応する方法を使用)
- 解凍後の成果物が pyc またはソースコードの場合は、次の手順に進みます
-
pyc のバージョンを識別する:
python3 -c "import struct,sys; print(struct.unpack('<H', open('main.pyc','rb').read(2))[0])"- magic の対応表(リトルエンディアンの 2 バイト、一般的な値。実際の値はローカルの MAGIC_NUMBER を基準にします):
0d33=3.6、0d42=3.7、0d55=3.8、0d61=3.9、0d6f=3.10、0da7=3.11、0dcb=3.12- 3.13 以降:数値を固定せず、ローカルの MAGIC_NUMBER から換算します(
python3 -c "import importlib.util; print(hex(int.from_bytes(importlib.util.MAGIC_NUMBER[:2],'little')))")
- pyc ヘッダーのレイアウト(3.7 以降は 16 バイトで統一):magic(4) + flags(4) + mtime(4) + size(4)。code オブジェクトはオフセット 16 から始まります。flags の下位 1 ビットは hash-based を示します(PEP 552。mtime/size に意味はありません)。PyInstaller から抽出した pyc のヘッダーでは flags/mtime/size がすべて 0 ですが、これは正常であり、破損ではありません
- バージョンによって marshal 形式が決まります:3.14 以降、marshal は v5 に更新され、code オブジェクト内部のレイアウトが旧版と異なります。marshal を手動で解析する場合は、magic のバージョンに応じて処理を分岐してください。pycdc などのツールは 3.13 以降への対応が遅れているため、対応するバージョンの Python の
dis/marshalを優先して代替手段として使用します - バージョンが一致する場合は、対応するバージョンの Python または pycdc で逆コンパイルします。一致しない場合は、対応するバージョンをインストールしてから再試行します
- magic の対応表(リトルエンディアンの 2 バイト、一般的な値。実際の値はローカルの MAGIC_NUMBER を基準にします):
-
逆コンパイルと整理:
pycdc main.pyc > main_decompiled.py # 或匹配版本 python 的 dis 模块- 難読化コードを整理します:偽の関数やデッドコードを削除し(PyArmor によく見られる、参照されていないラッパー関数など)、暗号化/ネットワーク/情報漏えいといった中核ロジックを特定します
- 逆コンパイルに失敗した場合(バージョン不一致/難読化)は、
disを使って重要な関数をバイトコードレベルで分析するか、[[re-binary-core]] に移行して詳細に復元します
分野横断の連携
- [[re-managed]] ゲートウェイ:本スキルの所属先(選択ツリーの「Python パッケージ化サンプル」分岐)
- [[re-script-deob]]:パッケージ化されていない純粋なスクリプトの難読化ケース
- [[re-malware]]:悪意のある Python サンプルの動作検証と IOC(解凍後、動作分析に移行)
- [[re-binary-core]]:pyc の詳細な復元/複合成果物(native モジュールを内包するもの。[[re-format-elf]])
- 基盤の [[re-triage]]:パッカー識別の初期調査入力
常见坑与陷阱
- pyc 版本不匹配直接反编译失败:现象——pycdc 输出乱码或报错;原因——magic 版本与目标不符;对策——先做步骤 4 的 magic 识别,按版本选工具
- pyc 头 mtime/size 为 0 不是损坏:现象——PyInstaller 解出的 pyc 用工具打开报错/空白,怀疑解包失败;原因——PyInstaller 写 PYZ 时清零 mtime/size(marshal 本体不受影响);对策——按 magic 识别版本后跳过 16 字节头,直接
marshal.loads验证 code 对象,或交给 pycdc - hash-based pyc(PEP 552):现象——pyc 头 flags 字段非 0(低 1 位为 1),mtime/size 是哈希或占位;原因——构建方用 hash 校验模式编译;对策——识别只看 magic 与 flags 低 1 位,反编译不受影响(code 对象仍是标准 marshal 流)
- PyArmor 版本差异导致解包器失效:现象——PyArmor-Unpacker 报不支持;原因——PyArmor 版本过新/过旧;对策——按版本换方法,或手动定位 runtime 的加解密逻辑(转 [[re-binary-core]])
- Cython/Nuitka 无 pyc:现象——找不到 .pyc;原因——编译型产物;对策——直接分析产物([[re-format-elf]] / [[re-ghidra]]),不找 pyc
- 假函数干扰定位:现象——解包后源码充斥无引用包装函数;原因——PyArmor 的 confusion code;对策——先按调用关系过滤(无调用者即候选),再定位核心逻辑
- strings 找打包器特征被加密隐藏:现象——strings 无 PyInstaller 特征但行为是 Python;原因——字符串加密/壳;对策——按行为线索与扩展名判断,必要时动态侧([[re-sandbox]])观察
- 混淆样本整体是"加密数据 + 引导壳 + 原生解密组件"三件套:现象——静态看只有一处引导调用和一段加密字节串常量,找不到正常 pyc,函数字节码在内存里也长期保持密文;原因——整份程序以 marshal 流形态加密后作为一个字节串常量整体内嵌进引导调用,解密密钥由随样本交付的原生扩展内部静态数据派生(密钥材料与配方随构建变化),单个函数执行前才原位解密、返回后立即原位重加密,明文存在的窗口被压到极短;函数常量表还被注入负责进入/离开/完整性校验的原生桩对象,文件里只剩标记字符串,真实对象只能运行期生成;对策——把样本拆成三件套分别处理,函数级明文要么静态还原加解密流程、要么在运行期解密窗口内抓取;注意解密后的函数体仍是普通 CPython 字节码,不存在自定义字节码虚拟机——看起来像 VM 的花哨字节只是加密后的标准字节码
- 密钥材料定位用"盐锚点 + 相对偏移",别硬编码绝对地址:现象——换一个构建版本的样本,旧的密钥定位脚本全部失效;原因——密钥由多段静态数据拼接后取摘要派生(典型拼段含盐、内嵌公钥、签名许可证描述哈希、一段需先还原的掩码数据),掩码区域靠固定长度区域与重复字节 pad 做 XOR 还原,而文件中的对齐填充同样表现为连续相同字节,全零候选一旦被当 pad 就派生错误密钥;参与拼接的输入还受条件分支控制(外部密钥文件、试用/正式开关);对策——在原生扩展的原始字节中先搜可见盐锚点字符串,其余密钥材料一律以锚点位置为基准按相对偏移计算,同一套代码可跨多个构建复用;自动找 pad 时排除全零候选;按标志位判断启用哪些输入;静态还原不必让启动期完整性校验(如公钥验签)通过,从分支逻辑复现出密钥派生的输入拼接方式即可
- 解密产物是标准 marshal 流,但重建 CodeType 有格式怪癖:现象——按常规 .pyc 流程解析解密出的字节流时字段错位、常量错乱,个别 Python 版本上还会直接崩溃且异常不可捕获;原因——序列化时常量元组声明的长度可能小于实际写入个数(多余条目无头部直接跟在后),流里还可能出现私有类型字节区间,代表只能由运行时在内存中生成的原生对象(静态读不到本体,占位符不剔除会导致后续常量索引错位),密文字节码直接进反汇编或 CodeType 构造在部分版本上会崩解释器;对策——用一次性模板代码编译后 .replace() 构造 CodeType 而非直接调构造器(构造器参数顺序随版本变化),本地变量数显式传入;原生对象以占位符表示并在构造前剔除;单条记录解析失败时保留已读字段(字节码与常量靠前、通常已拿到),整体降级为逐个报告完整解析的 code 对象而非全盘失败;解密失败用模板占位字节码顶替,密文仅十六进制展示;3.11 起 code 对象布局变化,从 blob 头读目标版本并自动切到匹配版本解释器,不为每个版本各写一套解析规则
- 动态取明文要卡"进入桩解密后"的窗口,导入方式也有讲究:现象——运行时 hook 后抓不到明文,或回调内部崩溃;原因——受保护函数每次调用都经进入桩解密、执行、离开桩重加密三步,明文窗口极短(短函数可能立即离开),事后读取拿不到;进入桩对外是 Python 可调用对象(内建函数对象形态),可从模块常量表辨认,真实原生函数指针藏在 C 函数对象的元数据结构里、经内存布局偏移可读出;首轮导入时回调内部再触发同一钩子会嵌套破坏回调链;以 main 方式运行会走另一条运行时路径、绕过发现机制;对策——拦截模块首次解密后执行的那次调用,从传入 code 对象的常量中读出进入桩指针;首轮导入返回空指针中止、重启一轮等内联钩子就位,后续执行用具名模块方式导入;定位导入槽在数据段内扫描已解析的真实函数地址指针,而不是扫调用点(调用点引用的是槽地址,直接扫容易错)
- 混淆器内部格式不是文档化兼容接口,结论必须标注版本:现象——同一套还原脚本换版本就失效,且说不清坏在哪一步;原因——密钥派生输入、嵌入标记位置、blob 头、函数级描述符、原生入口、marshal 重建行为都可能随版本变化,工具与目标解释器版本也强绑定(code 对象/marshal 布局随版本变);对策——任何结论标注精确版本,跨版本复用工具不假设可用;对每个新版本逐项重验(密钥配方、标记位置、头部结构、描述符、入口函数、重建行为),未经真实样本验证的分支显式标注未实测;静态重建与动态捕获(运行期真实解密产物)逐字段对照互相验证——动态能暴露静态推断的格式错误(如把原生对象误当普通常量、元组声明长度偏差),静态补足动态够不到的死路径(未调用函数、未触达分支);试用/正式、有无外部密钥文件等形态差异按标志位与分支逻辑处理,不假设单一形态
コメント
Markdown 対応