符号化输入:符号位向量、符号化寄存器与内存、符号化文件

好,咱们进入正题。这一章要聊的,是 angr 符号执行里最核心的玩法——怎么把输入变成符号。说白了,就是让程序「猜」你的输入是什么,而不是老老实实给它一个具体值。

我刚开始学 angr 的时候,总觉得这玩意儿很玄乎。后来做多了才明白,符号化其实就是给程序「开天眼」。你想想看,程序本来只能处理 0 和 1,现在你告诉它:「这个变量可以是任何值,你帮我跑一遍,看看哪些路径能走通。」这就是符号化的精髓。

符号位向量:最小的符号单元

先讲最基础的东西——符号位向量。在 angr 里,我们用 claripy.BVS() 来创建它。BVS 是 BitVectorSymbol 的缩写,说白了就是一个「符号化的位向量」。

核心概念:符号位向量 = 一个名字 + 一个位宽。它不代表具体数值,而是代表「所有可能的数值」。

import angr
import claripy

# 创建一个 32 位的符号位向量
sym_var = claripy.BVS('my_input', 32)

# 也可以指定具体范围
sym_byte = claripy.BVS('byte_input', 8)  # 一个字节
sym_word = claripy.BVS('word_input', 16) # 两个字节

我个人习惯给符号变量起有意义的名字,比如 password_inputserial_key。为什么?因为调试的时候,你看到 password_input_42 比看到 BVS_1 要舒服得多。我在项目里吃过这个亏——符号变量一多,名字乱起,最后自己都分不清哪个是哪个。

小技巧:符号位向量的名字会出现在求解结果里。比如你约束求解后,得到 password_input = 0x12345678,一看名字就知道这是密码输入。

符号化寄存器与内存:让程序「猜」状态

有了符号位向量,下一步就是把它塞进程序的状态里。angr 提供了两种方式:符号化寄存器和符号化内存。

先看寄存器。假设我们想符号化 EAX 寄存器:

proj = angr.Project('target_binary')
state = proj.factory.blank_state()

# 符号化 EAX 寄存器
sym_eax = claripy.BVS('eax_sym', 32)
state.regs.eax = sym_eax

嗯,这里要注意。直接赋值寄存器,angr 会把整个 32 位都符号化。但有时候你只想符号化低 16 位,或者某个特定字节。这时候可以用位提取:

# 只符号化 EAX 的低 16 位
sym_ax = claripy.BVS('ax_sym', 16)
state.regs.eax = claripy.Concat(claripy.BVV(0, 16), sym_ax)

内存符号化也类似,但更灵活。你可以指定地址和大小:

# 符号化地址 0x1000 开始的 64 个字节
sym_buffer = claripy.BVS('buffer', 64 * 8)  # 64 字节 = 512 位
state.memory.store(0x1000, sym_buffer)

# 也可以只符号化某个地址的一个字节
sym_byte = claripy.BVS('byte_at_0x2000', 8)
state.memory.store(0x2000, sym_byte)

避坑指南:我曾经犯过一个错误——符号化内存时没注意对齐。angr 对未对齐的内存访问处理得不太好,尤其是 ARM 架构下。建议你符号化内存时,尽量按 4 字节或 8 字节对齐。

符号化文件:处理输入输出的终极武器

讲完寄存器和内存,咱们聊聊文件。很多软件保护都依赖文件输入——读取配置文件、验证 license 文件、检查序列号文件。符号化文件,就是让 angr 把这些文件内容也变成「可猜测」的。

angr 的 SimFile 对象就是干这个的。看个例子:

# 创建一个符号化的文件
sym_file_content = claripy.BVS('file_content', 100 * 8)  # 100 字节
sim_file = angr.SimFile('license.key', content=sym_file_content)

# 把文件挂载到状态里
state.fs.insert('license.key', sim_file)

这样,当程序读取 license.key 时,angr 会把它当作符号值处理。程序会沿着所有可能的路径执行,直到找到满足条件的文件内容。

我做过一个实际案例:一个软件用 RSA 加密了 license 文件,然后程序启动时解密并验证。传统方法你得逆向 RSA 算法,找到私钥。但用符号执行,你直接把加密后的文件内容符号化,让 angr 自己去探索哪些内容能通过验证。结果?它找到了一个「等价」的解密结果,虽然和原始 license 不一样,但能通过校验。

核心思路:符号化文件 = 把文件内容变成符号变量 + 让程序自己探索哪些内容能走通路径。

实战:符号化一个简单的密码验证

光说不练假把式。咱们写个完整的例子,把上面三个知识点串起来。

假设目标程序是这样:

// 伪代码
int main() {
    char input[16];
    read(0, input, 16);  // 从 stdin 读取
    if (input[0] == 'A' && input[1] == 'B') {
        // 成功路径
        print("Correct!");
    } else {
        // 失败路径
        print("Wrong!");
    }
}

用 angr 符号化输入:

import angr
import claripy

proj = angr.Project('password_check')
state = proj.factory.blank_state()

# 符号化 stdin 输入
sym_stdin = claripy.BVS('stdin', 16 * 8)  # 16 字节
state.posix.files[0].read_from(0, sym_stdin, 16)

# 创建模拟管理器
simgr = proj.factory.simulation_manager(state)

# 探索到成功路径
simgr.explore(find=lambda s: b"Correct" in s.posix.dumps(1))

if simgr.found:
    solution = simgr.found[0]
    # 提取符号化输入的求解结果
    input_val = solution.solver.eval(sym_stdin, cast_to=bytes)
    print(f"找到密码: {input_val}")
else:
    print("未找到可行路径")

你看,整个过程我们没写任何具体的输入值。angr 自己「猜」出了 AB 开头的输入。这就是符号化的威力。

个人经验:符号化输入时,尽量缩小符号变量的范围。比如你知道输入是可见字符,可以加约束 sym_byte >= 0x20 && sym_byte <= 0x7e。这样求解器跑得快很多。

知识体系总览

下面这张图,是我自己总结的符号化输入知识体系。你看一遍,应该能对本章内容有个整体把握。

符号化输入知识体系 符号化输入 符号位向量 符号化寄存器与内存 符号化文件 claripy.BVS(name, size) 位宽:8/16/32/64 位 state.regs.eax = sym_var state.memory.store(addr, sym) angr.SimFile(name, content) state.fs.insert(path, simfile) 核心思想:把具体值变成符号变量 让程序自己探索所有可能的输入路径

这张图把本章的三个核心知识点串起来了。你从上往下看:符号位向量是基础,它构成了符号化寄存器、内存和文件的「砖块」。而这三者,最终都服务于同一个目标——让程序自己探索输入的可能性。

好了,这一章的内容就到这儿。记住一句话:符号化不是魔法,它只是把「猜」的过程自动化了。你给 angr 一个符号变量,它帮你跑完所有可能性。就这么简单。

专注资料整理