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) 根据符号名称查找符号对象(如函数或变量)。 定位函数入口点(如 mainprintf)以供进一步分析。
.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.eaxstate.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 路径组的存储字典,包含多个分类路径集合(如 activefoundavoid 等)。 检查路径分组情况,提取特定类型的路径以便进一步分析。
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

Claripyangr 中用于符号化计算和约束求解的关键库,它为符号执行提供了强大的表达式表示、符号求解、符号计算、以及符号约束管理功能。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) # 将两个位向量 bv1bv2 连接成一个 组合多个变量或部分数据(如标志位、操作码等)形成一个完整的值。
.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 获取位向量表达式的操作类型(如 AddSub 等运算符)。 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_exprfalse_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) # 对 xy 执行按位与运算
claripy.Or(expr1, expr2) 对两个符号表达式进行按位或(OR)运算。 result = claripy.Or(x, y) # 对 xy 执行按位或运算
claripy.Xor(expr1, expr2) 对两个符号表达式进行按位异或(XOR)运算。 result = claripy.Xor(x, y) # 对 xy 执行按位异或运算
claripy.Not(expr) 对符号表达式执行按位取反(NOT)运算。 result = claripy.Not(x) # 对 x 执行按位取反运算
claripy.Concat(expr1, expr2) 连接两个符号表达式,形成一个新的符号表达式。 result = claripy.Concat(x, y) # 将 xy 拼接在一起
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) 在当前函数内联调用另一个函数,模拟多个函数交互。
posted @ 2025-11-20 16:11  T0fV404  阅读(0)  评论(0)    收藏  举报