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

好的,让我们来一场关于 Python 基于 AST 的代码混淆与反混淆的讲座,用尽量接地气的方式,深入浅出地聊聊这个略显高深的话题。

各位听众,欢迎来到今天的“代码变形记”现场!

今天我们要聊的是代码混淆,听起来像魔术,但其实是门技术活。想象一下,你辛辛苦苦写的代码,不想被别人轻易看懂、复制,这时候代码混淆就能派上用场。而 AST (Abstract Syntax Tree,抽象语法树) 就像是代码的X光片,让我们能深入代码的骨骼,对其进行改造。

什么是AST?

简单来说,AST 是源代码的树状结构表示。编译器和解释器会先将代码解析成 AST,然后再进行后续的处理,比如优化、编译等等。

举个例子,对于这段简单的 Python 代码:

x = 1 + 2 * 3

它的 AST 大概长这样(简化版):

Assign(
    targets=[Name(id='x', ctx=Store())],
    value=BinOp(
        left=Constant(value=1),
        op=Add(),
        right=BinOp(
            left=Constant(value=2),
            op=Mult(),
            right=Constant(value=3)
        )
    )
)

可以看到,AST 把代码拆解成了赋值 (Assign)、变量名 (Name)、二元操作 (BinOp)、常量 (Constant) 等等节点,以及它们之间的关系。

为什么要用 AST 做混淆?

因为 AST 提供了代码的结构化表示,我们可以直接操作这些结构,实现各种复杂的代码变换,而不只是简单的字符串替换。字符串替换很容易被破解,而基于 AST 的混淆,效果通常更好,也更难逆向。

混淆的基本思路

基于 AST 的代码混淆,核心思路就是:

  1. 解析代码: 将 Python 代码解析成 AST。
  2. 修改 AST: 对 AST 进行各种变换,改变代码的结构和外观,但不改变其功能。
  3. 生成代码: 将修改后的 AST 转换回 Python 代码。

混淆的常见手段

接下来,我们来聊聊一些常见的基于 AST 的代码混淆手段,并结合代码示例进行讲解。

  1. 变量名混淆:

    这是最基本,也是最常用的混淆手段。把有意义的变量名改成无意义的字符串,增加代码的可读性难度。

    import ast
    import random
    import string
    
    def random_string(length=10):
        letters = string.ascii_lowercase
        return ''.join(random.choice(letters) for i in range(length))
    
    class NameObfuscator(ast.NodeTransformer):
        def __init__(self):
            self.name_map = {}
    
        def visit_Name(self, node):
            if node.id not in self.name_map:
                self.name_map[node.id] = random_string()
            node.id = self.name_map[node.id]
            return node
    
    # 示例代码
    code = """
    def my_function(input_value):
        result = input_value * 2
        return result
    """
    
    # 解析代码
    tree = ast.parse(code)
    
    # 混淆变量名
    obfuscator = NameObfuscator()
    new_tree = obfuscator.visit(tree)
    
    # 生成代码
    new_code = ast.unparse(new_tree)
    print(new_code)

    这段代码会把 my_functioninput_valueresult 等变量名替换成随机字符串。

    效果: 代码变得难以阅读,不容易理解变量的含义。

  2. 常量混淆:

    把常量的值进行编码或者变换,让攻击者不容易直接看到原始值。

    import ast
    import base64
    
    class ConstantObfuscator(ast.NodeTransformer):
        def visit_Constant(self, node):
            if isinstance(node.value, (int, str)):
                # 将常量编码为 Base64
                encoded_value = base64.b64encode(str(node.value).encode()).decode()
                # 创建一个调用 base64.b64decode 的节点
                new_node = ast.Call(
                    func=ast.Attribute(
                        value=ast.Name(id='base64', ctx=ast.Load()),
                        attr='b64decode',
                        ctx=ast.Load()
                    ),
                    args=[ast.Constant(value=encoded_value)],
                    keywords=[]
                )
                return new_node
            return node
    
    # 示例代码
    code = """
    x = 10
    message = "Hello, world!"
    """
    
    # 解析代码
    tree = ast.parse(code)
    
    # 混淆常量
    obfuscator = ConstantObfuscator()
    new_tree = obfuscator.visit(tree)
    
    # 生成代码
    new_code = ast.unparse(new_tree)
    print(new_code)

    这段代码会将整数和字符串常量编码成 Base64 字符串,并在代码中调用 base64.b64decode 来还原。

    效果: 隐藏了常量的值,增加了逆向难度。

  3. 控制流混淆:

    改变代码的控制流结构,比如插入无用的条件分支、循环,或者改变循环的结构。

    import ast
    import random
    
    class ControlFlowObfuscator(ast.NodeTransformer):
        def visit_If(self, node):
            # 随机插入一个 always True 的条件分支
            always_true = ast.Constant(value=True)
            dummy_node = ast.Pass()  # 什么也不做
            node.orelse.append(ast.If(test=always_true, body=[dummy_node], orelse=[]))
            return node
    
    # 示例代码
    code = """
    if x > 5:
        print("x is greater than 5")
    else:
        print("x is not greater than 5")
    """
    
    # 解析代码
    tree = ast.parse(code)
    
    # 混淆控制流
    obfuscator = ControlFlowObfuscator()
    new_tree = obfuscator.visit(tree)
    
    # 生成代码
    new_code = ast.unparse(new_tree)
    print(new_code)

    这段代码会在 if 语句的 else 分支中插入一个永远为真的条件分支,增加代码的复杂度。

    效果: 使代码的执行流程更加复杂,难以跟踪。

  4. 插入垃圾代码:

    在代码中插入一些没有任何实际作用的代码,增加代码的长度和复杂度。

    import ast
    import random
    
    class JunkCodeInserter(ast.NodeTransformer):
        def visit_FunctionDef(self, node):
            # 在函数体开头插入一些垃圾代码
            junk_code = [
                ast.Assign(
                    targets=[ast.Name(id='_junk', ctx=ast.Store())],
                    value=ast.Constant(value=random.randint(1, 100))
                ),
                ast.Expr(value=ast.Call(func=ast.Name(id='str', ctx=ast.Load()), args=[ast.Name(id='_junk', ctx=ast.Load())], keywords=[]))
            ]
            node.body = junk_code + node.body
            return node
    
    # 示例代码
    code = """
    def my_function(x):
        return x * 2
    """
    
    # 解析代码
    tree = ast.parse(code)
    
    # 插入垃圾代码
    obfuscator = JunkCodeInserter()
    new_tree = obfuscator.visit(tree)
    
    # 生成代码
    new_code = ast.unparse(new_tree)
    print(new_code)

    这段代码会在每个函数体的开头插入一些无用的赋值和函数调用。

    效果: 增加代码的体积,干扰分析。

  5. 操作符替换:

    用等价但更复杂的表达式替换简单的操作符。例如,用 a + b 替换 a - (-b)

    import ast
    
    class OperatorObfuscator(ast.NodeTransformer):
        def visit_BinOp(self, node):
            if isinstance(node.op, ast.Sub):
                # a - b  =>  a + (-b)
                node.op = ast.Add()
                node.right = ast.UnaryOp(op=ast.USub(), operand=node.right)
            return node
    
    # 示例代码
    code = """
    x = a - b
    """
    
    # 解析代码
    tree = ast.parse(code)
    
    # 混淆操作符
    obfuscator = OperatorObfuscator()
    new_tree = obfuscator.visit(tree)
    
    # 生成代码
    new_code = ast.unparse(new_tree)
    print(new_code)

    这段代码会将减法操作替换成加法和负号操作。

    效果: 使代码更难阅读,稍微增加逆向难度。

反混淆的思路

既然有混淆,自然就有反混淆。反混淆的目的是将混淆后的代码还原成可读性更高的形式。反混淆的难度取决于混淆的复杂程度。对于简单的混淆,我们可以手动分析代码,逐步还原。对于复杂的混淆,可能需要借助自动化工具。

反混淆的思路和混淆是相反的:

  1. 分析代码: 仔细阅读混淆后的代码,理解其逻辑。
  2. 去除混淆: 针对不同的混淆手段,采取相应的反制措施。
  3. 重构代码: 将代码重构为更清晰、易懂的形式。

反混淆的常见手段

  1. 变量名还原:

    如果变量名只是被简单地替换成了随机字符串,可以尝试通过代码的上下文来推断变量的含义,并将其替换回有意义的名称。

  2. 常量还原:

    如果常量被编码,比如 Base64 编码,可以使用相应的解码算法将其还原。

  3. 控制流简化:

    去除无用的条件分支、循环,简化代码的控制流结构。

  4. 垃圾代码去除:

    识别并删除没有任何实际作用的垃圾代码。

  5. 操作符还原:

    将复杂的表达式还原成简单的操作符。

示例:反混淆Base64编码的字符串

import base64

obfuscated_code = "base64.b64decode('SGVsbG8sIHdvcmxkIQ==')"

# 提取 Base64 编码的字符串
encoded_string = "SGVsbG8sIHdvcmxkIQ=="

# 解码 Base64 字符串
decoded_string = base64.b64decode(encoded_string).decode()

print(f"反混淆后的字符串: {decoded_string}")

反混淆的工具

  • 在线反混淆工具: 有一些在线工具可以自动反混淆 JavaScript 代码,但对于 Python 代码的支持可能有限。
  • Python 反编译器: 可以将 Python 字节码反编译成源代码,但对于混淆后的代码,反编译的效果可能不佳。
  • 手动分析: 对于复杂的混淆,可能需要人工分析代码,逐步还原。

混淆与反混淆的攻防博弈

代码混淆和反混淆是一场永无止境的攻防博弈。混淆者不断尝试新的混淆手段,反混淆者则不断寻找破解方法。

  • 混淆强度: 混淆强度越高,反混淆的难度越大,但同时也会增加代码的复杂度和性能开销。
  • 反混淆成本: 反混淆需要投入时间和精力,如果反混淆的成本高于获取代码的价值,攻击者可能会放弃。

总结

今天我们聊了 Python 基于 AST 的代码混淆与反混淆。代码混淆是一种保护代码的手段,但并不是万能的。高强度的混淆会带来性能损失,而任何混淆技术都有可能被破解。因此,在选择混淆方案时,需要在安全性、性能和可维护性之间进行权衡。

希望今天的讲座能让你对代码混淆有更深入的了解。记住,代码混淆只是一种辅助手段,更重要的是提高代码的安全性,防范各种攻击。

最后,送给大家一句至理名言:

“代码虐我千百遍,我待代码如初恋。”

感谢大家的收听!我们下次再见!

发表回复

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