PyInstaller打包勒索病毒样本分析:拆包、修复pyc头与反编译实战
发布时间:2026/10/21 23:57:51 锦皓数字建站

拿到一个勒索病毒的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上能找到官方仓库多年未更新但依然好用。uncompyle6Python字节码反编译工具支持Python 2.7和3.x到3.8。xdis / decompyle3uncompyle6不兼容时的替代品。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有自己的头部和目录区TOCTOC记录着每个条目在文件中的偏移量、长度和类型类型包括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版本生成一个参考pycimport py_compile py_compile.compile(rblank.py, cfilerreference.pyc)blank.py是一个只含pass的空文件。生成的reference.pyc前16字节就是当前Python版本的完整头。我习惯用Python 3.8跑这个操作因为这个版本的魔法数在uncompyle6那里兼容性最好。更快速的办法是直接在Python交互环境里取import importlib.util print(importlib.util.MAGIC_NUMBER.hex())拿到后转成字节写入即可。3.3 修复脚本实例与验证我自己写了一个小脚本专门处理这种缺头pycimport 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. 源码到手之后快速锁定核心逻辑与IOC5.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编码后硬编码在源码里运行时解码。反编译后的源码里可能看到一堆类似于bxxxxxx的字符串变量后面跟一个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打包的恶意程序或商业软件。工具链不变变的是你对源码的解读。拿到源码后的耐心和技术功底才真正决定了分析的深度。希望这篇实战记录对你有帮助。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。