Python 基于 AST 的代码混淆与反混淆技巧

好的,各位观众老爷们,欢迎来到今天的“Python AST 魔法秀”!今天咱们不表演变魔术,咱们表演“代码变形记”,啊不,是代码混淆与反混淆。

话说天下代码,写出来是给人看的,但有时候,我们又不想让别人轻易看懂,想给它加点“障眼法”。这时候,代码混淆就派上用场了。而反混淆呢?那就是解开这些障眼法,还原代码的真相。

那么,为啥要用 AST 呢?因为 AST (Abstract Syntax Tree,抽象语法树) 是理解代码结构的关键。直接操作字符串?那太 low 了,容易出错,而且不够优雅。AST 就像代码的骨架,我们直接在骨架上动刀子,那才是真正的“外科手术”级别的混淆。

第一幕:AST 入门扫盲

先别急着搬板凳,咱们先来了解一下 AST 是个啥玩意儿。

想象一下,你写了一行简单的 Python 代码:x = 1 + 2

这行代码,在 Python 解释器眼里,可不是简单的字符串,它会被解析成一棵树,这就是 AST。 这棵树大概长这样 (简化版):

Assign
  |
  +-- targets: Name (id='x')
  |
  +-- value: BinOp
        |
        +-- left: Constant (value=1)
        |
        +-- op: Add
        |
        +-- right: Constant (value=2)

看不懂?没关系,我们用代码来生成这棵树:

import ast

code = "x = 1 + 2"
tree = ast.parse(code)

print(ast.dump(tree, indent=4))

运行这段代码,你会看到类似以下的输出:

Module(
    body=[
        Assign(
            targets=[
                Name(id='x', ctx=Store())
            ],
            value=BinOp(
                left=Constant(value=1),
                op=Add(),
                right=Constant(value=2)
            ),
            type_comment=None
        )
    ],
    type_ignores=[]
)

这就是 AST 的文本表示形式。 Module 是根节点,Assign 是赋值语句节点,Name 是变量名节点,BinOp 是二元运算节点,Constant 是常量节点,等等。 每个节点都有自己的属性,比如 Name 节点有 id 属性 (变量名),Constant 节点有 value 属性 (常量值)。

第二幕:AST 节点巡礼

AST 里面有很多种节点,每种节点代表不同的语法结构。 常见的节点类型包括:

节点类型 描述 示例
Module 模块,是 AST 的根节点 ast.parse("print('Hello')")
FunctionDef 函数定义 def my_func(): pass
AsyncFunctionDef 异步函数定义 async def my_func(): pass
ClassDef 类定义 class MyClass: pass
Return 返回语句 return 1
Assign 赋值语句 x = 1
AugAssign 增强赋值语句 (例如 +=, -=) x += 1
AnnAssign 带类型注解的赋值语句 x: int = 1
For for 循环 for i in range(10): pass
AsyncFor 异步 for 循环 async for i in range(10): pass
While while 循环 while True: pass
If if 语句 if x > 0: pass
With with 语句 with open("file.txt") as f: pass
AsyncWith 异步 with 语句 async with open("file.txt") as f: pass
Try try…except 语句 try: pass except: pass
Raise 抛出异常 raise ValueError("Error")
Assert 断言语句 assert x > 0
Import 导入模块 import os
ImportFrom 从模块导入 from os import path
Expr 表达式语句 1 + 2
Pass pass 语句 pass
Break break 语句 break
Continue continue 语句 continue
Name 变量名 x
Constant 常量 (数字、字符串、布尔值、None) 1, "hello", True, None
BinOp 二元运算 (例如 +, -, *) 1 + 2
UnaryOp 一元运算 (例如 -, not) -x, not True
Call 函数调用 print("hello")
Attribute 属性访问 (例如 obj.name) obj.name
Subscript 索引访问 (例如 list[0]) list[0]
List 列表 [1, 2, 3]
Tuple 元组 (1, 2, 3)
Dict 字典 {"a": 1, "b": 2}
Set 集合 {1, 2, 3}
Compare 比较运算 (例如 ==, >, <) x == 1, x > 0
BoolOp 布尔运算 (例如 and, or) x and y, x or y
IfExp 条件表达式 (三元运算符) x if y else z
Lambda lambda 表达式 lambda x: x + 1
FormattedValue 格式化字符串字面量中的值 (f-string) f"{x=}"
JoinedStr 格式化字符串字面量 (f-string) f"hello {name}"

这些节点类型,就像乐高积木,可以组合成各种各样的代码结构。

第三幕:代码混淆之“乾坤大挪移”

现在,咱们来玩点刺激的,开始代码混淆。

1. 变量名替换 (Variable Renaming)

这是最常见的混淆手段,把有意义的变量名改成无意义的,让人摸不着头脑。

import ast
import astor  # 需要安装:pip install astor

def rename_variables(tree, prefix="var_"):
    """
    重命名变量。

    Args:
        tree: AST 树。
        prefix: 变量名前缀。

    Returns:
        修改后的 AST 树。
    """
    variable_counter = 0
    name_map = {}  # 存储旧变量名和新变量名的映射关系

    class VariableRenamer(ast.NodeTransformer):
        def visit_Name(self, node):
            nonlocal variable_counter
            if isinstance(node.ctx, (ast.Store, ast.Load)) and node.id not in name_map:
                name_map[node.id] = prefix + str(variable_counter)
                variable_counter += 1
            if node.id in name_map:
                node.id = name_map[node.id]
            return node

    renamer = VariableRenamer()
    new_tree = renamer.visit(tree)
    return new_tree

# 示例
code = """
def calculate_sum(a, b):
    result = a + b
    return result
"""
tree = ast.parse(code)
obfuscated_tree = rename_variables(tree)
obfuscated_code = astor.to_source(obfuscated_tree)

print("原始代码:")
print(code)
print("n混淆后的代码:")
print(obfuscated_code)

运行结果:

原始代码:
def calculate_sum(a, b):
    result = a + b
    return result

混淆后的代码:
def var_0(var_1, var_2):
    var_3 = var_1 + var_2
    return var_3

解释:

  • rename_variables 函数接收 AST 树和一个可选的前缀。
  • VariableRenamer 类继承自 ast.NodeTransformer,用于遍历和修改 AST 节点。
  • visit_Name 方法是关键,它会在遇到 Name 节点时被调用。
  • 我们只重命名 ast.Store (赋值目标) 和 ast.Load (读取变量) 上下文中的变量。
  • 使用 name_map 字典来存储旧变量名和新变量名的映射关系,避免重复重命名。
  • astor.to_source 函数将修改后的 AST 树转换回源代码。

2. 常量替换 (Constant Obfuscation)

把常量变成表达式,或者用其他方式隐藏。

import ast
import astor
import random

def obfuscate_constants(tree):
    """
    混淆常量。

    Args:
        tree: AST 树。

    Returns:
        修改后的 AST 树。
    """

    class ConstantObfuscator(ast.NodeTransformer):
        def visit_Constant(self, node):
            if isinstance(node.value, (int, float)):
                # 将数字常量替换为表达式
                value = node.value
                op = random.choice([ast.Add, ast.Sub, ast.Mult, ast.Div])() #选择加减乘除之一
                num = random.randint(1, 10)
                new_value = ast.BinOp(
                    left=ast.Constant(value=value + num if isinstance(op, ast.Sub) else value - num if isinstance(op, ast.Add) else value * num if isinstance(op, ast.Div) else value / num),
                    op=op,
                    right=ast.Constant(value=num)
                )
                return new_value
            elif isinstance(node.value, str):
                # 可以添加字符串混淆逻辑,例如字符串拼接、Base64 编码等
                pass
            return node

    obfuscator = ConstantObfuscator()
    new_tree = obfuscator.visit(tree)
    return new_tree

# 示例
code = """
x = 10
y = "hello"
"""
tree = ast.parse(code)
obfuscated_tree = obfuscate_constants(tree)
obfuscated_code = astor.to_source(obfuscated_tree)

print("原始代码:")
print(code)
print("n混淆后的代码:")
print(obfuscated_code)

运行结果 (可能不同,因为有随机性):

原始代码:
x = 10
y = "hello"

混淆后的代码:
x = 14 - 4
y = 'hello'

解释:

  • obfuscate_constants 函数遍历 AST 树,找到 Constant 节点。
  • 如果是数字常量,就把它替换成一个加法或减法表达式。
  • 如果是字符串常量,可以添加更复杂的混淆逻辑,比如字符串拼接、Base64 编码等。

3. 控制流平坦化 (Control Flow Flattening)

这是比较高级的混淆技巧,把代码的控制流打乱,让代码逻辑变得难以追踪。 简单来说,就是把代码块拆分成多个小块,然后用一个状态机来控制代码的执行顺序。

这部分代码比较复杂,这里给出一个简化的示例,展示如何将一个简单的 if 语句平坦化:

import ast
import astor

def flatten_if_statement(tree):
    """
    平坦化 if 语句。 (简化版,仅作演示)

    Args:
        tree: AST 树。

    Returns:
        修改后的 AST 树。
    """

    class IfFlattener(ast.NodeTransformer):
        def visit_If(self, node):
            # 创建一个状态变量
            state_var = ast.Name(id='state', ctx=ast.Load())  # 读取状态变量
            state_store = ast.Name(id='state', ctx=ast.Store()) # 写入状态变量
            state_init = ast.Assign(targets=[state_store], value=ast.Constant(value=0))

            # 创建一个字典,存储代码块和对应的状态值
            blocks = {
                0: node.body,  # if 的 body
                1: node.orelse if node.orelse else [ast.Pass()] # else 的 body,如果没有else,则pass
            }

            # 创建一个 case 列表
            cases = []
            for state, body in blocks.items():
                cases.append(ast.If(test=ast.Compare(left=state_var, ops=[ast.Eq()], comparators=[ast.Constant(value=state)]),
                                      body=body,
                                      orelse=[]))

            # 创建一个 while 循环,循环执行 case 列表
            while_loop = ast.While(test=ast.Constant(value=True),  # 无限循环
                                   body=cases,
                                   orelse=[])

            # 返回新的代码块
            return [state_init, while_loop]

    flattener = IfFlattener()
    new_tree = flattener.visit(tree)
    return new_tree

# 示例
code = """
x = 10
if x > 5:
    print("x is greater than 5")
else:
    print("x is not greater than 5")
"""
tree = ast.parse(code)
obfuscated_tree = flatten_if_statement(tree)
obfuscated_code = astor.to_source(obfuscated_tree)

print("原始代码:")
print(code)
print("n混淆后的代码:")
print(obfuscated_code)

运行结果:

原始代码:
x = 10
if x > 5:
    print("x is greater than 5")
else:
    print("x is not greater than 5")

混淆后的代码:
state = 0
while True:
    if state == 0:
        print('x is greater than 5')
    if state == 1:
        print('x is not greater than 5')

注意: 这个例子只是一个简化的演示,真正的控制流平坦化要复杂得多,需要考虑更多的情况,比如循环、异常处理等。 而且这个例子的结果并不等价于原始代码,因为没有对x > 5这个条件做处理。

4. 插入垃圾代码 (Dummy Code Insertion)

在代码中插入一些无用的代码,增加代码的复杂度。

import ast
import astor
import random

def insert_dummy_code(tree, num_statements=3):
    """
    插入垃圾代码。

    Args:
        tree: AST 树。
        num_statements: 插入的垃圾代码数量。

    Returns:
        修改后的 AST 树。
    """

    class DummyCodeInserter(ast.NodeTransformer):
        def visit_FunctionDef(self, node):
            # 在函数体的开头插入垃圾代码
            dummy_statements = []
            for _ in range(num_statements):
                # 生成随机的垃圾代码,例如无意义的变量赋值
                var_name = "dummy_" + str(random.randint(1000, 9999))
                value = random.randint(1, 100)
                dummy_statement = ast.Assign(
                    targets=[ast.Name(id=var_name, ctx=ast.Store())],
                    value=ast.Constant(value=value)
                )
                dummy_statements.append(dummy_statement)
            node.body = dummy_statements + node.body
            return node

    inserter = DummyCodeInserter()
    new_tree = inserter.visit(tree)
    return new_tree

# 示例
code = """
def my_function(x, y):
    result = x + y
    return result
"""
tree = ast.parse(code)
obfuscated_tree = insert_dummy_code(tree)
obfuscated_code = astor.to_source(obfuscated_tree)

print("原始代码:")
print(code)
print("n混淆后的代码:")
print(obfuscated_code)

运行结果 (可能不同,因为有随机性):

原始代码:
def my_function(x, y):
    result = x + y
    return result

混淆后的代码:
def my_function(x, y):
    dummy_3538 = 5
    dummy_8540 = 34
    dummy_5554 = 36
    result = x + y
    return result

第四幕:代码反混淆之“拨云见日”

反混淆,就是把混淆过的代码还原成原始代码。 这个过程通常比混淆要困难得多,因为混淆的方式有很多种,而且有些混淆是不可逆的。

反混淆的常见手段包括:

  • 静态分析: 分析代码的结构,找出混淆的模式。
  • 动态分析: 运行代码,观察代码的行为。
  • 符号执行: 用符号值代替具体值,推导代码的执行路径。
  • 人工分析: 实在不行,只能靠人肉分析了。

针对上面介绍的混淆方法,我们可以采取以下反混淆策略:

  1. 变量名还原: 如果变量名只是简单的替换,可以通过分析代码的上下文,推断出变量的含义,然后进行还原。
  2. 常量还原: 如果常量被替换成表达式,可以计算表达式的值,还原成常量。
  3. 控制流还原: 如果控制流被平坦化,需要分析状态机的逻辑,还原代码的执行顺序。
  4. 去除垃圾代码: 如果插入了垃圾代码,可以通过分析代码的依赖关系,找出无用的代码,然后删除。

第五幕:AST 实战演练

说了这么多理论,咱们来点实际的。 假设我们有一段被混淆的代码,如下所示:

def var_0(var_1, var_2):
    var_3 = var_1 + var_2
    return var_3

这段代码的变量名被替换了,我们需要把它还原成原始代码。

import ast
import astor

def deobfuscate_variables(tree, name_map):
    """
    还原变量名。

    Args:
        tree: AST 树。
        name_map: 旧变量名和新变量名的映射关系。

    Returns:
        修改后的 AST 树。
    """

    class VariableDeobfuscator(ast.NodeTransformer):
        def visit_Name(self, node):
            if node.id in name_map:
                node.id = name_map[node.id]
            return node

    deobfuscator = VariableDeobfuscator()
    new_tree = deobfuscator.visit(tree)
    return new_tree

# 示例
obfuscated_code = """
def var_0(var_1, var_2):
    var_3 = var_1 + var_2
    return var_3
"""
tree = ast.parse(obfuscated_code)

# 假设我们通过分析,知道了变量名的映射关系
name_map = {
    "var_0": "calculate_sum",
    "var_1": "a",
    "var_2": "b",
    "var_3": "result"
}

# 反转 name_map,因为 deobfuscate 需要的是新变量名到旧变量名的映射
reverse_name_map = {v: k for k, v in name_map.items()}

deobfuscated_tree = deobfuscate_variables(tree, reverse_name_map)
deobfuscated_code = astor.to_source(deobfuscated_tree)

print("混淆后的代码:")
print(obfuscated_code)
print("n反混淆后的代码:")
print(deobfuscated_code)

运行结果:

混淆后的代码:
def var_0(var_1, var_2):
    var_3 = var_1 + var_2
    return var_3

反混淆后的代码:
def calculate_sum(a, b):
    result = a + b
    return result

这个例子演示了如何使用 AST 来进行简单的反混淆。 实际的反混淆过程要复杂得多,需要结合多种技术和工具。

第六幕:总结与展望

今天我们一起学习了基于 AST 的代码混淆与反混淆技巧。 希望大家能够掌握 AST 的基本概念和使用方法,并能够灵活运用这些技巧来保护自己的代码,或者分析别人的代码。

当然,代码混淆只是一种安全手段,并不能完全防止代码被破解。 真正的安全需要从多个方面入手,比如代码签名、权限控制、安全审计等。

最后,祝大家代码安全,bug 远离! 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注