符号化输入:符号位向量、符号化寄存器与内存、符号化文件
好,咱们进入正题。这一章要聊的,是 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_input、serial_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。这样求解器跑得快很多。
知识体系总览
下面这张图,是我自己总结的符号化输入知识体系。你看一遍,应该能对本章内容有个整体把握。
这张图把本章的三个核心知识点串起来了。你从上往下看:符号位向量是基础,它构成了符号化寄存器、内存和文件的「砖块」。而这三者,最终都服务于同一个目标——让程序自己探索输入的可能性。
好了,这一章的内容就到这儿。记住一句话:符号化不是魔法,它只是把「猜」的过程自动化了。你给 angr 一个符号变量,它帮你跑完所有可能性。就这么简单。