0%

Angr Learning Note

迈向自动化的第二步——

0xFF Reference

1. Symbolic Execution Leaning Note (1) — Basis
2. 2020年Angr-Api非官方介绍
3. angr_ctf里介绍angr的ppt(讲的真的挺好的)

0x00 Recall-Symbolic Execution

笔者水平有限,如有错误请指出 ;-)

我们先回顾一下什么是符号执行:
我们在逆向一个程序、去找一个程序的漏洞时,我们会希望找到一个input,来触发我们需要的漏洞或者找到我们需要的flag,而符号执行的功能就是通过遍历程序所有可能的“状态(State)”,找到我们需要的State,进而通过约束求解器,求解出我们最后需要的input

宏观上看,符号执行可以分为三步

  1. 注入符号
  2. 分支
  3. 执行分支

我们用一个简单的函数来分别看一下这三步是什么

1
2
3
4
5
6
7
8
9
// demo func

Line1 void check_func(char* passwd)
Line2 {
Line3 if(passwd == "HeyGap")
Line4 printf("Access granted");
Line5 else
Line6 printf("Access denied");
Line7 }

1. 注入符号(Inject Symbols)

  • 什么是注入符号?
    我们在具体执行的时候,传入的passwd是一个具体的字符串;可我们在符号执行时,传入的是一个符号
  • 什么是符号?
    类比于我们小学学的方程x + 1 = 2,我们可以通过这样一个方程来求解出变量x,我们也称“这个方程约束了这个变量x”;而符号就是方程中的变量,用来约束符号的执行路径就可以类比成方程
  • 什么是执行路径?
    从State_A到State_B的执行路径就是从起点A可以执行到终点B的一条指令(instructions)路径,在每条执行路径中都会维护一个符号变量pc,pc记录了“从A点出发,要到达B的全部条件”,这个符号变量的类型是逻辑表达式,我们可以将这个符号变量扔到SMT求解器(比如z3)中去求解满足这个符号变量中所有约束条件的1~n个解
  • Example
    在上述例子中,我们将一个符号传入passwd中,就是注入符号。

2. 分支(Branching)

  • 什么是branching
    由于我们的符号是一个待确定的值,所以我们在遇到判断语句时,当前的State会设置为执行完毕(already executed),然后激活(active)两个新的state,由于要遍历全部state,符号在判断语句中得到不同返回值,而这两个state就是不同返回值进入的不同分支的起始状态
  • Example
    在上述例子中,可以理解为Line1-2为state1,当执行到Line3时,state1设置为执行完毕,同时激活state2和state3,state2为Line4,state3为Line6

3. 评估分支(Evaluate Each Branches)

  • 什么是Evaluate Branches?
    我们会评估(Evaluate)每一个active的state,判断他们是否符合我们的需求,如果不满足就设置terminated,如果遇到判断语句就进入第二步
  • Example
    假设当前激活的state为2和3,如果我们需要“标准输出中包含’granted’”,那符号执行器就会评估state2,发现这个state符合我们的要求,于是将state2加到found数组中;然后再评估state3,发现不符合,设置为terminated

0x01 An Introduction to Angr

1. 出现的class

笔者在此记录相对重要的class,读者可以通过阅读源码或官方手册等方式,先弄清楚这些class的作用

1
2
SimState
Simulation Manager(simgr)

2. Symbolic Execution In Angr

通过0x00,我们知道在符号执行中,执行路径符号是最重要的两个东西

  • Angr中的执行路径

    1. 我们知道每branch一次,就会添加两个state,而在Angr中,维护路径信息的pc由SimState Object组成,SimState用∩链在一起就组成了pc
    2. 由于我们要遍历全部state,所以我们需要有一个管理器来管理全部执行路径(a Set of Path),这个管理器就是Simulation Manager (simgr),下图为Angr中生成全部路径的过程
    3. 上图提到until we find what we want,simgr提供了函数explore,可以让我们通过两种方式来找到我们需要的state
      1. 通过指令在text段的地址
      2. 通过任意体现state特征的函数
  • State Explosion

    1. 但是单纯的遍历每一条路径会存在状态爆炸(State Explosion,有些地方也叫路径爆炸)的问题,来看下面这个循环
      1
      2
      3
      4
      5
      6
      7
      8
      int x;
      for(int 0;i<100;i++)
      {
      if(x > 10)
      printf("Good");
      else
      printf("Wrong");
      }
    2. 当i=1时,管理器会创建两个state,而i=2时,先前的两个state有分别会创建两个state,当i=100时,我们就会出现2^100个state,这就是状态爆炸问题
    3. 而explore也为我们提供了一个参数avoid,效果就是剪枝,在explore中添加avoid参数会帮助我们提前terminate那些我们不需要的state,因此这个state后续的branch就不会再跟进

0x01 库架构分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
angr
| - Project()
| - factory
| - state
| - entry_state()
| - blank_state(addr)

| - regs.eax/ebx/...
| - solver
| - BVS
| - eval
| - fs
| - insert(string filename,angr.storage.SimFile())
| - simulation_manager(init_state)
| - explore(find,avoid)
| - found[]

| - storage
| - SimFile(filename,content,size)
| - sim_options
| - SYMBOL_FILL_UNCONSTRAINED_MEMORY
| - SYMBOL_FILL_UNCONSTRAINED_REGISTERS

found_state
| - posix
| - dumps(std)
| - solver
| - eval(claripy.BVS pass,cast_to)

0x02 API手册

  • Project类(参数、功能、重载、返回值)

    1. 构造函数参数:“命令行中运行程序的指令”(eg: “./00_angr_find”)
    2. 功能:“构建一个符号执行项目”
    3. 附属成员
      • factory
        1. 附属成员
          • state
            1. 重载函数
              • entry_state()
                1. 返回值:程序入口点
              • blank_state(addr)
                1. 参数:模拟执行开始的地址
            2. 附属成员
              • regs
                1. 附属成员: 各种寄存器
              • fs
                1. 全称: filesystem
                2. 附属成员:
                  • insert(string filename,angr.storage.simFile)
                    1. 参数1:文件名称
                    2. 参数2:创建过的符号化文件对象
          • simulation_manager类
            1. 构造函数参数:起始状态
            2. 功能:设置模拟执行的起始地址,并返回实例化对象
            3. 附属成员
              • explore(find, avoid)
                1. 参数find:希望程序抵达的地址
                2. 参数avoid:希望程序不抵达的地址
                3. 功能:用符号探索到find指向的地址,并避免avoid指向的地址
                4. 重载:explore(find=func1,avoid=func2)
                  1
                  2
                  3
                  4
                  def func1(state):
                  return b"Good Job!" in state.posix.dumps(1)
                  def func2(state):
                  return b"Try Again!" in state.posix.dumps(1)
                  1. 功能:探索具有func1特征的的函数分支,回避具有func2特征的函数分支
              • found数组
                1. found_state的实例化数组
  • found_state类

    • posix
      • dumps(int std)
        1. 参数std: stdin(0)/stdout(1)/stderr(2)
        2. 功能: 将std指向的内容打印出来
    • solver
      • eval(claripy.BVS arg1)
        1. 参数:要求解的约束公式
        2. 功能:求解arg1并返回结果
  • claripy类

    • BVS类
      1. 构造函数参数:arg1: 别名 | arg2: 符号向量占多少位
      2. 功能:构建一个空的约束公示
  • storage类

    • SimFile(filename,content,size)
      1. 参数filename:要引入的filename
      2. 参数content:符号化向量
      3. 参数size:要从文件中读取的字节数*8(单位:bits)
      4. 功能:创建一个符号化的文件对象
  • sim_options类

    • SYMBOL_FILL_UNCONSTRAINED_MEMORY
      • 功能:自动用符号填充未约束的内存(模拟过程中没有明确值的内存位置)
    • SYMBOL_FILL_UNCONSTRAINED_REGISTERS
      • 功能:自动用符号填充未约束的寄存器

0x01 Angr语法

1. 准备阶段

  • 引入与初始化

    1
    2
    3
    4
    5
    6
    7
    8
    import angr

    p = angr.Project(exec_path)

    init_state = p.factory.entry_state()
    # init_state = p.factory.blank_state(addr) # addr = 0xbeef

    sm = p.factory.simulation_manager(init_state)
  • 命令行引入参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import sys

    def main(argv):
    arg1 = argv[1]
    ...


    if __name__ == '__main__':
    main(sys.argv)
  • 符号向量

    1
    2
    3
    import claripy

    pass1 = claripy.BVS('pass1', 32)
  • 符号化寄存器

    1
    init_state.regs.eax = pass1
  • 符号化文件

    1
    2
    3
    4
    5
    6
    7
    filename = ""
    filesize =

    password = init_state.solver.BVS('password',filesize*8)
    sim_file = angr.storage.SimFile(filename,content=password,size=filesize)

    init_state.fs.insert(sim_file)

2. 探索阶段

  • 探索指定地址并查看标准流

    1
    sm.explore(find = addr_to_find)
  • 利用函数explore分支

    1
    2
    3
    4
    5
    6
    7
    8
    def is_good(state):
    return b"Good Job!" in state.posix.dumps(1)

    def is_bad(state):
    return b"Try Again!" in state.posix.dumps(1)

    sm.explore(find=is_good, avoid=is_bad)
    # sm.explore(find=addr_find, avoid=addr_avoid)

3. 输出阶段

  • 查看结果

    1
    2
    3
    4
    if sm.found:
    found_state = sm.found[0]

    print('[x] Solution: {}'.format(found_state.posix.dumps(0)))
  • 求解寄存器的符号向量

    1
    result = init_state.solver.eval(BVS)
-------------文章就到这里啦!感谢您的阅读XD-------------