1. 项目概述:当加密脚本遇上静态解密
在Python生态里,代码保护一直是个让人又爱又恨的话题。爱的是,谁都不希望自己辛苦写的核心逻辑被别人轻易拿走;恨的是,Python作为一门解释型语言,其源码的“裸露”特性让保护变得异常棘手。Pyarmor作为一款成熟的Python代码混淆与加密工具,在商业软件、内部工具保护领域应用广泛。它通过运行时动态解密、代码混淆、许可证绑定等手段,为开发者提供了一道防线。然而,道高一尺,魔高一丈,总有一些场景——比如你遗失了某个关键脚本的源码,或者需要对一个合法获得的、但被加密的脚本进行安全审计和兼容性分析——你需要一种方法来“看清”它的真面目。这就是“Pyarmor-Static-Unpack-1shot”这类工具存在的意义:它不依赖于动态调试,而是尝试从静态文件层面,对Pyarmor加密后的脚本进行一次性解密还原。
简单来说,这个项目瞄准的是一个非常具体的痛点:如何在不运行目标脚本、不依赖特定运行环境的前提下,仅通过分析加密后的.pyc或包裹文件,逆向出被Pyarmor保护的原始Python源代码逻辑。它不是一个通用的破解工具,而更像是一个针对特定加密方案(Pyarmor)的静态分析“手术刀”。对于安全研究人员、进行遗留代码恢复的开发者,或是需要验证加密方案强度的工程师而言,掌握其原理和实现方法,无疑是一项极具价值的技能。接下来,我将带你深入拆解这套方案的底层逻辑、实现要点以及实操中那些“坑”,让你不仅能理解它,更能评估其局限性与应用边界。
2. 核心原理:拆解Pyarmor的加密外壳
要理解如何静态解密,首先必须彻底弄明白Pyarmor是如何给代码“穿上衣服”的。Pyarmor的加密并非铁板一块,它是一套组合拳,理解每一拳的招式,才能找到破绽。
2.1 Pyarmor加密机制深度剖析
Pyarmor的加密流程可以概括为“混淆+加密+运行时支撑”三步走。
第一步:代码变换与混淆。在加密前,Pyarmor会对源代码的抽象语法树(AST)进行一系列变换。这包括但不限于:重命名局部变量和函数名为无意义的短字符串(如a,b,c1)、插入垃圾代码(死代码,不会被执行但能干扰阅读)、控制流扁平化(将简单的if-else逻辑转化为switch-case或跳转表的形式,增加逆向难度)。这一步的目的不是防止执行,而是极大提高人工阅读和自动反混淆的难度。即使你拿到了解密后的代码,看到的也可能是一堆面目全非的a = b + c,需要结合上下文反复推断其原始含义。
第二步:字节码加密与封装。这是核心的加密层。Pyarmor会将混淆后的代码编译成Python字节码(.pyc格式),然后使用一个对称加密算法(通常是AES)对字节码进行加密。加密密钥并非固定不变,而是会动态生成或从许可证文件中读取。加密后的字节码并不会单独存在,而是被嵌入到一个新的“包裹脚本”中。这个包裹脚本本身是明文的Python代码,它包含了以下几个关键部分:
- 引导代码(Bootstrap):负责初始化Pyarmor的运行环境,检查许可证(如果启用)。
- 解密器(Decryptor):包含了解密算法的逻辑和解密密钥。注意,在早期或简单模式下,解密密钥可能以某种形式(如字符串常量、简单运算后)硬编码在包裹脚本中。在高强度模式下,密钥可能被分割、混淆或依赖外部输入。
- 加密数据块:这就是被加密的原始脚本字节码,通常以一大段Base64编码的字符串或者字节数组的形式存储在脚本的某个变量或数据结构中。
- 执行器:在内存中解密字节码后,通过Python的
exec()或types.CodeType等机制,动态加载并执行解密后的代码。
第三步:注入运行时依赖。生成的包裹脚本必须与Pyarmor的运行时文件(通常是pytransform相关模块)一起分发。这些运行时文件提供了底层解密函数、反调试检测和许可证验证等功能的真正实现。包裹脚本中的解密器往往只是“桥头堡”,最终会调用这些运行时模块中的C语言扩展(.so或.pyd文件)来完成高强度解密和反调试检查。
静态解密方案的目标,主要聚焦在第二步。它假设我们可以从包裹脚本中,通过静态分析提取出加密数据块,并定位或推导出解密密钥,从而在离线环境下还原出加密的字节码,进而反编译得到近似原始的Python源码。
2.2 静态解密的可行性窗口
为什么静态解密有可能成功?这源于几个关键的设计权衡:
- 算法已知性:Pyarmor使用的加密算法(如AES)是公开的、标准的。安全性不依赖于算法保密,而依赖于密钥保密。因此,问题的核心从“破解算法”转移到了“寻找密钥”。
- 密钥可能驻留:为了方便运行,解密密钥必须在某个时间点、以某种形式出现在内存或可执行文件中。在静态分析中,如果加密强度配置不高,密钥可能以明文或简单编码的形式存在于包裹脚本的字符串、常量或简单的算术逻辑中。
- 模式固定性:Pyarmor的包裹脚本结构有一定模式可循。例如,加密数据块常被赋值给一个特定变量名(如
encrypted_code),解密函数调用有固定模式。这为自动化工具定位关键代码段提供了特征。 - “1shot”的追求:所谓“1shot”(一击即中),体现了这类工具的理想:通过一套预设的分析规则和模式匹配,自动完成密钥查找、数据提取、解密和反编译的全流程,无需人工交互。
然而,这扇“窗口”正在迅速缩小。新版本的Pyarmor(如8.x以后)不断增强保护:
- 更强的密钥混淆:密钥不再简单存放,可能被分割成多个片段,穿插在无关代码中,或通过一系列复杂的算术和位运算动态合成。
- 依赖运行时:核心解密函数移入C扩展,包裹脚本中只留下一个调用接口。静态分析脚本只能看到一个
pytransform的函数调用,无法直接看到密钥和算法。 - 虚拟机(VMP)保护:对核心解密循环或校验代码使用虚拟化保护,将其转换为自定义的字节码,在自定义的虚拟机中执行,静态分析几乎无法理解其逻辑。
- 反调试与完整性校验:运行时检测调试器,或对脚本文件自身进行校验,一旦发现被修改或处于调试状态,则拒绝执行或输出错误结果。
因此,一个有效的“静态解密1shot”方案,通常针对的是特定版本(如Pyarmor 7.x及之前)或使用了较弱保护选项(未启用VMP、未绑定到特定机器)的加密脚本。对于高强度保护的脚本,静态方法可能只能完成初步的拆包和反混淆,提取出加密块,但无法进行最终解密,需要结合动态分析(调试)手段。
3. 方案设计与工具链选型
构建一个静态解密方案,本质上是打造一个针对Pyarmor包裹脚本的“静态分析流水线”。下面我们来拆解这个流水线的每个环节,以及为什么选择这些工具和方法。
3.1 整体工作流设计
一个典型的“1shot”静态解密工作流包含以下四个核心阶段,如下图所示(概念流程):
输入加密脚本 ↓ [阶段1:解析与特征提取] ├── 语法分析 (AST Parsing) ├── 识别关键变量/函数 └── 定位加密数据块 ↓ [阶段2:密钥提取与推导] ├── 静态数据流分析 ├── 常量传播与简化 └── 尝试匹配已知模式 ↓ [阶段3:数据解密] ├── 提取加密数据 (Base64/Hex解码) ├── 应用解密算法 (如AES) └── 输出解密后的字节码 ↓ [阶段4:代码恢复] ├── 反编译字节码为Python源码 ├── 基础反混淆(可选) └── 输出可读性更高的源码这个流程的目标是完全自动化,但实际中每个阶段都可能需要根据目标脚本的具体特征进行适配。
3.2 核心工具链解析
1. Python AST模块:脚本解析的基石ast(Abstract Syntax Tree)是Python自带的模块,用于将Python源代码解析成语法树。这是整个方案的起点。
- 为什么用它?我们需要理解包裹脚本的结构,而不是直接执行它。
ast允许我们以编程方式遍历代码,查找函数定义、变量赋值、字符串常量等,而不会触发任何可能存在的反调试或恶意代码。 - 关键应用:编写一个AST访问者(
ast.NodeVisitor),专门搜索符合以下特征的节点:- 非常大的字符串常量(可能是Base64编码的加密块)。
- 函数名包含
decrypt、decode、load等关键词的函数定义。 - 对特定模块(如
pytransform)的导入和调用。 - 将大字符串赋值给某个变量的操作。
2. 反编译库:从字节码到源码解密后得到的是Python字节码(.pyc格式或内存中的代码对象)。我们需要将其转换回Python源码。常用工具是uncompyle6或decompyle3。
uncompyle6:支持Python 2.7和3.4到3.8的字节码反编译,成熟稳定。decompyle3:uncompyle6的继任者,专注于更新版本的Python(3.7+),并积极维护。- 选择考量:根据目标脚本编译所用的Python版本选择。对于较旧的Pyarmor加密脚本,
uncompyle6可能兼容性更好;对于新版本,decompyle3是更佳选择。它们并非完美,对于高度混淆或经过特定优化的字节码,反编译出的代码可能包含语法错误或难以理解的变量名,但足以提供核心逻辑。
3. 密码学库:执行解密操作一旦找到加密数据和密钥,就需要使用正确的算法进行解密。Python的cryptography库或pycryptodome库是工业标准。
pycryptodome:功能全面,API友好,是PyCrypto的替代品。它提供了AES、DES、RSA等算法的实现。- 使用场景:假设分析出Pyarmor使用AES-128-CBC模式加密,密钥是
my_secret_key,初始向量(IV)可能附在数据块头部或也是硬编码的。我们需要用Crypto.Cipher.AES来解密。 - 注意事项:关键在于确定加密模式(如CBC、ECB)和填充方式(如PKCS7)。Pyarmor通常使用标准模式,这些信息有时可以从包裹脚本中初始化加密器的代码里推断出来。
4. 自定义静态分析逻辑:串联一切的核心这是项目的灵魂,需要自己编写。它利用ast解析结果,实现模式匹配和启发式搜索。
- 模式匹配:收集不同版本Pyarmor生成的包裹脚本样本,总结其代码模式。例如,加密数据变量名可能为
__pyarmor__、_code等;解密函数可能叫__decrypt。 - 数据流跟踪(简单版):尝试跟踪密钥的生成过程。例如,发现代码中有
key = b'secret' + struct.pack('I', 12345),就需要在AST层面解析这个表达式,计算出最终的密钥值。对于更复杂的混淆,可能需要实现一个简单的符号执行或常量传播分析器。 - 启发式规则:例如,“寻找长度超过500个字符的字符串常量,并尝试将其作为Base64解码”。或者“寻找调用了
code = compile(decrypted_data, ‘<s>’, ‘exec’)的语句,其decrypted_data的上游来源就是我们的目标”。
实操心得:工具链的版本陷阱这里有一个巨大的坑:Python字节码的版本兼容性。Pyarmor加密脚本时,会基于一个特定Python版本生成字节码。如果你用Python 3.9的
uncompyle6去反编译一个由Python 3.6编译的字节码,很可能失败或出错。因此,在解密前,最好能确定原始脚本的Python版本。一个笨办法但有效的方法是:准备多个Python环境(3.6, 3.7, 3.8, 3.9),用不同版本的uncompyle6或decompyle3逐一尝试。或者,更专业一点,可以解析.pyc文件的魔数(magic number)来确定其版本。
4. 关键步骤实现与代码拆解
现在,我们进入实战环节,一步步拆解如何实现这个静态解密工具的核心模块。请注意,以下代码示例旨在阐述原理和思路,是一个高度简化的教学版本,用于对付保护强度不高的旧版Pyarmor脚本。面对现代强保护,需要更复杂的分析。
4.1 步骤一:AST解析与特征提取
首先,我们需要读取加密脚本,并用AST分析其结构。
import ast import base64 import re class PyarmorAnalyzer(ast.NodeVisitor): def __init__(self): self.encrypted_data = None self.encrypted_data_var_name = None self.decrypt_func_name = None self.suspicious_large_strings = [] # 存储可能的大字符串(加密数据) def visit_Assign(self, node): # 遍历赋值语句,寻找可能存储加密数据的变量 for target in node.targets: if isinstance(target, ast.Name): var_name = target.id # 检查赋值右侧是否是一个巨大的字符串常量 if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str): str_value = node.value.value # 启发式规则:长度超长且看起来像Base64或Hex if len(str_value) > 1000: self.suspicious_large_strings.append((var_name, str_value)) # 进一步检查是否包含常见Base64模式(可选) if re.match(r'^[A-Za-z0-9+/]+={0,2}$', str_value): print(f"[*] 发现潜在Base64加密数据,变量名: {var_name}, 长度: {len(str_value)}") self.encrypted_data = str_value self.encrypted_data_var_name = var_name self.generic_visit(node) def visit_FunctionDef(self, node): # 遍历函数定义,寻找可能包含解密逻辑的函数 func_name = node.name # 简单关键词匹配(实际中需要更复杂的模式) if 'decrypt' in func_name.lower() or 'decode' in func_name.lower(): print(f"[*] 发现疑似解密函数: {func_name}") self.decrypt_func_name = func_name # 这里可以进一步分析函数体,寻找密钥和算法 # 例如,可以再写一个visitor来遍历这个函数体内的节点 self.generic_visit(node) def visit_Call(self, node): # 遍历函数调用,寻找对pytransform或exec/compile的调用 if isinstance(node.func, ast.Attribute): # 例如:pytransform.decrypt(...) if node.func.attr == 'decrypt': print(f"[*] 发现对解密函数的调用: {ast.dump(node)}") elif isinstance(node.func, ast.Name): # 例如:exec(decrypted_code) if node.func.id in ('exec', 'eval', 'compile'): print(f"[*] 发现代码执行调用: {node.func.id}") self.generic_visit(node) def analyze_script(file_path): with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() try: tree = ast.parse(content) analyzer = PyarmorAnalyzer() analyzer.visit(tree) return analyzer except SyntaxError as e: print(f"[!] 语法解析错误,脚本可能被混淆或包含非法语法: {e}") return None这个分析器只是一个起点。它找到了大字符串和疑似函数,但真正的密钥和算法可能隐藏在复杂的表达式或函数逻辑中。
4.2 步骤二:密钥提取与算法识别
这是最困难的一步,需要根据具体脚本定制规则。假设我们在某个函数里发现了类似下面的代码模式(经过反混淆后可能的样子):
def __decrypt(data): from Crypto.Cipher import AES import base64 key_part1 = b'x7d*' key_part2 = 0x55 # 模拟一个简单的密钥组合 key = key_part1 + bytes([key_part2] * 12) # 组合成16字节密钥 iv = b'1234567890abcdef' # 假设IV是固定的 cipher = AES.new(key, AES.MODE_CBC, iv) encrypted_bytes = base64.b64decode(data) decrypted_padded = cipher.decrypt(encrypted_bytes) # 去除PKCS7填充 padding_len = decrypted_padded[-1] decrypted = decrypted_padded[:-padding_len] return decrypted我们的静态分析器需要能“理解”这段代码。这可能需要一个更强大的符号执行或数据流分析引擎。作为简化版,我们可以实现一个“模式匹配+简单求值”的策略:
import ast import base64 from Crypto.Cipher import AES class KeyExtractor(ast.NodeVisitor): def __init__(self): self.key = None self.iv = None self.mode = 'CBC' # 假设 def visit_BinOp(self, node): # 尝试解析 key = part1 + part2 这样的操作 if isinstance(node.op, ast.Add): left = self._eval_ast(node.left) right = self._eval_ast(node.right) if left is not None and right is not None: if isinstance(left, bytes) and isinstance(right, bytes): self.key = left + right print(f"[+] 推导出密钥: {self.key}") self.generic_visit(node) def _eval_ast(self, node): """非常简单的AST节点求值,仅处理常量和简单字节转换""" if isinstance(node, ast.Constant): return node.value elif isinstance(node, ast.Bytes): return node.s elif isinstance(node, ast.Call): # 处理 bytes([0x55, ...]) 这种调用 if isinstance(node.func, ast.Name) and node.func.id == 'bytes': if node.args and isinstance(node.args[0], ast.List): elts = node.args[0].elts try: values = [self._eval_ast(e) for e in elts] if all(isinstance(v, int) for v in values): return bytes(values) except: pass # 更多类型处理... return None def extract_key_from_ast(tree, target_func_name): extractor = KeyExtractor() # 首先找到目标函数节点 for node in ast.walk(tree): if isinstance(node, ast.FunctionDef) and node.name == target_func_name: extractor.visit(node) # 遍历该函数体提取信息 break return extractor.key, extractor.iv注意事项:静态分析的局限性上述代码极度简化。现实中,密钥可能通过一系列位运算、查表、或从多个无关函数中收集片段而来。Pyarmor 8+的脚本可能将密钥生成逻辑放在C扩展里,静态分析到此就束手无策了。此时,“静态解密1shot”的前提就不复存在,必须转向动态分析或放弃。
4.3 步骤三:数据解密与字节码提取
一旦我们(侥幸)提取到了密钥和IV,并确定了算法(例如AES-128-CBC),解密过程就相对标准了。
def decrypt_data(encrypted_b64, key, iv, mode='CBC'): """ 使用AES解密数据 :param encrypted_b64: Base64编码的加密数据 :param key: 字节串密钥 :param iv: 字节串初始向量 :param mode: 加密模式,如 'CBC' :return: 解密后的字节串 """ try: encrypted_bytes = base64.b64decode(encrypted_b64) if mode.upper() == 'CBC': cipher = AES.new(key, AES.MODE_CBC, iv) else: # 处理其他模式 raise ValueError(f"不支持的加密模式: {mode}") decrypted_padded = cipher.decrypt(encrypted_bytes) # 尝试PKCS7去除填充 padding_len = decrypted_padded[-1] # 验证填充是否有效 if padding_len < 1 or padding_len > cipher.block_size or decrypted_padded[-padding_len:] != bytes([padding_len]) * padding_len: print(f"[!] 警告:填充验证失败,可能使用了非标准填充或密钥错误。尝试返回原始解密数据。") return decrypted_padded # 返回未去填充的数据 return decrypted_padded[:-padding_len] except Exception as e: print(f"[!] 解密过程中发生错误: {e}") return None解密后的数据很可能就是Python的字节码(一个代码对象序列化后的形式,或者直接是.pyc文件的内容)。我们需要将其反编译。
4.4 步骤四:字节码反编译与输出
import marshal import uncompyle6 import sys from io import StringIO def decompile_bytecode(decrypted_bytes): """ 尝试将解密后的字节码反编译为Python源码 """ try: # 尝试将其作为marshal序列化的代码对象加载 code_obj = marshal.loads(decrypted_bytes) # 使用uncompyle6反编译 output = StringIO() uncompyle6.code_deparse(code_obj, out=output) source_code = output.getvalue() return source_code except (ValueError, TypeError, EOFError): # 如果不是marshal格式,可能是.pyc文件格式(包含魔数和时间戳) print(f"[*] 解密数据不是直接的marshal代码对象,尝试作为.pyc文件解析...") # .pyc文件通常前16字节是魔数和时间戳,之后是marshal数据 if len(decrypted_bytes) > 16: try: # 跳过前16个字节(Python 3.7+) code_obj = marshal.loads(decrypted_bytes[16:]) output = StringIO() uncompyle6.code_deparse(code_obj, out=output) source_code = output.getvalue() return source_code except Exception as e: print(f"[!] 作为.pyc文件解析也失败: {e}") except Exception as e: print(f"[!] 反编译失败: {e}") # 可以尝试其他反编译器,如decompyle3 # import decompyle3 # source_code = decompyle3.decompile_code(code_obj) return None如果一切顺利,source_code变量里就是被还原的Python源代码。不过,这代码很可能还带着Pyarmor第一层混淆(变量名重命名等),可读性较差,但逻辑是完整的。
5. 实战挑战与避坑指南
纸上得来终觉浅,绝知此事要躬行。在实际操作中,你会遇到各种各样的问题。下面是我总结的一些常见挑战和应对策略。
5.1 常见问题与排查技巧
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| AST解析失败,报语法错误 | 1. 脚本被高度混淆,包含非法语法或异常结构。 2. 文件编码问题或包含非文本字节。 | 1. 尝试使用errors='ignore'模式读取文件,跳过非法字符。2. 使用正则表达式或简单文本搜索先提取出疑似代码块,再对代码块进行AST解析。 3. 直接使用文本搜索定位关键字符串和函数名,绕过完整语法分析。 |
| 找到了加密数据,但无法确定密钥 | 1. 密钥被深度混淆或分割。 2. 密钥生成依赖运行时信息(如文件哈希、机器ID)。 3. 核心解密在C扩展中。 | 1. 对疑似密钥生成函数进行更深入的数据流分析,尝试符号执行简化表达式。 2.转向动态分析:在受控环境(沙箱、虚拟机)中运行脚本,使用调试器(如 pyrasite,pyringe)在解密函数被调用时dump内存中的密钥和明文代码。这是对抗强保护的主要手段。3. 分析 pytransform等扩展模块(如果存在),但这需要逆向工程技能。 |
| 解密成功,但反编译失败或输出乱码 | 1. Python版本不匹配。 2. 解密出的不是有效的字节码(密钥错误)。 3. 字节码本身被混淆或破坏。 | 1.确认Python版本:检查原始脚本的运行环境,或尝试多个版本的uncompyle6/decompyle3。2.验证解密:如果可能,用已知的简单加密脚本测试你的解密流程,确保基础功能正确。 3. 解密出的数据可能还需要一层解压缩或额外的变换(XOR等),检查包裹脚本中在 decrypt调用后是否还有zlib.decompress或类似的调用。 |
脚本运行依赖pytransform等外部模块 | 静态分析时,这些模块不存在,导致分析逻辑无法追踪到核心解密。 | 1. 收集这些运行时模块(.so/.pyd文件)。2. 对于简单的调用,可以尝试用Python模拟其接口(返回假数据),让我们的分析脚本能继续执行以观察数据流。 3. 更实际的方法是:在完整环境中运行脚本,并在其调用扩展模块前后进行Hook,截获参数和返回值。 |
| 反编译出的代码变量名全是a,b,c | 这是Pyarmor的标识符重命名混淆,属于第一层保护。 | 1.人工分析:根据上下文逻辑重命名变量。这是个体力活,但对于理解核心算法至关重要。 2.使用基础反混淆工具:有些工具能进行简单的数据流分析,将单次使用的临时变量内联,但效果有限。 3.接受现状:对于审计或恢复逻辑,变量名不重要,控制流和函数调用关系才是关键。 |
5.2 高级对抗:当遇到VMP和反调试
新版本Pyarmor的虚拟化(VMP)和反调试功能是静态解法的“终结者”。
虚拟化(VMP):关键的解密循环或校验逻辑被转换成自定义指令集(字节码),在一个内置的解释器中执行。静态分析看到的只是一大段看似随机的数据和一个解释器循环,完全无法理解其原始逻辑。
- 应对思路:极其困难。可以尝试定位并提取出自定义字节码,然后逆向分析这个简易VM的指令集,但这工作量相当于写一个反汇编器,通常得不偿失。更可行的方法依然是动态分析:在VM解释执行完毕后,内存中会留下解密后的真实代码或密钥,此时进行内存dump。
反调试:脚本会检测是否被调试(如检查
sys.gettrace()),或检测进程名、父进程等。- 应对思路:在动态分析时,需要隐藏调试器。可以使用
ptrace注入、修改Python解释器源码、或使用更底层的调试器(如GDB)附加到进程上。对于简单的检测,可以在脚本开头通过sys.settrace(None)或修改环境变量来绕过。
- 应对思路:在动态分析时,需要隐藏调试器。可以使用
核心心得:静态解密的边界经过多个项目的实践,我深刻认识到“Pyarmor-Static-Unpack-1shot”的适用范围是有限的。它更像是一个针对特定版本、特定配置的自动化辅助工具,而不是一个万能钥匙。它的最大价值在于处理大量使用相同、较弱加密配置的遗留脚本,或者作为动态分析的“前锋”,快速完成初步的拆包和定位工作。对于重要的、高强度的保护目标,动静结合才是王道:用静态分析理清程序结构,找到切入点;用动态调试在运行时捕获关键状态。永远不要指望一个全自动的工具能解决所有问题,分析者的智慧和耐心才是最终的解密密钥。
6. 工具化实践与扩展思路
尽管面临挑战,将上述流程工具化仍然非常有价值,可以极大提升处理同类脚本的效率。
6.1 构建一个简单的命令行工具
我们可以将前面的代码模块整合起来,形成一个简单的命令行工具框架:
# pyarmor_static_unpack.py import argparse import sys from pathlib import Path # 导入前面定义的各个分析模块... def main(): parser = argparse.ArgumentParser(description='Pyarmor静态解密尝试工具 (简化版)') parser.add_argument('input_file', help='被Pyarmor加密的脚本文件路径') parser.add_argument('-o', '--output', help='解密后源码输出文件路径', default='decrypted_output.py') parser.add_argument('--no-decompile', action='store_true', help='只解密字节码,不反编译') args = parser.parse_args() input_path = Path(args.input_file) if not input_path.exists(): print(f"[!] 输入文件不存在: {input_path}") sys.exit(1) print(f"[*] 开始分析文件: {input_path}") analyzer = analyze_script(input_path) if not analyzer or not analyzer.encrypted_data: print(f"[!] 未能在脚本中找到明显的加密数据块。") # 可以尝试其他启发式方法... sys.exit(1) print(f"[*] 找到加密数据,变量名: {analyzer.encrypted_data_var_name}") # 这里需要更智能的密钥提取,假设我们通过某种方式获得了key和iv # 以下为示例,实际需要根据分析结果动态获取 guessed_key = b'this_is_a_guess_key' # 应替换为实际提取逻辑 guessed_iv = b'1234567890abcdef' # 应替换为实际提取逻辑 if not guessed_key: print(f"[!] 无法提取解密密钥,尝试失败。") print(f"[*] 提示:请检查脚本中是否存在名为 '{analyzer.decrypt_func_name}' 的函数,并手动分析其密钥生成逻辑。") sys.exit(1) print(f"[*] 使用密钥进行解密...") decrypted_bytes = decrypt_data(analyzer.encrypted_data, guessed_key, guessed_iv) if not decrypted_bytes: print(f"[!] 解密失败。") sys.exit(1) print(f"[*] 解密成功,数据长度: {len(decrypted_bytes)} 字节") if args.no_decompile: output_path = Path(args.output).with_suffix('.bin') output_path.write_bytes(decrypted_bytes) print(f"[+] 字节码已保存至: {output_path}") else: print(f"[*] 尝试反编译字节码...") source_code = decompile_bytecode(decrypted_bytes) if source_code: output_path = Path(args.output) output_path.write_text(source_code, encoding='utf-8') print(f"[+] 源码反编译成功,已保存至: {output_path}") else: print(f"[!] 反编译失败,已保存原始字节码。") output_path = Path(args.output).with_suffix('.bin') output_path.write_bytes(decrypted_bytes) if __name__ == '__main__': main()这个工具非常初级,真正的核心在于analyze_script和密钥提取逻辑的不断强化和适配。
6.2 未来扩展方向
- 模式库建设:收集不同版本Pyarmor(5.x, 6.x, 7.x, 8.x)生成的样本,建立特征模式库。工具可以自动匹配版本并应用相应的解析规则。
- 符号执行引擎集成:集成像
z3这样的约束求解器,对密钥生成代码进行符号执行,自动求解出密钥值,应对中等强度的混淆。 - 动态分析桥接:当静态分析失败时,工具可以自动生成一个简单的“Hook脚本”,引导用户在调试器中运行目标脚本,并在特定位置断点以提取密钥和代码。
- 反混淆后处理:集成基本的反混淆算法,如常量传播、死代码消除、控制流简化,对反编译出的代码进行初步清理,提升可读性。
- 社区化与插件化:设计一个插件架构,允许用户提交针对特定版本或混淆模式的“解密插件”,不断丰富工具的能力。
最后,我必须强调,所有技术都应当用于合法的目的,例如软件兼容性研究、安全审计、或恢复自己丢失源码的资产。尊重知识产权和软件许可协议是每一位技术从业者的底线。这个项目更像是一把“手术刀”,帮助我们理解保护机制的运作方式,从而能更好地设计自己的保护方案,或者在最必要的时候进行“外科手术”式的分析和恢复。