好的,各位观众老爷们,欢迎来到今天的“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
第四幕:代码反混淆之“拨云见日”
反混淆,就是把混淆过的代码还原成原始代码。 这个过程通常比混淆要困难得多,因为混淆的方式有很多种,而且有些混淆是不可逆的。
反混淆的常见手段包括:
- 静态分析: 分析代码的结构,找出混淆的模式。
- 动态分析: 运行代码,观察代码的行为。
- 符号执行: 用符号值代替具体值,推导代码的执行路径。
- 人工分析: 实在不行,只能靠人肉分析了。
针对上面介绍的混淆方法,我们可以采取以下反混淆策略:
- 变量名还原: 如果变量名只是简单的替换,可以通过分析代码的上下文,推断出变量的含义,然后进行还原。
- 常量还原: 如果常量被替换成表达式,可以计算表达式的值,还原成常量。
- 控制流还原: 如果控制流被平坦化,需要分析状态机的逻辑,还原代码的执行顺序。
- 去除垃圾代码: 如果插入了垃圾代码,可以通过分析代码的依赖关系,找出无用的代码,然后删除。
第五幕: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 远离! 咱们下期再见!