Python恶意样本分析实战教学文档
文档概述
本教学文档基于一次真实的Python恶意样本分析实战,详细拆解了攻击者使用的多层混淆与反检测技术,并逐步演示了如何从高度混淆的Payload中还原出恶意代码的原始逻辑。文档将遵循分析师的实战步骤,并深入解释每个环节的技术原理和操作细节,旨在帮助安全研究人员掌握类似的Python恶意软件分析技能。
核心分析链路:
多层编码/压缩 → 隐藏字符干扰 → compile+exec触发 → reverse + marshal.loads链式加载 → 字节码还原
第一章:样本初步观察与核心API识别
1.1 初始Payload结构
分析始于一段嵌入在XML/HTML中的Python代码。初始Payload的核心特征是大量使用Python内置的编码和压缩模块,并最终通过compile和exec函数执行。
关键代码模式识别:
# 样本中发现的典型模式
code_obj = compile(source_decoded, '<string>', 'exec')
exec(code_obj, globals_dict, locals_dict)
1.2 compile 与 exec 函数深度解析
-
compile函数:- 作用: 将源代码字符串编译为可执行的代码对象(code object)。
- 关键参数:
source:源代码字符串。在恶意样本中,这通常是经过多层解码后得到的明文或半明文代码。filename:代码所在的文件名。恶意样本常使用'<string>'以避免在错误信息中暴露真实路径。mode:编译模式。'exec':用于编译模块或一段程序语句。'eval':用于编译单个表达式,并返回其结果。'single':用于编译单条交互式语句。
-
执行链风险:
compile和exec的组合使得攻击者能够动态地执行任意代码。样本通常会先通过一个复杂的解码链还原出真正的恶意源代码,然后编译并执行。
1.3 多层压缩编码识别与自动化解码
初始Payload经过了一系列的编码和压缩,顺序通常为:base64 → gzip → bz2 → lzma → zlib。
- 自动化解码脚本思路:
- 使用正则表达式匹配
base64.b64decode('...')中的内容。 - 编写一个
deobfuscate函数,对提取出的数据依次尝试上述各种解压算法。 - 循环执行“匹配-解码”过程,直到无法再匹配到
base64.b64decode模式为止,此时意味着我们可能已经剥离了一层“外壳”。
- 使用正则表达式匹配
import re
import base64
import gzip
import bz2
import lzma
import zlib
def deobfuscate(encoded_data):
# 尝试多种解压方式
try:
# 1. 先进行base64解码
decoded = base64.b64decode(encoded_data)
# 2. 尝试用各种压缩库解压
for module in [gzip, bz2, lzma, zlib]:
try:
if module is zlib:
return module.decompress(decoded).decode('utf-8')
else:
with module.open(fileobj=io.BytesIO(decoded)) as f:
return f.read().decode('utf-8')
except Exception:
continue
# 如果都无法解压,尝试直接解码为字符串
return decoded.decode('utf-8')
except Exception as e:
print(f"Deobfuscation error: {e}")
return None
payload = "初始的混淆Payload字符串"
while True:
match = re.search(r"base64\.b64decode$'([^']*)'$", payload)
if not match:
break
encoded_str = match.group(1)
new_payload = deobfuscate(encoded_str)
if new_payload is None:
break
payload = new_payload
print(f"Decoded payload length: {len(payload)}")
# 循环结束后,payload 可能进入下一阶段
第二章:中级对抗技巧分析与绕过
2.1 隐藏字符处理
在自动化解码链结束后,得到的Payload可能看起来变短了,但在可视化检查时发现异常。
-
问题发现:
- 打印
len(payload)发现长度与肉眼所见字符数不符。 - 将Payload转换为十六进制(
payload.hex())或直接输出,可能会发现大量不可见的控制字符、空白字符(如零宽空格)或非常规Unicode字符。
- 打印
-
解决方案:
- 编写简单的替换脚本,将这些干扰字符移除或替换为普通字符。
- 示例:
cleaned_payload = re.sub(r'[\x00-\x1f\x7f-\x9f\u200b-\u200f\u202a-\u202e]', '', payload)
2.2 Marshal模块与代码对象反序列化
经过清理后,Payload进入了新的阶段:使用 marshal 模块。
-
Marshal模块是什么?
- Python内置的一个用于序列化Python对象(特别是代码对象、内部类型)的模块。它主要用于Python解释器自身生成
.pyc文件。 - 警告: Marshal格式不保证跨Python版本兼容,且不适合处理不受信任的数据,因为它可以直接构造代码对象。
- Python内置的一个用于序列化Python对象(特别是代码对象、内部类型)的模块。它主要用于Python解释器自身生成
-
样本中的常见模式:
# 通常会对数据进行反转(reverse)以增加分析难度 data = payload[::-1] # 将字符串或字节串反转 code_obj = marshal.loads(data) # 从反转后的数据中加载代码对象
2.3 逆向Pyc文件结构
为了分析 marshal.loads 加载的代码对象,一个常见的思路是将其重构为一个有效的 .pyc 文件,然后使用反编译工具。
-
Pyc文件结构:
- Magic Number (4字节): 标识创建此pyc文件的Python版本。例如,
b'\xf3\x0d\x0d\x0a'对应 Python 3.13。 - Bit Field (4字节): 标志位。最低位为1表示使用哈希校验格式,为0表示使用时间戳格式。
- 后续8字节:
- 时间戳格式: 4字节修改时间 + 4字节源文件大小。
- 哈希格式: 8字节哈希值。
- Marshal数据: 序列化后的代码对象数据。
- Magic Number (4字节): 标识创建此pyc文件的Python版本。例如,
-
伪造Pyc头:
样本可能会伪造一个简单的头部,让工具能够识别。pyc_header = b'\xf3\x0d\x0d\x0a' + b'\x00' * 12 # Python 3.13 魔数 + 全0的时间戳/哈希区 with open('output.pyc', 'wb') as f: f.write(pyc_header) f.write(marshal_data) # 这里是经过反转等处理后的数据 -
遇到的挑战: 反编译工具(如
uncompyle6,pycdc)可能尚未支持最新的Python版本(如实战中的3.13),导致无法直接反编译出源码。
第三章:高级分析与源码还原
3.1 链式Marshal加载与代码对象提取
当反编译工具失效时,需要采用更底层的分析方法。分析师发现,恶意样本的代码对象(code_obj)的常量池(co_consts)中,存储着下一个阶段的Payload。
- 动态提取循环:
import marshal import dis # Python反汇编模块 # 假设 initial_data 是经过之前步骤处理后的字节串 data = initial_data try: while True: # 1. 反转数据 data = data[::-1] # 2. 加载为代码对象 code_obj = marshal.loads(data) # 3. 反汇编当前代码对象,查看其指令 print("Disassembly of current code object:") dis.dis(code_obj) print("\n" + "="*50 + "\n") # 4. 关键:从代码对象的常量池(co_consts)中提取第一个常量,它通常是下一阶段的载荷 # 注意:co_consts 是一个元组,需要根据实际情况选择索引 if code_obj.co_consts and isinstance(code_obj.co_consts[0], (bytes, str)): data = code_obj.co_consts[0] # 更新data,继续循环 else: break except Exception as e: print(f"Loop ended: {e}") # 最后出错的code_obj可能就是最内层的核心逻辑
通过这个循环,可以一层层地“剥开”恶意样本的外壳,直到最内层的恶意逻辑暴露出来。循环终止时,最后一个能被成功加载的 code_obj 通常就是核心功能模块。
3.2 字节码反汇编与人工/LLM辅助还原
对于无法反编译的Python版本,最后的还原手段是分析字节码。
-
使用
dis模块:
dis.dis(code_obj)可以将代码对象反汇编为人类可读的字节码指令。 -
借助LLM(大语言模型)进行还原:
- 提取字节码: 将
dis.dis(code_obj)的输出文本保存下来。 - 提示工程: 向LLM提供清晰的指令。
提示词示例:
“你是一个资深的Python安全专家。请将以下Python字节码反汇编结果还原为等效的、可读性高的Python源代码。注意分析控制流和数据流。
【此处粘贴dis.dis的输出】” - 审计与校对: LLM的还原结果可能不完全准确,需要分析师凭借对Python字节码的理解进行人工校对,重点关注系统调用、文件操作、网络通信等敏感行为。
- 提取字节码: 将
第四章:总结与防御建议
4.1 技术总结
本次分析的恶意样本巧妙地组合了多种技术:
- 混淆层: 多层压缩编码、隐藏字符。
- 执行层: 使用
compile和exec动态执行。 - 持久化/隐藏层: 利用
marshal序列化代码对象,并通过链式加载和Pyc文件格式伪装,增加静态分析难度。 - 版本对抗: 采用较新的Python版本,利用反编译工具的滞后性。
4.2 检测与防御建议
-
静态检测规则(IDS/YARA):
- 关注代码中是否连续出现
base64、gzip、bz2、lzma、zlib等模块的调用。 - 检测
compile(..., '<string>', 'exec')和exec(...)的组合。 - 监控对
__builtins__、__import__的修改操作。 - 查找
marshal.loads(...)以及字节串的[::-1]反转操作。
- 关注代码中是否连续出现
-
动态沙箱分析:
- 在隔离环境(沙箱、容器)中运行样本。
- 钩住(Hook)关键函数(如
os.system,open,__import__,socket.connect),记录所有敏感操作。 - 监控进程树和网络连接。
-
供应链安全:
- 严格审查第三方Python包。
- 使用虚拟环境或容器限制应用的权限。
4.3 自动化分析脚本思路
建议将整个分析流程脚本化,形成一个自动化分析管道:
- 输入: 混淆的Payload。
- 循环解码: 自动进行多层编码/压缩解码。
- 字符清理: 自动移除隐藏字符。
- Marshal探测: 自动尝试反转并加载为代码对象,递归提取
co_consts。 - 输出: 每层的解码结果、最终的反汇编代码、以及尝试还原的源码。同时记录各阶段的长度、哈希值,便于回溯分析。
文档说明: 本文档完全基于提供的链接内容进行整理、扩展和深化,未添加任何外部无关信息。所有技术细节均可在原文中找到对应或推导出的依据。