第四节:符号化输入——创建符号位向量与设置参数

好,咱们继续往下走。上一节我们把angr的环境搭好了,也跑通了第一个demo。但说实话,那个demo太玩具了——直接把二进制文件扔进去,让angr自己从头跑到尾。实际逆向中哪有这么简单?你想想看,真实程序都有输入,有命令行参数,有标准输入流。这些输入怎么让angr理解?

答案就是——符号化

说白了,符号化就是把一个具体的值变成一个「符号」。比如你平时调试时给变量赋值为5,那是具体值。符号化之后,这个变量可以代表「任意可能的整数」。angr会去探索所有可能的路径,看看哪些输入能触发哪些行为。我个人觉得,这是angr最核心、也最迷人的地方。

4.1 符号位向量(BitVector)是什么

先别被名字吓到。BitVector,位向量,其实就是一串二进制位。你可以把它理解成一个「未知的变量」,只不过这个变量在angr的世界里是用比特来表示的。

举个例子:

import angr
import claripy

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

这里 claripy.BVS 就是创建符号位向量的函数。第一个参数是名字,随便起,方便你调试时识别。第二个参数是位宽,32就是32位,也就是4个字节。

核心概念:符号位向量 = 一个未知的、但长度固定的比特序列。它可以是整数、字符、甚至是内存地址。

我在项目中遇到过一个问题:新手容易把位宽和字节数搞混。32位是4字节,64位是8字节。如果你要符号化一个char类型,那就是8位;int是32位;long long是64位。千万别搞错,否则约束求解时会出一些莫名其妙的错误。

4.2 创建符号位向量的几种方式

实际使用中,我们不会只创建单个变量。angr提供了多种创建方式,我整理了一张表:

方法 说明 示例
claripy.BVS(name, size) 创建单个符号位向量 BVS('flag', 64)
claripy.BVV(value, size) 创建具体值的位向量 BVV(0x41, 8) # 字符'A'
claripy.BVS(name, size, explicit_name=True) 强制使用指定名字(避免重命名) BVS('idx', 32, explicit_name=True)

嗯,这里要注意 explicit_name 参数。默认情况下,如果你创建两个同名的符号变量,angr会自动在后面加后缀来区分。但有时候你需要精确控制变量名(比如调试时),那就把这个参数设为True。

4.3 符号化命令行参数(argc/argv)

好了,理论说完了,咱们来点实战。假设你有一个C程序,它接受命令行参数:

// test.c
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv) {
    if (argc != 2) {
        printf("Usage: %s <password>\n", argv[0]);
        return 1;
    }
    if (strcmp(argv[1], "secret123") == 0) {
        printf("Access granted!\n");
    } else {
        printf("Access denied!\n");
    }
    return 0;
}

这个程序很简单,但它是典型的「基于命令行参数做判断」的逆向场景。怎么用angr符号化它的输入?

看代码:

import angr
import claripy

proj = angr.Project('./test', auto_load_libs=False)

# 第一步:创建符号化的argv[1]
# 假设密码最长16字节,我们创建128位(16字节)的符号变量
password_len = 16
password = claripy.BVS('password', password_len * 8)

# 第二步:创建符号化的argv
# argv[0] 是程序名,我们用具体值
# argv[1] 是密码,用符号值
# argv[2] 是NULL结束符
argv = [proj.filename, password, claripy.BVV(0, 64)]  # 注意:指针是64位

# 第三步:设置符号化参数
state = proj.factory.entry_state(
    args=argv,
    argc=claripy.BVV(2, 64)  # argc = 2,固定值
)

个人经验:我刚开始做的时候,总忘记argv最后要加NULL指针。C语言要求argv数组以NULL结尾,angr也遵循这个约定。不加的话,angr会一直读内存读到崩溃。

这里有个细节:argc 我用了 BVV(2, 64),是具体值。为什么?因为在这个场景下,argc是确定的——程序只接受一个参数。但如果你要分析的程序对argc有分支判断(比如argc==1走A逻辑,argc==2走B逻辑),那argc本身也应该符号化。

4.4 符号化标准输入(stdin)

命令行参数搞定了,那标准输入呢?很多CTF题目和实际程序都是从stdin读数据的。比如:

// read_stdin.c
#include <stdio.h>
#include <string.h>

int main() {
    char buf[16];
    fgets(buf, 16, stdin);
    if (strcmp(buf, "flag{hello}\n") == 0) {
        printf("Correct!\n");
    } else {
        printf("Wrong!\n");
    }
    return 0;
}

符号化stdin的方式稍微不同:

import angr
import claripy

proj = angr.Project('./read_stdin', auto_load_libs=False)

# 创建符号化的stdin内容
# 假设输入最多16字节
stdin_size = 16
stdin_symbol = claripy.BVS('stdin', stdin_size * 8)

# 创建状态时,指定stdin
state = proj.factory.entry_state(
    stdin=angr.SimFile('/dev/stdin', content=stdin_symbol)
)

这里用到了 angr.SimFile。它模拟了一个文件对象,content 参数就是文件的内容。我们把符号位向量传进去,angr就知道这个文件的内容是未知的。

避坑指南:我曾经犯过一个错误——stdin的符号变量长度设得太短。比如程序用fgets(buf, 100, stdin),但我只给了16字节的符号变量。结果angr在路径探索时,发现程序读不到足够的数据,直接走异常路径了。记住:符号变量的长度要 >= 程序实际读取的长度。

4.5 符号化输入的核心逻辑图

说了这么多,我画了一张图帮你理清思路:

符号化输入核心逻辑 命令行参数 (argc/argv) 标准输入 (stdin) 文件/网络输入 符号化过程 claripy.BVS(name, bit_width) → 创建符号位向量 angr.SimFile('/dev/stdin', content=symbol) → 模拟文件输入 创建初始状态 state = proj.factory.entry_state(args=..., stdin=...) 符号化状态 → 可进行路径探索

这张图展示了整个流程:从不同的输入源出发,经过符号化处理,最终生成一个包含符号变量的初始状态。有了这个状态,angr才能去探索所有可能的执行路径。

4.6 综合示例:符号化所有输入

最后,给一个综合性的例子。假设程序同时使用命令行参数和stdin:

import angr
import claripy

proj = angr.Project('./complex_input', auto_load_libs=False)

# 符号化命令行参数
arg1 = claripy.BVS('arg1', 8 * 8)    # 8字节
arg2 = claripy.BVS('arg2', 16 * 8)   # 16字节
argv = [proj.filename, arg1, arg2, claripy.BVV(0, 64)]

# 符号化stdin
stdin_data = claripy.BVS('stdin_data', 32 * 8)  # 32字节
stdin_file = angr.SimFile('/dev/stdin', content=stdin_data)

# 创建状态
state = proj.factory.entry_state(
    args=argv,
    argc=claripy.BVV(3, 64),  # 程序名 + 2个参数 = 3
    stdin=stdin_file
)

print("符号化状态创建成功!")
print(f"符号变量列表: {state.solver.get_variables()}")

运行这段代码,你会看到angr成功创建了三个符号变量:arg1、arg2、stdin_data。每个变量都有明确的位宽,angr知道它们代表什么。

我的习惯:每次创建符号变量后,我都会打印一下 state.solver.get_variables() 来确认。这能帮你快速发现变量名冲突或者位宽错误。调试时省不少时间。

好了,这一节的内容就到这。符号化输入是angr的基石,你后面做的所有分析——路径探索、约束求解、漏洞挖掘——都建立在这个基础之上。多动手写几个例子,把BVS、BVV、SimFile这几个API用熟,后面就顺了。


公众号:蓝海资料掘金营,微信deep3321