Angr使用速记
Angr
由于傻逼省赛不让出网,所以这里稍微记一下东西,其实建议看文献。
参考文献
官方网址:https://angr.io/
介绍和分析:
https://www.52pojie.cn/thread-1861336-1-1.html
https://github.com/jakespringer/angr_ctf/blob/master/SymbolicExecution.pptx
https://1ens.github.io/2024/12/08/符号执行与angr/
https://bbs.kanxue.com/thread-276860.htm
练习学习用:
https://github.com/jakespringer/angr_ctf
https://space.bilibili.com/386563875
https://bbs.kanxue.com/thread-276834.htm
https://www.cnblogs.com/sanyic/p/16965056.html
https://zh-closure.github.io/2024/07/28/通过Angr-CTF入门Angr/#15-angr-arbitrary-read
原理
Angr 被称之为 IR-Based 类的符号执行引擎,他会对输入的二进制重建对应的 CFG ,在完成重建后开始模拟执行。而对于分支语句,就需要分支出两个不同的情况:跳转 和 不跳转 。
环境搭建
pip install angr
流程
先用angr.Project加载一个二进制文件,然后初始化一个SimState对象,来模拟计算机。然后就可以利用各种条件(比如达到地址,约束求解,以及模拟一些对象的变化)达到自己目标地址,来hook出flag或者条件。
符号化模板
需要事先说明的是,现在的Angr优化的非常厉害,下面有很多情况实际上Angr可以自动处理,不过还是了解一下,避免有些麻烦情况。
根据地址寻找
import angr
import sys
def main(argv):
path_to_binary = argv[1] # 可从命令行参数获取二进制文件路径
project = angr.Project(path_to_binary) # 创建 Angr 工程对象
# 初始化程序执行的起始状态,从 main() 函数入口开始执行
initial_state = project.factory.entry_state(
add_options = {
angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY, # 当程序访问了一块“未约束的内存”(也就是 Angr 无法确定里面是什么内容的内存区域,比如未初始化的变量或堆内存)时,Angr 会自动用一个 符号变量(symbolic variable) 去填充这块内存。
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS # 当寄存器(register)中有无法确定的值时,用符号变量来代替这些未约束的寄存器值。
} # 意思就是在程序执行时遇到无法确定是什么的数据(不管是在寄存器里还是内存里),别停下来,先假设它是某个未知的符号变量,继续执行下去。
)
# 创建模拟管理器,用于控制符号执行的路径搜索
simulation = project.factory.simgr(initial_state)
# 设置“Good Job.” 输出对应的目标地址,用于告诉 angr 应该搜索哪个路径
print_good_address = xxxx # TODO
# 设置需要避开的地址
will_not_succeed_address = xxxx # TODO
# 探索可达目标地址的执行路径,避开不需要到达的地址
simulation.explore(find=print_good_address, avoid=will_not_succeed_address)
# 判断是否找到了目标路径
if simulation.found:
# 获取第一个成功到达目标地址的执行状态
solution_state = simulation.found[0]
# 输出 angr 为 stdin 构造的输入,即密码
print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
else:
# 如果未找到路径,抛出异常
raise Exception('未找到可行的解决路径')
if __name__ == '__main__':
main(sys.argv)
根据标准输出流寻找
simulation.explore(find=lambda state: b"xxxx" in state.posix.dumps(sys.stdout.fileno()), avoid=lambda state: b"xxxx" in state.posix.dumps(sys.stdout.fileno()))
符号化寄存器
说句题外话,虽然文章用的是scanf这个例子,但是现在的Angr实际上是支持部分格式化输入了,一般的scanf的用entry_state肯定可以。
# 指定符号执行的起始地址(从 scanf 调用之后开始执行, 跳过angr不能模拟的函数)
start_address = xxxx # TODO
# 创建空白执行状态,从指定地址开始,不自动执行初始化代码
initial_state = project.factory.blank_state(
addr=start_address,
add_options = {
angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS
}
)
# 创建符号变量(BitVector),表示 scanf 读入的多个数值
password_size_in_bits = 32
password0 = claripy.BVS('password0', password_size_in_bits)
password1 = claripy.BVS('password1', password_size_in_bits)
password2 = claripy.BVS('password2', password_size_in_bits)
# 将符号变量赋给寄存器,模拟 scanf 读入数据后寄存器的变化 TODO
initial_state.regs.eax = password0
initial_state.regs.ebx = password1
initial_state.regs.edx = password2
...
# 若找到了正确路径,则求解密码
if simulation.found:
solution_state = simulation.found[0]
# 使用 solver 求解符号变量对应的整数值
solution0 = solution_state.solver.eval(password0)
solution1 = solution_state.solver.eval(password1)
solution2 = solution_state.solver.eval(password2)
# 格式化输出结果(以十六进制形式显示)
solution = ' '.join(map('{:x}'.format, [solution0, solution1, solution2]))
print(solution)
else:
# 若未找到路径,抛出异常
raise Exception('未找到可行的解决路径')
符号化栈
# 地址在scanf后面
# 设置初始栈环境:令 ebp 与 esp 相同,模拟函数内局部栈帧建立前的状态
initial_state.regs.ebp = initial_state.regs.esp
# 创建符号变量,用来代表 scanf 读入的两个数值
password0 = claripy.BVS('password0', 32)
password1 = claripy.BVS('password1', 32)
# 模拟局部栈帧中为 scanf 变量预留的空间(即手动分配栈空间)
padding_length_in_bytes = 8
initial_state.regs.esp -= padding_length_in_bytes
# 将符号变量依次压入栈中(注意顺序要与 scanf 参数的顺序保持一致)
initial_state.stack_push(password0)
initial_state.stack_push(password1)
符号化bss段
state.memory.store(0x083E0DB4, addr1),Angr在内存中存储整数使用大端序,加上参数e可以以小端序存储。
使用initial_state.mem[0x0A1BA1C0].uint64_t = user_input直接注入进去要注意字节序的问题,转成bytes之后要[::-1]。
# 程序调用 scanf("%8s %8s %8s %8s"),因此需准备4个长度为8字节的符号字符串
password0 = claripy.BVS('password0', 8 * 8)
password1 = claripy.BVS('password1', 8 * 8)
password2 = claripy.BVS('password2', 8 * 8)
password3 = claripy.BVS('password3', 8 * 8)
# 将符号变量写入 scanf 会存放输入的全局变量地址
# 这些地址在反汇编中可查到
password0_address = 0xab232c0
initial_state.memory.store(password0_address, password0)
password1_address = 0xab232c8
initial_state.memory.store(password1_address, password1)
password2_address = 0xab232d0
initial_state.memory.store(password2_address, password2)
password3_address = 0xab232d8
initial_state.memory.store(password3_address, password3)
...
# 若找到正确路径,解出符号输入
if simulation.found:
solution_state = simulation.found[0]
# 解出符号字符串(以字节形式求解,转为字符串)
solution0 = solution_state.solver.eval(password0, cast_to=bytes).decode()
solution1 = solution_state.solver.eval(password1, cast_to=bytes).decode()
solution2 = solution_state.solver.eval(password2, cast_to=bytes).decode()
solution3 = solution_state.solver.eval(password3, cast_to=bytes).decode()
# 合并四段输入并打印
solution = ' '.join([solution0, solution1, solution2, solution3])
print(solution)
else:
# 未找到路径则抛出异常
raise Exception('未找到可行的解决路径')
符号化堆
符号化堆内存,关键在于怎么获得内存地址。思路是伪造一个未被使用的内存地址,并修改数据指针指向该地址。这个就非常像游戏外挂找基地址时候的人造指针。
我们让buffer0和buffer1指向我们确定的地址,一般直接在BSS段里面找就行。
# 程序调用 scanf("%8s %8s") — 输入两个长度为 8 字节的字符串
password0 = claripy.BVS('password0', 8 * 8)
password1 = claripy.BVS('password1', 8 * 8)
# 在此题中,scanf 输入会写到由 malloc 分配的内存区域,
# 但 Angr 不模拟 malloc,因此我们手动构造“假堆内存”。
# 1. 指定两个假的堆地址
# 2. 覆写全局指针(指向 malloc 返回的地址)为这些假的堆空间
# 注意:x86 为小端序,因此应显式指定 endness 参数
fake_heap_address0 = 0x4444444
pointer_to_malloc_memory_address0 = 0xa2def74
initial_state.memory.store(
pointer_to_malloc_memory_address0,
fake_heap_address0,
endness=project.arch.memory_endness,
size=4
)
fake_heap_address1 = 0x4444454
pointer_to_malloc_memory_address1 = 0xa2def7c
initial_state.memory.store(
pointer_to_malloc_memory_address1,
fake_heap_address1,
endness=project.arch.memory_endness,
size=4
)
# 将符号字符串写入模拟的“堆内存”中
# 假设 scanf 写入的第一个/第二个字符串分别对应 fake_heap_address0 与 fake_heap_address1
initial_state.memory.store(fake_heap_address0, password0)
initial_state.memory.store(fake_heap_address1, password1)
符号化文件
# 从 fread 调用后的地址开始执行符号化分析
start_address = 0x80488bc
...
# 程序通过 fread 读取文件中的内容来验证密码
# 我们需要构造一个同名的“虚拟文件”,并将符号化内容写入其中
filename = "FOQVSBZB.txt" # fread 读取的文件名(根据反汇编确定)
symbolic_file_size_bytes = 8 # 读取的字节数
# 创建符号化变量作为文件内容(即密码内容)
password = claripy.BVS("password", symbolic_file_size_bytes * 8)
# 创建符号化文件对象,把符号化内容作为文件的读入内容
password_file = angr.storage.SimFile(filename, content=password)
# 将符号化文件插入符号化文件系统(相当于替换掉原始磁盘文件)
initial_state.fs.insert(filename, password_file)
符号化动态库
def main(argv):
# 从命令行参数中获取共享库文件路径
path_to_binary = argv[1]
# 由于 .so 文件采用位置无关代码 (PIC),需要为其指定基址
base = 0x4000000
project = angr.Project(
path_to_binary,
load_options={'main_opts': {'base_addr': base}}
)
# 为函数参数构造符号化数据。第一个参数是指针,第二个参数是长度
buffer_pointer = claripy.BVV(0x3000000, 32) # 缓冲区地址
# validate 函数的偏移地址为 0x670,根据 base_addr 加上偏移得到实际地址
validate_function_address = base + 0x670
# 创建调用状态 (call_state) 模拟从外部调用 validate 函数
# 对应函数原型:int validate(char* buffer, int length)
initial_state = project.factory.call_state(
validate_function_address,
buffer_pointer,
claripy.BVV(8, 32), # 模拟长度参数 length = 8
add_options={
angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS
}
)
# 构造 8 字节符号变量作为用户输入(即密码)
password = claripy.BVS("password", 8 * 8)
# 将符号化密码内容写入内存缓冲区
initial_state.memory.store(buffer_pointer, password)
# 创建符号执行管理器
simulation = project.factory.simgr(initial_state)
# 目标地址:validate 函数将要返回的位置(ret 指令之前)
check_constraint_address = base + 0x71c
simulation.explore(find=check_constraint_address)
# 如果找到了到达返回点的执行状态
if simulation.found:
solution_state = simulation.found[0]
# 在函数返回前,寄存器 eax 存放返回值 (非零视为 True)
# 添加约束:eax != 0 -> 函数返回 True
solution_state.add_constraints(solution_state.regs.eax != 0)
# 求解符号变量 password,使之满足上述约束
solution = solution_state.solver.eval(password, cast_to=bytes).decode()
# 输出求得的密码
print(solution)
else:
raise Exception("未找到可行的解决路径")
if __name__ == "__main__":
main(sys.argv)
符号化静态库
#如果二进制文件被编译成静态二进制文件,我们通过主动替换库函数避免路径爆炸和加速,而且angr为这些写好了SimProcdure
project = angr.Project(path_to_binary,load_options={"auto_load_libs":True})
initial_state = project.factory.entry_state(add_options=angr.options.unicorn)
simulation = project.factory.simgr(initial_state,veritesting=True)
project.hook(0x0804ED40, angr.SIM_PROCEDURES['libc']['printf']())
project.hook(0x0804ED80, angr.SIM_PROCEDURES['libc']['scanf']())
project.hook(0x0804F350, angr.SIM_PROCEDURES['libc']['puts']())
project.hook(0x08048D10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())
优化模板
路径爆炸函数在最后可以考虑find地址然后手动约束
路径爆炸函数之后还有约束,hook比较方便
多次调用路径爆炸函数可以用SimProcdure来hook符号
手动约束
这里实际遇到的是for中的判断,因为这样会造成2的n次方个路径,导致路径爆炸,我们要自己约束一下。
# Angr 无法真正探索到输出 "Good Job." 的分支,所以我们寻找函数调用前的状态
# 当程序到达 check_equals_* 函数调用处的地址时,暂停执行
address_to_check_constraint = 0x8048683
simulation.explore(find=address_to_check_constraint)
# 若找到目标路径,则手动添加字符串匹配约束
if simulation.found:
solution_state = simulation.found[0]
# 获取 check_equals_* 调用时的参数地址
constrained_parameter_address = 0x804a040 # 即 encrypted_input 所在地址
constrained_parameter_size_bytes = 16 # 长度 = 16 字符
constrained_parameter_bitvector = solution_state.memory.load(
constrained_parameter_address,
constrained_parameter_size_bytes
)
# 我们希望让该内存变量等于目标参考字符串(程序中 hardcode 的校验值)
constrained_parameter_desired_value = b"OSIWHBXIFOQVSBZB"
# 添加约束:要求加密结果与目标值相等
solution_state.add_constraints(
constrained_parameter_bitvector == constrained_parameter_desired_value
)
# 求解出原始输入 password 的具体值
solution = solution_state.solver.eval(password, cast_to=bytes).decode()
print(solution)
else:
raise Exception("未找到可行的解决路径")
hook函数
# 钩住(hook)程序中调用 check_equals_* 的位置,用自定义函数替换原逻辑
check_equals_called_address = xxxx # TODO
# 跳过当前调用指令的长度(call 指令为 5 字节)
instruction_to_skip_length = xx # TODO
# 定义 Hook 函数,用来模拟 check_equals_* 的行为
@project.hook(check_equals_called_address, length=instruction_to_skip_length)
def skip_check_equals_(state):
# 加载第一段用户输入在内存中的位置(函数参数传入的地址)
user_input_buffer_address = 0x804a044 # 输入缓冲区地址(根据反汇编确认)
user_input_buffer_length = 16
# 从内存中读取该输入缓冲区内容,得到一个符号化位向量
user_input_string = state.memory.load(
user_input_buffer_address,
user_input_buffer_length
)
# 我们希望检测的字符串,即实际比较的目标明文(根据函数名解析)
check_against_string = b"OSIWHBXIFOQVSBZB"
# 模拟函数返回值:在 x86 中,返回值保存在 eax 中
# 使用 claripy.If 来符号化 if-else 逻辑
# 若输入字符串等于目标字符串 -> eax = 1,否则 eax = 0
state.regs.eax = claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, 32),
claripy.BVV(0, 32)
)
hook符号
# 定义一个 SimProcedure,用以替代原来的 check_equals_* 函数
class ReplacementCheckEquals(angr.SimProcedure):
# run() 方法会在函数被调用时触发
# 参数 `to_check` 和 `length` 对应原函数的参数
def run(self, to_check, length):
# 从传入的指针地址读取 length 个字节(被加密的用户输入)
user_input_buffer_address = to_check
user_input_buffer_length = length
user_input_string = self.state.memory.load(
user_input_buffer_address,
user_input_buffer_length
)
# 原函数需要对比的目标字符串(根据函数名确定)
check_against_string = b"OSIWHBXIFOQVSBZB"
# 比较内容并返回结果:
# 相等 → 返回 1
# 不相等 → 返回 0
# claripy.If 允许以符号化形式表达此逻辑
return claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, 32),
claripy.BVV(0, 32)
)
# 将 check_equals_OSIWHBXIFOQVSBZB 函数替换为自定义 SimProcedure
check_equals_symbol = "check_equals_OSIWHBXIFOQVSBZB"
project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())
在拿一个scanf的hook来举例:
class SimProcScanf(angr.SimProcedure):
def run(self, format_string, scanf0_address, scanf1_address):
scanf0 = claripy.BVS("scanf0", 32)
scanf1 = claripy.BVS("scanf1", 32)
self.state.memory.store(
scanf0_address, scanf0, endness=project.arch.memory_endness
)
self.state.memory.store(
scanf1_address, scanf1, endness=project.arch.memory_endness
)
self.state.globals['solutions'] = (scanf0, scanf1) #这里使用了一个globals全局变量,因为scanf的结果需要保存,使用时候再从全局变量中取用就好。
scanf_symbol = "__isoc99_scanf"
project.hook_symbol(scanf_symbol, SimProcScanf(), replace=True)
一些可选参数
#符号执行,一种是动态符号执行(Dynamic Symbolic Execution,简称DSE),另一种是静态符号执行(Static Symbolic Execution,简称SSE)。
#动态符号执行会去执行程序然后为每一条路径生成一个表达式。在生成表达式上引入了很多的开销,然而生成的表达式很容易求解。
#而静态符号执行将程序转换为表达式,每个表达式都表示任意条路径的属性生成表达式容易,但是表达式难求解。
#veritesting就是在这二者中做权衡,使得能够在引入低开销的同时,生成较易求解的表达式。
#"auto_load_libs":False也能降低复杂度,避免直接加载库。
project = angr.Project(path_to_binary,load_options={"auto_load_libs":False})
initial_state = project.factory.entry_state(add_options=angr.options.unicorn)
simulation = project.factory.simgr(initial_state,veritesting=True)
避免无约束情况
在符号执行中,“无约束状态”指的是一种程序执行状态,其中程序计数器(指令指针)或其他关键寄存器被设置为符号变量。这意味着符号执行引擎无法确定下一条指令的具体位置,因为它可以是任何值,由输入的符号变量控制。
这里是一个简单的ROP,通过read_input跳转到print_good函数中。
这里的符号执行推进的方式不再是高度封装的explore,而采用了step步进。SimManager将State以Stash的形式存储,当一个状态遇到符号分支条件时,两个后继状态都会出现在 stash 中,并且可以将它们同步步进,会步进的状态只有active列表中的stash。
当一个指令有很多分支的可能性时,称为不受约束的状态,比如这里输入决定了EIP的位置。在启用save_unconstrained选项下,不受约束的状态会被放置在unconstrained列表中。否则会被弃置。我们利用不受约束状态来进行ROP。
import angr
import claripy
import sys
def main():
proj = angr.Project('./17_angr_arbitrary_jump',load_options={"auto_load_libs":False})
init_state = proj.factory.entry_state(add_options=angr.options.unicorn)
simulation = proj.factory.simgr(
init_state,
save_unconstrained=True,
stashes={
'active':[init_state],
'unconstrained': [],
'found': []
},
veritesting = True
)
def has_found_solution():
return simulation.found
def has_unconstrained():
return simulation.unconstrained
def has_active():
return simulation.active
while( has_active() or has_unconstrained() ) and (not has_found_solution()) :
simulation.move('unconstrained','found')
simulation.step()
if simulation.found:
solution_state = simulation.found[0]
solution_state.add_constraints(solution_state.regs.eip == 0x42585249)
print(solution_state.posix.dumps(sys.stdin.fileno()))
else:
raise Exception('Could not find the solution')
if __name__ == '__main__':
main()
指令简介
angr.Project
| API 函数 | 功能 | 常见场景 |
|---|---|---|
angr.Project(filename) |
加载二进制文件,初始化 angr.Project 实例。 |
启动项目,加载目标二进制文件。 |
.factory.entry_state() |
创建从程序入口点(通常是 _start)开始的初始状态。 |
启动符号执行,分析程序主逻辑。 |
.factory.simgr() |
创建 SimulationManager,管理和探索符号执行路径。 |
进行路径探索,寻找满足特定条件的执行路径。 |
.factory.blank_state() |
创建一个空白状态,允许完全自定义内存和寄存器值。 | 模拟特定状态,分析特定函数或跳过程序初始化。 |
.factory.call_state(addr, *args) |
创建直接跳转到目标函数的状态,同时传递参数。 | 跳过程序的主逻辑,直接分析特定函数的行为。 |
.loader |
提供访问加载器信息的接口,包括程序段、符号表和地址映射。 | 分析程序的符号表、段信息或全局变量。 |
.loader.find_symbol(name) |
根据符号名称查找符号对象(如函数或变量)。 | 定位函数入口点(如 main 或 printf)以供进一步分析。 |
.analyses.CFGFast() |
快速构建控制流图(CFG),适用于大部分静态分析场景。 | 分析函数边界、基本块、调用关系等。 |
.analyses.CFGAccurate() |
构建更精确的控制流图,结合符号执行以支持复杂逻辑分析。 | 分析动态分支或复杂的跳转逻辑(耗时较长)。 |
.kb.functions |
提供程序中所有函数的集合,提取的函数信息存储在知识库中。 | 获取函数的入口点、调用地址及详细信息。 |
.arch |
返回二进制程序的架构信息(如 x86, ARM)。 |
匹配目标处理器架构,适配指令集和寄存器配置。 |
.factory.block(addr) |
返回指定地址的基本块对象,包含该块的指令信息和控制流。 | 分析特定地址的基本块逻辑,查看反汇编的指令。 |
.hook(addr, hook_obj) |
在指定地址设置钩子,重定向程序的逻辑到自定义函数或模拟对象。 | 替换系统调用、模拟函数逻辑或跳过某些程序段。 |
.unhook(addr) |
移除指定地址的钩子,恢复地址的原始逻辑。 | 取消对程序特定地址的重定向,恢复正常分析流程。 |
.string(addr, maxlen=100) |
从内存中提取指定地址的字符串。 | 提取程序中的硬编码数据(如密码、路径或消息)。 |
.factory.state_from_addr(addr) |
创建从特定地址开始的执行状态。 | 手动设置分析起点,避免从入口点(_start)执行整个程序。 |
SimState
angr.SimState 类是 angr 框架的核心组件之一,表示符号执行过程中的程序状态。它包含寄存器、内存、约束、文件 I/O 等信息,能够模拟程序的执行流程,追踪路径上的符号化变量以及相应的约束条件。
| API 函数 | 功能描述 | 常见场景 |
|---|---|---|
state.regs |
获取寄存器状态集合,所有寄存器均为符号化位向量(BitVector)。支持通过寄存器名称访问和修改。 |
查看和操作寄存器值,例如修改栈指针(esp)、读取通用寄存器(如 eax)。 |
state.regs.<name> |
通过指定寄存器名称直接访问对应寄存器的值,例如 state.regs.eax 或 state.regs.esp。 |
分析和控制寄存器值,影响程序执行路径。 |
state.mem[addr].type |
通过类型化方式访问内存,type 可以是 char, short, int, long 等。 |
模拟程序对内存的读写操作,提取变量或修改地址值。 |
state.mem[addr].type.concrete |
获取内存中值的具体表示形式(非符号化)。 | 提取内存中实际存储的数据值,例如常量或硬编码数据。 |
state.mem[addr].type.resolved |
获取内存中值的符号化表示形式(符号化位向量)。 | 分析程序中符号化变量的状态或约束。 |
state.memory.load(addr, size) |
从指定地址加载内存,返回大小为 size 字节的位向量(BitVector)。 |
更灵活地获取指定大小的内存数据,适合非标准类型的内存访问。 |
state.memory.store(addr, bitvector) |
将符号化位向量存储到指定内存地址。 | 模拟写入内存的操作,调整或修改程序的内存状态。 |
state.posix.dumps(fileno) |
获取指定文件描述符(如 stdin, stdout, stderr)上的数据流。 |
模拟或分析程序的输入输出交互行为,例如提取输入数据或分析输出结果。 |
state.solver.eval(expr) |
求解符号化表达式的值,返回实际值(具体化结果)。 | 检查寄存器、内存或表达式的符号化解。 |
state.solver.eval_upto(expr, n) |
求解符号化表达式的多个可能值,返回最多 n 个具体值。 |
检查表达式的多种可能解,用于多路径分析。 |
state.solver.satisfiable() |
判断当前状态是否满足所有符号化约束。 | 验证路径是否有效或继续符号执行的条件是否成立。 |
state.globals |
自定义全局变量存储区,允许用户将数据存储到状态中以供后续分析使用。 | 保存和共享分析中提取的额外信息,例如标志位或动态计算结果。 |
SimulationManager
angr.SimulationManager 是 angr 框架中用于管理和操作多个程序执行路径(SimulationState 对象)的核心组件。它提供了一种高层抽象,用于对符号化执行进行路径分类、过滤、探索以及管理,是分析和控制程序路径执行的强大工具。
angr.SimulationManager 类中使用率较高的重要 API 函数及其功能的详细表格:
| API 函数 | 功能描述 | 常见场景 |
|---|---|---|
simgr.step() |
执行符号化状态的单步操作,更新路径组。 | 模拟逐步执行程序的每条指令或基本块。 |
simgr.explore(find=addr_or_func, avoid=addr_or_func) |
按条件探索路径,找到满足 find 条件且避免 avoid 条件的路径。 |
搜索特定地址或满足特定条件的路径,常用于解题和漏洞挖掘。 |
simgr.run() |
完成符号执行,直到没有更多路径可以探索(或满足指定的终止条件)。 | 对程序进行全面探索,用于分析所有可能的执行路径。 |
simgr.stashes |
路径组的存储字典,包含多个分类路径集合(如 active、found、avoid 等)。 |
检查路径分组情况,提取特定类型的路径以便进一步分析。 |
simgr.active |
当前处于活跃状态的路径集合,表示正在探索的路径。 | 分析程序当前的执行状态或提取路径。 |
simgr.found |
已经找到满足 find 条件的路径集合(通过 explore 或手动操作生成)。 |
获取解决问题的路径,例如找到密码或破解条件。 |
simgr.avoid |
已经找到满足 avoid 条件的路径集合(通过 explore 或手动操作生成)。 |
检查和分析避免的路径,例如危险分支或无解的条件路径。 |
simgr.move(from_stash, to_stash) |
将路径从一个存储分组移动到另一个分组,例如从 active 移动到 found。 |
管理和分类路径的状态,特别是手动调整路径分组以优化分析流程。 |
simgr.filter(func, stash='active') |
按指定条件筛选路径,将符合条件的路径移动到其他分组。 | 用于路径过滤和重分类,例如将特定地址的路径移动到 found 分组。 |
simgr.split(func) |
按条件对路径进行分组,将每个分组放入单独的存储。 | 分类路径,例如按程序逻辑分割路径。 |
simgr.prune() |
清除指定存储中的路径,释放不需要的路径以节省内存。 | 删除不再需要的路径,优化性能或专注于特定分组路径。 |
simgr.drop(stash) |
删除指定分组中的所有路径。 | 清理无用路径集合,提升符号执行效率。 |
simgr.use_technique(technique) |
应用特定的探索技术(ExplorationTechnique),如深度优先或宽度优先搜索。 |
定制符号执行策略,优化路径探索过程。 |
Claripy
Claripy 是 angr 中用于符号化计算和约束求解的关键库,它为符号执行提供了强大的表达式表示、符号求解、符号计算、以及符号约束管理功能。Claripy 处理和操作符号化数据(如变量和内存地址)并提供求解器接口,用于符号计算、约束求解和路径分析。对于我们而言一般不需要直接与其进行交互,但通常我们会使用其提供的一些接口
位向量(bitvector)是 angr 求解引擎中的一个重要部分,其表示了 一组位 (a sequence of bits)。
| 函数/属性 | 功能描述 | 示例代码 | 应用场景 |
|---|---|---|---|
claripy.BVV(value, size) |
创建具有具体值的位向量(BitVector Value)。支持整数或字符串作为值,size 指定位宽。 |
bv = claripy.BVV(10, 32) # 创建一个32位具体值位向量10 |
表示程序中固定值的变量,例如初始化常量、硬编码地址等。 |
claripy.BVS(name, size) |
创建具有符号值的位向量(BitVector Symbol),name 为符号变量名,size 指定位宽。 |
sym_bv = claripy.BVS('x', 32) # 创建一个32位符号位向量 x |
表示程序中未知或可变值的变量,例如输入、环境变量、程序状态。 |
.concat(*args) |
将多个位向量连接成一个更大的位向量。 | result = claripy.Concat(bv1, bv2) # 将两个位向量 bv1 和 bv2 连接成一个 |
组合多个变量或部分数据(如标志位、操作码等)形成一个完整的值。 |
.zero_extend(bits) |
对位向量执行零扩展,将其长度增加指定的 bits 位,用 0 填充扩展部分。 |
extended_bv = bv.zero_extend(8) # 将 bv 扩展 8 位 |
统一位宽,适配不同长度变量的运算需求。 |
.sign_extend(bits) |
对位向量执行符号扩展,将其长度增加指定的 bits 位,用符号位填充扩展部分。 |
extended_bv = bv.sign_extend(8) # 将 bv 符号扩展 8 位 |
处理带符号运算时的位宽适配。 |
.op |
获取位向量表达式的操作类型(如 Add、Sub 等运算符)。 |
operation = sym_bv.op # 返回 sym_bv 的运算类型 |
分析复杂表达式结构,理解操作类型。 |
.args |
获取位向量表达式的参数(子表达式或值)。 | arguments = sym_bv.args # 返回 sym_bv 的操作数 |
获取表达式的子结构,用于符号执行中的表达式简化和优化。 |
claripy.If(cond, true_val, false_val) |
条件表达式,返回当条件为真或假时的不同值。 | result = claripy.If(flag == 1, sym_bv1, sym_bv2) |
表示程序中的条件分支逻辑,例如条件跳转和选择性赋值。 |
.extract(high_bit, low_bit) |
提取位向量中的某些位。 | extracted = bv.extract(15, 8) # 提取 bv 的第 8 至 15 位 |
解析复杂数据格式,如拆解操作码、提取标志位。 |
.reverse() |
反转位向量的字节顺序。 | reversed_bv = bv.reverse() # 将 bv 的字节顺序反转 |
处理不同字节序的兼容性问题,例如大端与小端的转换。 |
.length |
获取位向量的长度(位宽)。 | length = bv.length |
检查变量的大小是否符合预期,用于程序状态分析。 |
.simplify() |
对符号表达式进行简化,尝试减少表达式的复杂度。 | simplified_expr = sym_expr.simplify() |
优化复杂表达式,提高符号执行效率。 |
.is_symbolic() |
检查位向量是否为符号变量(返回 True)或具体值(返回 False)。 |
is_sym = sym_bv.is_symbolic() |
判断变量属性,确定其是否需要约束求解。 |
.resolved |
获取位向量的具体值(BVV),当符号变量已被约束求解时可用。 |
concrete_val = sym_expr.resolved |
将符号变量的求解结果转换为具体值。 |
.concrete |
获取位向量的具体值(int 类型),仅适用于具体值位向量。 |
value = bv.concrete |
检查和操作具体值,例如验证程序中常量的正确性。 |
Claripy ,涵盖了符号表达式的符号计算、符号约束和求解等关键操作。
| 函数 | 功能描述 | 示例代码 |
|---|---|---|
claripy.Bool(name) |
创建一个符号布尔表达式,用于表示符号化的布尔值。 | b = claripy.Bool('b') # 创建一个符号布尔变量 b |
claripy.If(cond, true_expr, false_expr) |
根据条件 cond 返回 true_expr 或 false_expr,类似于三元运算符。 |
result = claripy.If(x == 1, y + 10, y - 10) # 如果 x == 1, 返回 y + 10,否则返回 y - 10 |
claripy.And(expr1, expr2) |
对两个符号表达式进行按位与(AND)运算。 | result = claripy.And(x, y) # 对 x 和 y 执行按位与运算 |
claripy.Or(expr1, expr2) |
对两个符号表达式进行按位或(OR)运算。 | result = claripy.Or(x, y) # 对 x 和 y 执行按位或运算 |
claripy.Xor(expr1, expr2) |
对两个符号表达式进行按位异或(XOR)运算。 | result = claripy.Xor(x, y) # 对 x 和 y 执行按位异或运算 |
claripy.Not(expr) |
对符号表达式执行按位取反(NOT)运算。 | result = claripy.Not(x) # 对 x 执行按位取反运算 |
claripy.Concat(expr1, expr2) |
连接两个符号表达式,形成一个新的符号表达式。 | result = claripy.Concat(x, y) # 将 x 和 y 拼接在一起 |
claripy.LShR(expr, n) |
对符号表达式 expr 进行逻辑右移运算。 |
result = claripy.LShR(x, 4) # 对 x 进行逻辑右移4位 |
claripy.Shl(expr, n) |
对符号表达式 expr 进行逻辑左移运算。 |
result = claripy.Shl(x, 4) # 对 x 进行逻辑左移4位 |
claripy.BVVal(value, size) |
创建一个具有固定值和大小的符号位向量(常量值)。 | value = claripy.BVVal(5, 32) # 创建一个32位的符号常量值 5 |
claripy.symbolic() |
将一个给定值标记为符号化值。返回符号值,表示该值在符号执行中需要被处理。 | symbol = claripy.symbolic(val) # 将 val 标记为符号值 |
claripy.eval_upto(expr, n) |
返回符号表达式的多个解,最多返回 n 个解。 |
solver.eval_upto(x + y, 5) # 求解 x + y 的5个解 |
claripy.solver.Solver() |
创建一个符号求解器实例,用于对符号表达式求解。 | solver = claripy.solver.Solver() # 创建一个符号求解器实例 |
solver.add(expr) |
向符号求解器中添加一个符号约束。 | solver.add(x == 10) # 添加约束 x == 10 |
solver.eval(expr) |
求解符号表达式,返回其具体值(求解后的整数值)。 | solver.eval(x + y) # 求解 x + y 的值 |
solver.satisfiable() |
检查当前符号约束是否可满足,若可满足返回 True,否则返回 False。 |
solver.satisfiable() # 检查当前约束是否可满足 |
solver.unsat_core() |
返回符号求解器中无法满足的最小约束集(unsat core)。 | solver.unsat_core() # 返回最小不可满足约束集 |
claripy.strlen(expr) |
返回一个符号字符串表达式的长度。 | length = claripy.strlen(some_str) # 获取符号字符串 some_str 的长度 |
claripy.strtoint(expr, base) |
将符号字符串转换为整数,base 为进制(如 10 表示十进制)。 |
num = claripy.strtoint(some_str, 10) # 将符号字符串转换为整数 |
hook
在 angr 中,hook 允许用户为程序的特定地址或函数提供自定义的处理逻辑。通过 hook,用户可以拦截程序的控制流,并用自定义的代码替代原有的函数或指令。
常用 hook 函数表格
| API 函数 | 功能描述 | 示例代码 | 常见场景 |
|---|---|---|---|
proj.hook(addr, hook_func) |
将给定地址的原始指令替换为 hook_func 函数的逻辑。 |
def hook_func(state): state.regs.eax = 42 # 设置 eax 寄存器的值为 42 proj.hook(0x400080, hook_func) |
在符号执行时,替换特定地址的指令,实现对程序行为的模拟。 |
proj.hook_symbol(symbol_name, hook_func) |
将给定符号(如函数名)对应的代码替换为 hook_func 函数的逻辑。 |
def hook_func(state): state.regs.eax = 42 # 设置 eax 寄存器的值为 42 proj.hook_symbol('target_func', hook_func) |
在符号执行时,替换函数的实现,用于模拟函数调用。 |
proj.hook_at_addr(addr, hook_func) |
绑定在特定地址上,执行 hook 操作。 | proj.hook_at_addr(0x400080, hook_func) |
通过地址钩住程序,模拟执行特定的内存位置的代码或替换其逻辑。 |
proj.hook_call(addr, hook_func) |
钩住在调用特定地址时触发的操作。 | proj.hook_call(0x400080, hook_func) |
用于模拟对某些函数或操作的调用,替代原本的调用过程。 |
proj.hook_register(register_name, hook_func) |
为寄存器创建钩子函数,替代特定寄存器的操作。 | proj.hook_register('eax', hook_func) |
替代对寄存器的操作,模拟寄存器值的更新。 |
hook_func(state) |
用户定义的钩子函数,接收一个 state 参数并返回新的状态或修改当前状态。 |
def hook_func(state): state.regs.eax = 42 # 设置 eax 寄存器的值为 42 |
用户定义的钩子函数,用于修改符号执行时的状态,如设置寄存器的值、修改内存等。 |
SimFile和SimPackets
| API 函数 | 功能描述 | 示例代码 | 常见场景 |
|---|---|---|---|
angr.SimFile(name, content, size) |
创建一个模拟文件(SimFile),可以包含具体值或符号变量内容,并指定大小。 | import angr, claripy sim_file = angr.SimFile('a_file', content="flag{F4k3_f1@9!}\n") bvs = claripy.BVS('bvs', 64) sim_file2 = angr.SimFile('another_file', bvs, size=8) |
用于创建模拟文件,进行符号执行时模拟文件内容的读取、写入等操作。 |
state.fs.insert(name, sim_file) |
将 SimFile 插入到当前状态的文件系统中。 |
state.fs.insert('test_file', sim_file) |
将模拟文件插入到符号执行的文件系统,模拟程序与文件系统的交互。 |
sim_file.read(pos, size) |
从模拟文件中读取数据,返回数据、实际读取的字节数以及新的位置。 | pos = 0 data, actual_read, pos = sim_file.read(pos, 0x100) |
模拟文件的读取操作,用于获取文件内容。 |
sim_file.write(pos, data) |
将数据写入模拟文件的指定位置。 | pos = 0 sim_file.write(pos, b'new_data') |
用于模拟文件的写操作。 |
sim_file.set_state(state) |
将模拟文件与给定的状态关联,指定哪个状态拥有该文件的内容。 | sim_file.set_state(state) |
将模拟文件与某个特定状态进行关联,确保文件在符号执行过程中的状态同步。 |
angr.SimPackets(name) |
创建一个模拟数据包流(SimPackets),用于模拟与流相关的文件或网络通信。 | sim_packet = angr.SimPackets('my_packet') |
用于模拟流数据(如 TCP 连接、标准 IO 等),帮助处理符号执行中的流操作。 |
sim_packet.read(pos, size) |
从模拟数据包流中读取指定位置的数据。 | sim_packet.read(pos, size) |
用于模拟读取流数据,常用于模拟网络数据流或 I/O 操作中的流数据。 |
sim_packet.write(pos, data) |
将数据写入模拟数据包流的指定位置。 | sim_packet.write(pos, b'new_packet_data') |
用于模拟写入流数据,通常应用于网络协议或 I/O 操作中。 |
state.fs.get(filename) |
获取当前状态下文件系统中指定文件名的模拟文件对象。 | sim_file = state.fs.get('test_file') |
获取已插入的模拟文件,用于进一步的读取、写入等操作。 |
state.fs.remove(filename) |
从当前状态下的文件系统中删除指定文件。 | state.fs.remove('test_file') |
删除文件系统中的模拟文件,常用于清理操作或修改执行路径。 |
sim_file.size |
获取模拟文件的大小。 | file_size = sim_file.size |
获取模拟文件的大小,在处理文件时非常有用,特别是符号执行中需要知道文件大小的场景。 |
sim_file.seek(pos) |
设置模拟文件的读写位置。 | sim_file.seek(0x100) |
用于在文件中移动读写位置,类似于传统文件操作中的 seek 函数。 |
SimProcedure
angr.SimProcedure 类是 angr 提供的一个用于模拟程序中的函数过程的类,它可以用来模拟文件中的某些函数的行为,尤其是在符号执行过程中。通过继承并重写 SimProcedure 类的 run() 方法,用户可以定义自定义的函数行为,这对于替换或模拟二进制程序中调用的特定函数非常有用
SimProcedure 主要 API 函数表格
| API 函数 | 功能描述 | 示例代码 | 常见场景 |
|---|---|---|---|
SimProcedure.run(*args) |
模拟目标函数的行为,必须重写此方法。参数是目标函数的输入。 | class MyProcedure(angr.SimProcedure): def run(self, arg1, arg2): return self.state.memory.load(arg1, arg2) |
用于定义目标函数的模拟逻辑,模拟函数的行为。 |
SimProcedure.ret(expr) |
模拟函数的返回操作,expr 为返回的值。 |
self.ret(result) |
在自定义函数的 run() 方法中模拟函数的返回。 |
SimProcedure.jump(addr) |
模拟跳转到指定地址。 | self.jump(0x400080) |
模拟函数内的跳转,改变程序的执行流。 |
SimProcedure.exit(code) |
模拟程序的退出操作,code 为退出码。 |
self.exit(0) |
模拟程序退出的场景。 |
SimProcedure.call(addr, args, continue_at) |
模拟函数调用,addr 是函数的地址,args 是参数,continue_at 是执行的下一地址。 |
self.call(0x400080, [arg1, arg2], continue_at=0x400100) |
在自定义函数中模拟对其他函数的调用。 |
SimProcedure.inline_call(procedure, *args) |
内联调用另一个 SimProcedure。 |
self.inline_call(AnotherProcedure(), arg1, arg2) |
在当前函数内联调用另一个函数,模拟多个函数交互。 |

浙公网安备 33010602011771号