news 2026/10/21 23:57:41

PyInstaller打包勒索病毒样本分析:拆包、修复pyc头与反编译实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PyInstaller打包勒索病毒样本分析:拆包、修复pyc头与反编译实战

拿到一个勒索病毒的Python打包样本,第一件事就是想办法把它还原成能看的源码。这个套路我已经跑过几轮了,涉及的工具链基本固定:pyinstxtractor负责拆包,uncompyle6负责反编译,中间还有一步很容易被忽略——修复pyc头。今天就把这条链路完整走一遍,包括踩过的坑和备选方案,给做恶意样本分析的朋友做个参考。

先说清楚这个场景。勒索病毒用Python写并不罕见,攻击者图的是开发效率高、跨平台方便、免杀库现成。为了不让受害者直接看到代码,也为了让目标机器上有Python解释器就能跑,他们通常会拿PyInstaller把脚本打成Windows可执行文件。所以分析这类样本时,真正的战场不在逆向PE文件本身,而在还原PyInstaller的打包内容、把pyc反编译回Python源码。

这篇文章写给三类人:一类是刚接触恶意代码分析的安全新人,想知道这类样本从哪下手;一类是已经会跑工具但经常在反编译环节翻车的人,我这里有不少报错对照;还有一类是自己写的工具被PyInstaller打包后源码丢了、想从产物里抢救点东西出来的开发者。无论哪种场景,方法论是通用的。

1. 前置准备:环境、样本和工具链选型

1.1 为什么勒索病毒会以Python + PyInstaller形态出现

这类样本这两年见得越来越多,原因很现实。Python写勒索逻辑比C/C++快很多,加密库、文件遍历、系统API调用都有成熟封装,攻击者甚至可以拿开源项目改一改就上线。打包成exe之后,Python代码以字节码形式藏在资源段里,普通用户根本看不懂,足够挡住绝大多数人。

但反过来看,这也是分析者的机会。C/C++写的勒索病毒要逆出核心逻辑,得啃汇编、过混淆、还原加密流程,工作量极大。Python样本只要把pyc恢复出来,绝大部分逻辑直接以接近源码的形式摆在你面前,静态分析效率高了一个量级。这也是pyinstxtractor这类工具如此重要的原因。

1.2 分析环境搭建与安全基线

动手之前先把环境说清楚。我用的是一台独立虚拟机,Windows 7/10 x64都行,装好Python 3.7或3.8,配好快照。为什么强调这两个小版本,后面会讲,uncompyle6对3.8以下的兼容性最稳,3.9以上的pyc它经常罢工。

安全基线就三条:全程断网、只做静态分析、样本保持原始哈希记录。勒索病毒样本无论看起来多像“分析靶场玩具”,都不要双击运行,更不要在自己的主机上解包。所有操作都限制在隔离虚拟机内,宿主机的共享文件夹关闭,U盘不接,防止样本自动释放或感染相邻系统。

1.3 工具清单与版本选择

具体工具我列一下,都是免费开源的:

  • pyinstxtractor.py:一个Python脚本,专门拆PyInstaller打包体,GitHub上能找到,官方仓库多年未更新但依然好用。
  • uncompyle6:Python字节码反编译工具,支持Python 2.7和3.x到3.8。
  • xdis / decompyle3:uncompyle6不兼容时的替代品。
  • 010 Editor 或 Python的hexdump模块:看pyc头、改字节时用。
  • py_compile 或 importlib.util:用来获取当前Python版本的pyc魔法数(magic number)。

这些工具的安装都很简单,uncompyle6直接pip装,pyinstxtractor是一个独立脚本,下载后放到工作目录就行。需要提醒的是,不要把样本和解包工具放在同一个目录下反复折腾,我习惯建三个子目录:sample/ 放原始样本,extract/ 放解包产物,output/ 放反编译后的源码,互不干扰也方便记录分析过程。

2. pyinstxtractor拆包:原理与实操

2.1 PyInstaller打包格式的核心结构

拆包之前先理解PyInstaller到底干了什么。PyInstaller把Python脚本、依赖库、资源文件一起塞进一个自定义的归档容器里,这个容器在PE文件中叫做CArchive。CArchive有自己的头部和目录区(TOC),TOC记录着每个条目在文件中的偏移量、长度和类型,类型包括PYZ(压缩的Python库)、PKG(程序主逻辑包)、DATA(数据文件)、BINARY(二进制扩展模块)等。

pyinstxtractor做的事,就是扫描PE文件,定位CArchive,解析TOC,把里面所有条目按类型dump出来。也就是说它并不需要真正“脱壳”或“解密”,只是把打包内容精确地切出来,这也是它能处理绝大多数PyInstaller版本的原因。

2.2 提取操作与产物解读

拿到样本后先确认确实是PyInstaller产物,方法有两个:一是用PE工具看文件区段,PyInstaller打包的exe通常有pyi-windows-manifest等特征字符串;二是直接用pyinstxtractor试跑,工具能识别出CArchive就算成功。

执行命令很简单:

python pyinstxtractor.py sample.exe

跑完后同目录下会出现一个sample_extracted文件夹。正常的话里面能看到main.pyc(或实际入口名的pyc)、大量依赖库的pyc、还有可能的二进制扩展文件。我个人经验是先看结构再动手反编译,重点关注三个位置:

  • 最上层能直接看到的pyc文件,一般是主脚本的字节码。
  • PYZ归档解出来的库文件,通常在PYZ.pyz_extracted子目录里。
  • 有没有pyd或dll类文件,说明样本可能引入了C扩展,这部分pyc解不了。

2.3 为什么有时候提取出来就能反编译,有时候不行

这里有个关键区别。PyInstaller打包时,依赖库的pyc通常是完整带头的,而主脚本.pyc往往经过特殊处理,文件头被剥离了。提取出来一看,库文件pyc前16字节是正常的魔法数和时间戳,主脚本pyc却直接从marshal数据开始,或者前几字节是乱码。这就是下一步要修pyc头的直接原因。

还有一个注意点:pyinstxtractor提取出的目录里有些pyc文件其实是运行时生成的临时文件,或者PyInstaller内部模块,比如pyimod00_importers.pyc。分析勒索病毒时这些大概率用不上,别被它们带偏重点。

2.4 拆包失败场景与应对思路

不是每次都能顺利拆出来。我碰到过几种失败情况:

  • 样本套了额外的壳,比如把PyInstaller产物又用UPX压缩过,pyinstxtractor找不到CArchive。这时先脱壳再拆包。
  • 攻击者改过PyInstaller的bootloader或自定义TOC,特征被抹掉,工具识别失败。这种情况我会用HxD手动搜索特征字符串硬定位CArchive,再手工解析TOC,费劲但可行。
  • 打包时某些资源用的特殊PyInstaller版本,pyinstxtractor会有兼容问题,可以换新版分支,比如带archive.py修复的仓库版本。

3. 修复pyc头:反编译前最容易翻车的一步

3.1 不同Python版本的pyc头结构差异

pyc文件头不是固定的。Python 3.7以后,pyc头是16字节:4字节魔法数 + 4字节标志位/哈希类型 + 4字节时间戳 + 4字节源文件大小。魔法数随Python小版本变化,标志位指示是否启用hash校验,时间戳和大小用于源码与字节码的对应检查。

反编译工具识别pyc,首要看魔法数。如果魔法数缺失或不对,uncompyle6会直接报“Bad magic number”,根本不会进入下一步。PyInstaller主脚本pyc被剥离头部后,我们需要把正确的16字节补回去。

3.2 获取魔法数的标准做法

获取魔法数最靠谱的方式是拿你本机的Python版本生成一个参考pyc:

import py_compile py_compile.compile(r'blank.py', cfile=r'reference.pyc')

blank.py是一个只含pass的空文件。生成的reference.pyc前16字节就是当前Python版本的完整头。我习惯用Python 3.8跑这个操作,因为这个版本的魔法数在uncompyle6那里兼容性最好。

更快速的办法是直接在Python交互环境里取:

import importlib.util print(importlib.util.MAGIC_NUMBER.hex())

拿到后转成字节写入即可。

3.3 修复脚本实例与验证

我自己写了一个小脚本,专门处理这种缺头pyc:

import struct, sys, importlib.util def fix_pyc_header(src, dst): with open(src, 'rb') as f: data = f.read() # 判断是否已经有完整头 magic = importlib.util.MAGIC_NUMBER if data[:4] == magic: print(f'{src} header ok') open(dst, 'wb').write(data) return # 搜索标准pyc头的magic标记 idx = data.find(b'\xe3') if idx == -1: print(f'{src} code object not found') return # 新头: magic + flags(0) + timestamp + srcsize header = magic + struct.pack('<III', 0, 0x5F5F5F5F, 0) new_data = header + data[idx:] open(dst, 'wb').write(new_data) print(f'{src} fixed -> {dst}') if __name__ == '__main__': fix_pyc_header(sys.argv[1], sys.argv[2])

这个脚本的思路是先检查头部缺失,再在数据里找到marshal code object的起始标记(通常以\xe3开头),然后把正确的头拼接上去。观察\xe3位置是修复过程中的关键技巧,因为有些pyc头部并不完全为零,直接用固定16字节覆盖会覆盖掉真正的数据。

修复完之后,用marshal模块验证一下:

import marshal with open('fixed.pyc', 'rb') as f: f.read(16) code = marshal.load(f) print(type(code))

能正常输出types.CodeType就说明修复成功。也可以用dis模块看一眼字节码:

import dis dis.dis(code)

这一步我非常建议在批量反编译前做,可以先挑一个功能简单的库pyc验证工具链通不通,再处理真正的主脚本。走通一遍再批量处理,不容易积压一堆坏文件。

4. uncompyle6反编译:实操、边界与替代方案

4.1 uncompyle6的兼容性边界

uncompyle6的官方支持范围是Python 2.7和3.x到3.8。原理上它是按字节码版本做规则匹配的,Python 3.9以后新增的字节码指令、异常处理机制(异常组的引入)让它的规则表失效,所以经常返回“decompiling failed”或生成明显错的代码。所以前面才反复强调分析环境用Python 3.8,就是为了让样本pyc的版本落在uncompyle6能力范围内。

如果样本是Python 3.9以上版本编译的,uncompyle6基本没辙。这时候我通常会先看pyc头里的magic对应哪个Python版本,再决定下一步工具。有些样本会用高版本Python打包,但内部逻辑简单,硬啃或者换工具也能救回来。

4.2 标准反编译操作与批量处理

单文件反编译很简单:

uncompyle6 -o output/ fixed_main.pyc

-o参数指定输出目录,工具会自动把.pyc扩展名换成.py。如果只要单个文件不写目录,它会直接输出到控制台。

实际分析时,提取目录里的pyc可能有几十个,我写了一个简单脚本批量处理:

for pyc in extract/*.pyc; do uncompyle6 -o output/ "$pyc" 2>> error.log done

这个循环配合error.log,可以快速筛出哪些反编译成功、哪些失败。成功的结果直接看,失败的文件再单独处理。我自己跑这个过程时发现,有些库文件反编译后非常有价值,比如样本引用的加密库代码、配置模块,这些往往会暴露攻击者怎么调用的加密算法。

4.3 报错不是终点:强制模式与字节码辅助

uncompyle6最常见的报错是“Decompiling failed: AST out of range”和“No module named...”。前者通常是某个函数结构太复杂或者用了高版本语法,后者是pyc引用了缺失的外部模块,但这不影响反编译本体,报错后依然可能输出大部分源码。

一个实用的技巧是加--verify参数,让uncompyle6尝试对反编译结果重新编译,再和原pyc对比字节码。如果验证不通过,它会提示结果可能不精确,但代码依然有参考价值。我自己处理勒索样本时,很少要求100%还原,能做到逻辑可读就足够提取IOC和加密行为了。

如果uncompyle6彻底失败,我会退回用dis模块直接看字节码:

import dis, marshal with open('bad_one.pyc', 'rb') as f: f.read(16) code = marshal.load(f) dis.dis(code)

字节码虽然不如源码直观,但函数名、字符串常量、import调用都暴露得很清楚。尤其是样本中调用AES加密库或者遍历目录的代码,在字节码层一眼就能识别出来。

4.4 高版本Python样本的替代方案

遇到uncompyle6搞不定的高版本pyc,我的备选工具是decompyle3和pycdc。decompyle3专门支持Python 3.7到3.9的部分版本,pycdc是C++实现的Cpython反编译器,兼容性广但还原质量中等。还有在线反编译服务可以做兜底,但样本敏感度高,不建议上传第三方平台,一切离线处理更稳妥。

工具选型的总结就是:能用uncompyle6就跑uncompyle6,输出质量最好;跑不了再上decompyle3和pycdc;最后都用不了就dis硬啃字节码。这套组合拳下来,绝大多数Python打包样本都能被翻个底朝天。

5. 源码到手之后:快速锁定核心逻辑与IOC

5.1 入口定位与主流程还原

反编译会得到一堆.py文件,从哪里看起?我的习惯是先看入口文件。PyInstaller打包时,入口脚本通常是被直接提取到根目录的那个pyc对应的源码。如果反编译出了main.py,直接打开它。

勒索病毒的主流程基本跑不掉这几件事:收集系统信息、遍历磁盘文件、判断文件后缀并进行加密、在目录里投放勒索信、修改或禁用系统恢复功能、展示勒索界面。我在源码里搜索关键词的效率很高:walk、listdir、glob、encrypt、decrypt、AES、RSA、base64、shell、os.system、subprocess等。

如果找到加密函数,重点看它调用了哪些密码学库。常见组合是Crypto.Cipher模块的AES配合RSA公钥加密会话密钥,加密后的密钥写在文件尾部或单独文件里。这类信息直接决定了动态解密能不能写脚本,也决定文件恢复的可行性。

5.2 从源码中提取IOC与行为指纹

IOC这个词听着玄,其实就是能用来识别样本的可观察特征。在源码里我能提取到:

  • 加密文件后缀名,比如.encrypted、.locked、特定的随机后缀。
  • 勒索信文件名和内容里的联系方式、钱包地址。
  • 被加密文件的排除名单,很多勒索病毒会跳过Windows目录和Program Files。
  • 互斥量名,用于防止重复运行。
  • 网络行为代码中的C2地址(如果样本有外联行为)。

提取这些信息时,我会同步记录下来,做成一个样本行为摘要。这个摘要比完整的源码更能支撑后续的蓝队响应和溯源工作。不只是“这个样本是AES加密”,而是“文件后缀为abc,勒索信名为READ_ME.txt,排除了哪些目录,修改了哪些注册表键”这种可直接固化的指标。

5.3 碰到底层混淆或字符串解密时的处理

现在很多Python勒索样本会做一层混淆,最常见的是把关键字符串用base64编码后硬编码在源码里,运行时解码。反编译后的源码里可能看到一堆类似于b'xxxxxx'的字符串变量,后面跟一个decode调用。这种情况直接在源码里搜decode,把对应的base64字符串复制出来解一下就行。

更高端一点的会用marshal.dumps把一段代码序列化后再exec执行。反编译出来可能是一段又长又乱的字节串,此时不要试图从源码层还原,直接用marshal.loads解出内层code object,再单独对这块字节码做dis或uncompyle6反编译。处理混淆本身也是逆向的常见流程,遇到一次就会记住套路了。

6. 常见问题与排查技巧实录

6.1 高频错误和解决办法速查

整理一张表,都是实际操作频率最高的问题:

现象原因对策
pyinstxtractor找不到CArchive样本被额外加壳先脱壳再用pyinstxtractor
提取后没有main.pyc入口脚本不叫main翻看提取目录下其他根级pyc,或者从pyz目录找
uncompyle6报Bad magic numberpyc头缺失或版本不匹配用参考pyc修复头,确认Python版本
uncompyle6报AST out of range使用了高版本语法换decompyle3/pycdc,或与dis联用
反编译结果只有几个函数某些复杂函数反编译失败用dis查看失败的函数字节码
源码中字符串都是乱码/bytes内置base64编码混淆搜索decode调用,逐一解出字符串

这些情况我都在不同样本上遇到过,没有一个是无解的。关键心态是:反编译是辅助手段,不是最终目的,我们只要能拿到足够的信息即可,不需要强迫每个函数都完美还原。

6.2 Python版本不一致造成的一连串坑

版本问题是这套流程里最隐蔽的坑。比如我用Python 3.7的环境去跑一个基于Python 3.8生成的pyc,魔法数对不上,修复头之后uncompyle6能跑但结果可能南辕北辙。更麻烦的是,有些样本用了Python 3.10的walrus运算符或3.9的新语法,旧版工具解析不了。

我的建议是准备两到三个版本的Python环境,至少覆盖3.7、3.8和3.10。先用pyc头里的魔法数确定样本版本,再选择对应的分析环境。判定版本可以直接查magic对照表,也可以用一个简单办法:用不同版本的Python分别import该pyc(使用importlib),能正常加载且不出SyntaxError的那个版本大概率接近编译版本。

6.3 提取产物不完整时的补救思路

有时候pyinstxtractor解出来的目录缺东西,比如主脚本pyc找不到,或者PYZ解出来的依赖库不完整。这时别急着放弃,检查一下原始exe的大小和提取目录的大小,如果相差太多,可能CArchive之外还有附加数据。这种情况我会直接用HxD打开exe,搜索“PYZ”、“MEIPASS”、“_MEI”等字符串,手工定位附加段。

如果目标机器上实际运行样本还能拿到进程内存转储,那么在内存里找pyc是另一个思路。但作为静态分析为主的文章,我只能说动态内存提取是最终的兜底手段,启动过程要有完备的沙箱和监控措施。

6.4 几个实用技巧:哈希、记录与复核

最后分享几个实操习惯。

第一步永远先算样本哈希并记录。分析完整个样本后,把提取的pyc、修复后的pyc、反编译源码都各自算一遍哈希,这能保证从原始样本到最终产物的整个链路可追溯,将来写分析报告也用得上。

每修复一个pyc头,我都在文件名里标注来源,比如main_fixed.pyc、lib1_fixed.pyc,防止加工过的文件与原文件混淆。反编译时产生的error.log不是垃圾,它是后续深入分析的线索索引,报错文件往往意味着样本的关键模块用了复杂语法,值得回头看。

在验证反编译结果时,我偶尔会把反编译的.py文件重新用py_compile编译回pyc,再对比反编译前后的字节码结构。如果差异过大,说明反编译过程丢失了关键信息,这时再回头用dis核对具体函数。

7. 分析流程复盘与个人心得

整套流程走下来,从拿到打包好的exe到产出可读源码和IOC清单,熟练之后通常半小时内完成。pyinstxtractor拆包是机械操作,pyc头修复是技术活,uncompyle6反编译是运气加经验,最后源码分析是重点。每个环节都有各自的坑,但走通一次之后,后续再遇到都是体力活。

我个人在实操中有一条体会:不需要迷信“源码完美复原”。即使uncompyle6反编译出来的代码有残缺,或者个别函数因为高版本语法无法还原,只要能还原出主流程、加密逻辑和IOC,分析目的就已经达成。反过来,如果过分追求每个字节都还原,反而会在个别复杂函数上耗掉大量时间,得不偿失。

这篇文章涉及的样本分析思路,同样适用于分析其他PyInstaller打包的恶意程序或商业软件。工具链不变,变的是你对源码的解读。拿到源码后的耐心和技术功底,才真正决定了分析的深度。希望这篇实战记录对你有帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 23:56:22

知识工作插件系统设计:架构取舍、生命周期与生态运营实战

1. 从“知识工作插件”这个命名说起&#xff1a;它到底想解决什么问题 第一次看到 knowledge-work-plugins 这个命名&#xff0c;我的直觉是&#xff1a;这不是一个普通的工具库&#xff0c;而是一套面向“知识工作者”的扩展机制。所谓知识工作者&#xff0c;说白了就是每天…

作者头像 李华
网站建设 2026/10/21 23:55:36

LiteLLM统一多模型调用:从碎片化适配到生产级网关实践

1. 从一个真实困境说起&#xff1a;多模型调用的碎片化之痛 如果你正在做AI应用开发&#xff0c;大概率遇到过这样的场景&#xff1a;项目初期用OpenAI的接口跑通了原型&#xff0c;效果不错&#xff0c;上线后老板说“能不能加个Claude做对比”&#xff0c;你花了两天适配Anth…

作者头像 李华
网站建设 2026/10/21 23:48:50

基于Zynq+AD9361的双通道SDR平台:架构、校准与场景应用实战

1. 先捋清楚自己到底在找什么&#xff1a;为什么最后盯着ZynqAD9361不放最近一直在评估一套基于 ZynqAD9361 的双通道SDR平台&#xff0c;也就是标题里那套 E310。参数表一眼看过去很干脆&#xff1a;2x2 MIMO、56MHz 瞬时带宽、软硬一体开放生态&#xff0c;应用场景直接指向频…

作者头像 李华
网站建设 2026/10/21 23:48:36

S7-1200与S120 PROFINET通讯实战:标准报文1配置与参数读写

简介&#xff1a;这份PDF文档面向工业自动化工程师、PLC编程人员及高校相关专业学习者&#xff0c;系统讲解如何通过PROFINET实现S7-1200 PLC与SINAMICS S120驱动器之间的实时通讯&#xff0c;解决控制器与分布式驱动设备组网配置、编程调试中的实际问题。资源包内含1个PDF文件…

作者头像 李华
网站建设 2026/10/21 23:45:58

Python环境容器化:Dockerfile构建与依赖管理实战指南

上午给A项目装好Python 3.8的环境&#xff0c;下午B项目要升3.11&#xff0c;一执行pip install&#xff0c;A项目的依赖直接崩掉——这种被环境问题反复折磨的场景&#xff0c;做过几年Python开发的基本都熟。docker容器配置Python环境的思路&#xff0c;就是从根上把这个问题…

作者头像 李华
网站建设 2026/10/21 23:44:32

PostgreSQL物理存储结构全解析:从数据目录到页面与元组

很多朋友第一次打开 PostgreSQL 的数据目录&#xff0c;都会愣一下&#xff1a;base、global、pg_wal 一堆文件夹&#xff0c;想找自己建的那张 orders 表&#xff0c;却根本看不到 orders.db 这样的文件。这个困惑我解释过太多次了。实际上 PostgreSQL 把表、索引、序列、物化…

作者头像 李华