angr学习-笔记汇总

推荐肉丝r0ysue课程(包含安卓逆向与js逆向):https://img-blog.csdnimg.cn/7fa698312c304ab7bb0a03e2c866990f.png
整理存货…乱七八糟的

angr实战

00_angr_find

import angr
p = angr.Project('/home/cx330/Desktop/Debugging/learn/00_angr_find',auto_load_libs=False)
# 加载文件
# auto_load_libs=False   不加载共享库文件

state = p.factory.entry_state()
# 创建一个状态state
# entry_state()  构造一个已经准备好从函数入口点执行的状态。

sm = p.factory.simgr(state)
# 创建一个Simulation Managers  模拟管理器
# 也可改为
# sm = p.factory.simulation_manager(state)
# 目前没发现区别

good = 0x804867d
# 正确路径的地址

sm.explore(find = good)
# 让angr开始探索,这个路径
# 如果找到了满足find的状态,则将其保存在模拟管理器的found分类里

if sm.found:
    #如果found分类不为空
    find_state = sm.found[0]
    # 就把found分类里面的状态给find_state

flag = find_state.posix.dumps(0)
# state.posix.dumps(0) 获取输入

print(str(flag,'utf-8'))
# type(flag)为'bytes',将其转换为字符串再输出


####################################
# 其中good还可换成
good = lambda s:b'Good Job.' in s.posix.dumps(1)
# 意为寻找输出为'Good Job.'的状态,注意这里为字节型数据
# # state.posix.dumps(1) 获取输出

01_angr_avoid

import angr
p = angr.Project('/home/cx330/Desktop/Debugging/learn/01_angr_avoid',auto_load_libs=False)
# 加载文件
# auto_load_libs=False   不加载共享库文件

state = p.factory.entry_state()
# 创建一个状态state
# entry_state()  构造一个已经准备好从函数入口点执行的状态。

sm = p.factory.simulation_manager(state)
# 创建一个Simulation Managers  模拟管理器

good = lambda s:b'Good Job.' in s.posix.dumps(1)
# 意为寻找输出为'Good Job.'的状态,注意这里为字节型数据
# state.posix.dumps(1) 获取输出
# 也可换为     good = 0x080485E0  # 即为换成地址

bad = 0x080485A8
#需要避免的地址,即找到的状态需满足 不执行此地址

sm.explore(find = good,avoid = bad)
# 让angr开始探索,这个路径
# 如果找到了满足find和avoid的状态,则将其保存在模拟管理器的found分类里
# 如果有经过avoid的状态,将其保存再avoid分类里

if sm.found:#如果found分类里不为空,即存在满足条件的状态,将其去除
    find_state = sm.found[0]
flag = find_state.posix.dumps(0)
# 得到这种状态的输入
print(str(flag,'utf-8'))

02_angr_find_condition

import angr
p = angr.Project('/home/cx330/Desktop/Debugging/learn/02_angr_find_condition',auto_load_libs=False)
state = p.factory.entry_state()
sm = p.factory.simulation_manager(state)
def is_good(state):
    return b'Good Job' in state.posix.dumps(1)
sm.explore(find = is_good)
if sm.found:
    find_state = sm.found[0]
flag = find_state.posix.dumps(0)
print(str(flag,'utf-8'))

03_angr_symbolic_registers

老版angr不支持scanf多个参数(现已支持,因此完全可以用之前的脚本打通……)

这里使用老版的解法

main()函数中

.text:0804897B call    get_user_input
.text:08048980 mov     [ebp+var_14], eax
.text:08048983 mov     [ebp+var_10], ebx
.text:08048986 mov     [ebp+var_C], edx

可以看到scanf是把三个输入分别给了eax,ebx,edx

import angr,claripy
p = angr.Project('/home/cx330/Desktop/Debugging/learn/03_angr_symbolic_registers')
init_addr = 0x08048980
state = p.factory.blank_state(addr=init_addr)
# 构造一个空状态,即跳过了scanf,从其下部开始执行

p1 = claripy.BVS('p1',32)
p2 = claripy.BVS('p2',32)
p3 = claripy.BVS('p3',32)
# 定义三个位向量,分别表示三个输入

state.regs.eax = p1
state.regs.ebx = p2
state.regs.edx = p3
# 将三个位向量分别给eax,ebx,edx。(因为get_user_input这个函数就是这么干的)

sm = p.factory.simulation_manager(state)
# 构造一个模拟管理器
# def is_bad(state):
#        return b'Try again' in state.posix.dumps(1)
# 可以加上此约束,即在explore之中加上 avoid = is_bad
def is_good(state):
    return b'Good Job' in state.posix.dumps(1)
sm.explore(find = is_good)
if sm.found:
    find_state = sm.found[0]
    flag1 = find_state.solver.eval(p1)
    flag2 = find_state.solver.eval(p2)
    flag3 = find_state.solver.eval(p3)
    print('{:x} {:x} {:x}'.format(flag1,flag2,flag3))
    # 输出16进制的结果,因为原程序是按照16进制来读取的
else:
    print('No')
    # 没找到符合条件的状态则输出no

04_angr_symbolic_stack

模拟栈

.text:08048679 public handle_user
.text:08048679 handle_user proc near
.text:08048679
.text:08048679 var_10= dword ptr -10h
.text:08048679 var_C= dword ptr -0Ch
.text:08048679
.text:08048679 ; __unwind {
.text:08048679 push    ebp
.text:0804867A mov     ebp, esp
.text:0804867C sub     esp, 18h
.text:0804867F sub     esp, 4
.text:08048682 lea     eax, [ebp+var_10]
.text:08048685 push    eax
.text:08048686 lea     eax, [ebp+var_C]
.text:08048689 push    eax
.text:0804868A push    offset aUU      ; "%u %u"
.text:0804868F call    ___isoc99_scanf
.text:08048694 add     esp, 10h
.text:08048697 mov     eax, [ebp+var_C]
.text:0804869A sub     esp, 0Ch
.text:0804869D push    eax

假设在scanf函数下执行,08048694为scanf函数恢复栈,所以应从08048697开始模拟

import angr
p = angr.Project('/home/cx330/Desktop/Debugging/learn/04_angr_symbolic_stack')
# 载入程序

def good(state):
    return b"Good Job." in state.posix.dumps(1)

def bad(state):
    return b"Try again." in state.posix.dumps(1)

start_addr = 0x08048697
# 开始模拟的地址

state = p.factory.blank_state(addr = start_addr)
# 新建一个状态

state.stack_push(state.regs.ebp)
# 压入ebp,模拟地址0x08048679代码

state.regs.ebp = state.regs.esp
# 模拟地址0x0804867A代码

offest = 0x8
# 两个局部变量
# var_C= dword ptr -0Ch  也就是占的地址是 0x9 a b c
# var_10= dword ptr -10h 占的地址是 d e f 0x10
state.regs.esp -= offest
# 初始化栈

p1 = state.solver.BVS('p1',32)
p2 = state.solver.BVS('p2',32)
state.stack_push(p1)
state.stack_push(p2)
# 创建两个位向量,并将其压入栈

sm = p.factory.simgr(state)
sm.explore(find = good, avoid = bad)
# 创建模拟管理器并开始探索路径
if sm.found:
    found_state = sm.found[0]
    flag1 = found_state.solver.eval(p1)
    flag2 = found_state.solver.eval(p2)
    print('{} {}'.format(flag1,flag2))
    # 输出
else:
    print('No')

05_angr_symbolic_memory

import angr
p = angr.Project('/home/cx330/Desktop/Debugging/learn/05_angr_symbolic_memory')

def good(state):
    return b"Good Job." in state.posix.dumps(1)

def bad(state):
    return b"Try again." in state.posix.dumps(1)

start_addr = 0x08048601
# 依旧是选择在scanf函数的下方初始化状态

state = p.factory.blank_state(addr = start_addr)
p1 = state.solver.BVS('p1',64)
p2 = state.solver.BVS('p2',64)
p3 = state.solver.BVS('p3',64)
p4 = state.solver.BVS('p4',64)
state.memory.store(0x0A1BA1C0,p1)
state.memory.store(0x0A1BA1C8,p2)
state.memory.store(0x0A1BA1D0,p3)
state.memory.store(0x0A1BA1D8,p4)
# 初始化四个位向量,并存入内存

sm = p.factory.simgr(state)
sm.explore(find = good, avoid = bad)
if sm.found:
    found_state = sm.found[0]
    flag1 = found_state.solver.eval(p1,cast_to=bytes).decode('utf-8')
    flag2 = found_state.solver.eval(p2,cast_to=bytes).decode('utf-8')
    flag3 = found_state.solver.eval(p3,cast_to=bytes).decode('utf-8')
    flag4 = found_state.solver.eval(p4,cast_to=bytes).decode('utf-8')
    # 先解释成bytes类型再转换成str
    # 貌似不能直接转换str
    print('{} {} {} {}'.format(flag1,flag2,flag3,flag4))
else:
    print('No')

ubuntu安装angr

  1. 安装依赖

    sudo apt-get install python-dev libffi-dev build-essential virtualenvwrapper
    export WORKON_HOME=$HOME/Python-workhome
    source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
    
  2. 安装angr

mkvirtualenv angr && pip install angr
()
mkvirtualenv --python=$(which python3) angr && pip install angr
  • 若报错:“未找到命令”

    进行virtualenvwrapper初始化

    输入

    export WORKON_HOME=$HOME/Python-workhome
    
    source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
    
  • 若报错:ERROR: pyvex 7.8.9.26 has requi rement future=-0.16.0, but you’ll have future 0.18.2 which is incompatible.
    将python2和python3的future均改为0.16.0版本
    命令:

    sudo pip uninstall future
    sudo pip install future==0.16.0
    sudo pip3 uninstall future
    sudo pip3 install future==0.16.0
    

angr的某些组件需要python版本>=3.6

先升级python版本,若有以下报错

# 报错
OSError: Command /home/ palmer/.virtualenvs/angr/bin/ python3
SetuptooLs pkg_resources pip wheel failed with error code 2
# 解决
sudo pip install setuptools
sudo pip install --upgrade setuptools
sudo pip install virtualenv
sudo pip install --upgrade virtualenv

# 报错
share/virtuaLenvwrapper/virtualenvwrapper.sh
/usr/bin/python3: Error while finding spec for 'virtualenvwrapper.hook_Loader' (ImportError: No module named 'virtuaLenvwrapper')
virtualenvwrapper.sh: There was a problem running the initialization hooks.
# 解决
sudo apt-get install python3-pip
sudo pip3 install virtualenv virtualenvwrapper
source .bashrc

mkvirtualenv --python=$(which python3) angr

# 离线安装pip包cle-8.20.1.7
# (如果install angr可以安装,则无需手动安装)

pip install angr

核心概念

顶层接口

使用angr的第一个操作始终是将二进制文件加载到项目中。将使用/bin/true作为以下例子的示例二进制文件。

>>> import angr
>>> proj = angr.Project('/bin/true')

项目是对angr的控制基础。有了它,将能够对刚加载的可执行文件进行分析和模拟。在Angr中使用的几乎每个对象都将取决于某种形式的项目的存在。

基本特性

首先,我们具有一些该项目的基本属性:其CPU架构,其文件名以及其入口点的地址。

>>> import monkeyhex # 这将格式化十六进制数值结果
>>> proj.arch
<Arch AMD64 (LE)>
>>> proj.entry
0x401670
>>> proj.filename
'/bin/true'
  • proj.arch:arch是archinfo.Arch对象的一个实例,表示程序指令架构,在本例中是AMD64小端,一般会关心 arch.bits, arch.bytes,arch.name与arch.memory_endness。
  • proj.entry:二进制文件的入口
  • proj.filename:二进制文件的文件名

加载器

angr使用CLE模块将二进制文件映射到虚拟地址空间,CLE得到的结果作为.loader属性,可以通过它查看与程序一起加载的angr共享库以及加载地址空间的一些信息

>>> proj.loader  # &将其加载到内存
<Loaded true, maps [0x400000:0x5004000]>

>>> proj.loader.shared_objects # &共享库
{'ld-linux-x86-64.so.2': <ELF Object ld-2.24.so, maps [0x2000000:0x2227167]>,
 'libc.so.6': <ELF Object libc-2.24.so, maps [0x1000000:0x13c699f]>}

>>> proj.loader.min_addr  # &内存中最小地址
0x400000
>>> proj.loader.max_addr  # &内存中最大的地址
0x5004000

>>> proj.loader.main_object  # 我们在这个项目中加载了几个二进制文件。这是最主要的那一个。
<ELF Object true, maps [0x400000:0x60721f]>

>>> proj.loader.main_object.execstack  # 示例查询:这个二进制文件有可执行堆栈吗?
False
>>> proj.loader.main_object.pic  # 示例查询:这个二进制独立吗?(is this binary position-independent?)
True

The factory

angr里面定义了许多类,大多数类都需要使用project来实例化。为了不使你的代码里到处都是project,我们提供一个project.factory 对象,它为一些你经常使用的对象内置了一些方便的构造函数。

Blocks

我们有project.factory.block(),用于从给定地址提取基本代块。

>>> block = proj.factory.block(proj.entry) # 取出一块代码 从程序的入口点
<Block for 0x401670, 42 bytes>

>>> block.pp()                          # 将反汇编打印到标准输出
0x401670:       xor     ebp, ebp
0x401672:       mov     r9, rdx
0x401675:       pop     rsi
0x401676:       mov     rdx, rsp
0x401679:       and     rsp, 0xfffffffffffffff0
0x40167d:       push    rax
0x40167e:       push    rsp
0x40167f:       lea     r8, [rip + 0x2e2a]
0x401686:       lea     rcx, [rip + 0x2db3]
0x40168d:       lea     rdi, [rip - 0xd4]
0x401694:       call    qword ptr [rip + 0x205866]

>>> block.instructions                  # 此块代码 拥有的指令数
0xb
>>> block.instruction_addrs             # 此块代码 每条指令的起始4地址
[0x401670, 0x401672, 0x401675, 0x401676, 0x401679, 0x40167d, 0x40167e, 0x40167f, 0x401686, 0x40168d, 0x401694]

此外,你可以通过block得到代码块的其他表示形式

>>> block.capstone                       # capstone disassembly &啥啥啥反汇编?
<CapstoneBlock for 0x401670>
>>> block.vex                            # VEX IRSB(这是python内部地址,不是程序地址)
<pyvex.block.IRSB at 0x7706330>
States(状态)

Project对象只代表程序的一个初始化镜像,当你使用angr来执行时,你使用的是表示模拟程序状态的特定对象SimState

>>> state = proj.factory.entry_state()
<SimState @ 0x401670>

一个SimState包括程序内存,寄存器,文件系统数据…任何可以通过执行来更改的“活动数据”在state中都可以找到。现在我们可以使用state.regsand state.mem

>>> state.regs.rip        # 获取当前指令指针,即当前执行到的代码地址,或者说获取EIP寄存器里存储的值
<BV64 0x401670>
>>> state.regs.rax
<BV64 0x1c>
>>> state.mem[proj.entry].int.resolved  # &将入口点的内存地址解释成C int数据(interpret the memory at the entry point as a C int)
<BV32 0x8949ed31>
  • state.regs:寄存器
  • state.mem:内存地址

这些是bitvector变量。可以把它看作是一个整数,用一系列位来表示,在angr里用来表示CPU数据。注意,每个位向量都有一个.length属性,以位为单位描述它的宽度。

这里是如何将python int转换为位向量,然后再转换回来:

>>> bv = state.solver.BVV(0x1234, 32)       # 创建一个32位宽的bitvector变量,其值为0x1234
<BV32 0x1234>                               # BVV stands for bitvector value
>>> state.solver.eval(bv)                # 转换为python int变量
0x1234

你可以把这些bitvector赋给寄存器或内存,或者直接赋值整数,它将被自动转为合适大小的bitvector

>>> state.regs.rsi = state.solver.BVV(3, 64)
>>> state.regs.rsi
<BV64 0x3>

>>> state.mem[0x1000].long = 4
>>> state.mem[0x1000].long.resolved
<BV64 0x4>

其中mem的简短使用方式为:

  1. 使用array[index]的形式来指定地址;
  2. 使用.<type>来指定内存需要解释为的类型(char, short, int, long, size_t, uint8_t, uint16_t...)
  3. 通过它
    • 你可以存入一个值(bitvector或python int)
    • .resolved来获取bitvector
    • .concrete来获取python int值。

如果你尝试访问更多的寄存器,你将会发现一些奇怪的值。

>>> state.regs.rdi
<BV64 reg_48_11_64{UNINITIALIZED}>

这还是一个64位的位向量,但是里面并没有存储一个数值,取而代之的是一个名字!这被称为“符号变量”,并且这是符号化执行的基础。

仿真管理器(Simulation Managers)

如果一个state使我们能够为我们实时呈现一个程序的执行状态,那么就必须有一个方法使我们达到程序执行的下一个状态。一个“仿真管理器”是angr中使用state来实现执行(模拟,或者任何你喜欢的称呼)的基本接口。作为一个简要的介绍,我们展示如何让我们之前创建的state实例“前进”几个基本块。

首先,我们创建一个将会使用到的SM。构造函数接收一个state或一个state的列表作为参数:

>>> simgr = proj.factory.simulation_manager(state)
<SimulationManager with 1 active>
>>> simgr.active
[<SimState @ 0x401670>]

一个SM可能包含多个存储state的列表。默认的列表是active,它和我们传入的state(或state的列表)一起被初始化。如果我们觉得不够的话,可以看看simgr.active[0]来看看我们当前的状态信息。

现在……做一些执行操作。

simgr.step()

我们刚刚演示了执行一个可符号化执行的基本块!我们可以再看看active列表,注意到它已经被更新了,并且不仅如此,它没有修改我们的初始状态。SimState对象在执行时被视为“不可变”的——你可以安全地将一个单独的state作为多轮执行的一个“基点”。

>>> simgr.active
[<SimState @ 0x1020300>]
>>> simgr.active[0].regs.rip                 # 崭新的值
<BV64 0x1020300>
>>> state.regs.rip                           # 依旧相同
<BV64 0x401670>

分析器(Analyses)

angr封装了一些可能会被用来从程序中提取一些有趣信息的内置分析工具。

>>> proj.analyses.            # 在ipython中按TAB键可以得到所有内容的自动补全列表:
 proj.analyses.BackwardSlice        proj.analyses.CongruencyCheck      proj.analyses.reload_analyses       
 proj.analyses.BinaryOptimizer      proj.analyses.DDG                  proj.analyses.StaticHooker          
 proj.analyses.BinDiff              proj.analyses.DFG                  proj.analyses.VariableRecovery      
 proj.analyses.BoyScout             proj.analyses.Disassembly          proj.analyses.VariableRecoveryFast  
 proj.analyses.CDG                  proj.analyses.GirlScout            proj.analyses.Veritesting           
 proj.analyses.CFG                  proj.analyses.Identifier           proj.analyses.VFG                   
 proj.analyses.CFGEmulated          proj.analyses.LoopFinder           proj.analyses.VSA_DDG               
 proj.analyses.CFGFast              proj.analyses.Reassembler

下面是如何能够创建并使用一个快速的程序控制流程图的例子:

# 最初,当我们加载这个二进制文件时,它也将所有依赖项加载到同一个虚拟地址空间中
# 这对于大多数分析来说是不可取的。
>>> proj = angr.Project('/bin/true', auto_load_libs=False)
>>> cfg = proj.analyses.CFGFast()
<CFGFast Analysis Result at 0x2d85130>

# cfg.graph is a networkx DiGraph full of CFGNode instances
# 您应该去查询networkx api来学习如何使用它!
>>> cfg.graph
<networkx.classes.digraph.DiGraph at 0x2da43a0>
>>> len(cfg.graph.nodes())
951

# 要获取给定地址的CFGNode,使用 cfg.get_any_node
>>> entry_node = cfg.get_any_node(proj.entry)
>>> len(list(cfg.graph.successors(entry_node)))
2

加载一个二进制文件(Loading a Binary)

我们简要提到了angr的二进制加载组件,CLE。CLE代表“CLE Loads Everything”,负责加载二进制文件(以及任何这个文件以来的库),并用一种容易操作的方式将它们展示给angr的其余组件。

加载器 (The Loader)

让我们重新加载examples/fauxware/fauxware并且深入学习如何和loader交互:

>>> import angr, monkeyhex
>>> proj = angr.Project('examples/fauxware/fauxware')
>>> proj.loader
<Loaded fauxware, maps [0x400000:0x5008000]>
已加载的对象(Loaded Objects)

CLE加载器(cle.Loader)代表一整个被加载的二进制对象的集合,它们被加载并且映射到同一个内存空间中。每一个二进制对象都被一个能够处理对应文件类型(cle.Backend的子类)的加载器后端加载。比如,cle.ELF被用来加载ELF格式的二进制文件。

内存中也会有一些和任何被加载的二进制文件都无关的对象。比如,被用来提供线程本地存储(Thread-Local storage)支持的对象和用于提供未解析的符号的外部对象。

你可以使用loader.all_objects来获取CLE已经加载的所有对象,也可以指定更加具体的类别来访问这些对象:

# 所有加载对象
>>> proj.loader.all_objects
[<ELF Object fauxware, maps [0x400000:0x60105f]>,
 <ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>,
 <ELF Object ld-2.23.so, maps [0x2000000:0x2227167]>,
 <ELFTLSObject Object cle##tls, maps [0x3000000:0x3015010]>,
 <ExternObject Object cle##externs, maps [0x4000000:0x4008000]>,
 <KernelObject Object cle##kernel, maps [0x5000000:0x5008000]>]

# 这是“main”对象,是您在加载项目时直接指定的对象
>>> proj.loader.main_object
<ELF Object fauxware, maps [0x400000:0x60105f]>

# 这是一个从共享对象名到对象的字典映射
>>> proj.loader.shared_objects
{ 'fauxware': <ELF Object fauxware, maps [0x400000:0x60105f]>,
  'libc.so.6': <ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>,
  'ld-linux-x86-64.so.2': <ELF Object ld-2.23.so, maps [0x2000000:0x2227167]> }

# 这是所有从ELF文件中加载的对象
# 如果这是一个windows程序,我们会使用 all_pe_objects
>>> proj.loader.all_elf_objects
[<ELF Object fauxware, maps [0x400000:0x60105f]>,
 <ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>,
 <ELF Object ld-2.23.so, maps [0x2000000:0x2227167]>]

# 这是“externs对象”,我们使用它为未解析的导入和angr内部提供地址
>>> proj.loader.extern_object
<ExternObject Object cle##externs, maps [0x4000000:0x4008000]>

# 此对象用于为模拟的系统调用提供地址
>>> proj.loader.kernel_object
<KernelObject Object cle##kernel, maps [0x5000000:0x5008000]>

# 最后,您可以获得一个对象的引用,该对象中有一个地址
>>> proj.loader.find_object_containing(0x400000)
<ELF Object fauxware, maps [0x400000:0x60105f]>

你可以直接与这些对象交互,从它们中提取元数据:

>>> obj = proj.loader.main_object

# 对象的入口点
>>> obj.entry
0x400580

>>> obj.min_addr, obj.max_addr
(0x400000, 0x60105f)

# 获取ELF的内存分段和文件分段:
>>> obj.segments
<Regions: [<ELFSegment memsize=0xa74, filesize=0xa74, vaddr=0x400000, flags=0x5, offset=0x0>,
           <ELFSegment memsize=0x238, filesize=0x228, vaddr=0x600e28, flags=0x6, offset=0xe28>]>
>>> obj.sections
<Regions: [<Unnamed | offset 0x0, vaddr 0x0, size 0x0>,
           <.interp | offset 0x238, vaddr 0x400238, size 0x1c>,
           <.note.ABI-tag | offset 0x254, vaddr 0x400254, size 0x20>,
            ...etc

# 你可以得到一个单独的段或节地址,它包含:
>>> obj.find_segment_containing(obj.entry)
<ELFSegment memsize=0xa74, filesize=0xa74, vaddr=0x400000, flags=0x5, offset=0x0>
>>> obj.find_section_containing(obj.entry)
<.text | offset 0x580, vaddr 0x400580, size 0x338>

# 获取PLT表信息
>>> addr = obj.plt['strcmp']
>>> addr
0x400550
>>> obj.reverse_plt[addr]
'strcmp'

# 展示预链接基址和实际装载到的内存基址,它实际上是映射到内存中的CLE
>>> obj.linked_base
0x400000
>>> obj.mapped_base
0x400000
符号和重定位(Symbols and Relocations)

在使用CLE的同时也可以使用符号。一个“符号”在形式化执行的世界中是一个基础概念,它将一个名字有效地(effectively)映射到一个地址。

从CLE中获取符号,最简单的方法是使用loader.find_symbol, 它接收一个名字或者一个地址并返回一个符号对象。

>>> strcmp = proj.loader.find_symbol('strcmp')
>>> strcmp
<Symbol "strcmp" in libc.so.6 at 0x1089cd0>

一个符号最有用的属性是它的名字、父对象(owner)、和它的地址。但是一个符号的“地址”却是一个含糊的概念。有三种方式来准确表述一个符号的“地址”:

  • .rebased_addr 是一个符号在全局地址空间中的地址。这就是你直接打印symbol.addr会显示的内容。
  • .linked_addr是符号相对于二进制文件预链接基址的地址,这个地址和用诸如readelf(1)显示的地址是一样的。
  • .relative_addr是符号相对于对象基址的地址。在一些文献中(尤其在windows的文献中),这样的地址被称为RVA(相对虚拟地址)
>>> strcmp.name
'strcmp'

>>> strcmp.owner
<ELF Object libc-2.23.so, maps [0x1000000:0x13c999f]>

>>> strcmp.rebased_addr
0x1089cd0
>>> strcmp.linked_addr
0x89cd0
>>> strcmp.relative_addr
0x89cd0

除了提供调试信息外,符号还支持动态链接的概念。libc将strcmp符号作为导出提供,主要的二进制文件依赖于它。如果我们要求CLE直接从主对象中给我们一个strcmp符号,它会告诉我们这是一个导入符号。导入符号没有与它们相关联的有意义的地址,但是它们提供了对用于解析它们的符号的引用,如.resolvedby

>>> strcmp.is_export
True
>>> strcmp.is_import
False

# 在之前的loader中,我们使用find_symbol方法,因为它执行一个搜索操作来找到指定的符号
# 在一个独立的对象中,这个方法是get_symbol,因为一个给定的名字只会对应一个符号
>>> main_strcmp = proj.loader.main_object.get_symbol('strcmp')
>>> main_strcmp
<Symbol "strcmp" in fauxware (import)>
>>> main_strcmp.is_export
False
>>> main_strcmp.is_import
True
>>> main_strcmp.resolvedby
<Symbol "strcmp" in libc.so.6 at 0x1089cd0>

导入符号(import symbol)和导出符号(export symbol)的关系应该在内存中以一种特殊的方式被记录,这种方式引出另一种特殊的概念——“重定位”。“重定位”说的是:当你将一个[import]和一个导出符号匹配的时候,请将导出符号的地址按照[format]的形式写到[location]。我们可以通过obj.relocs(获取Relocation实例)看到一个对象的完成重定位表,或者通过obj.imports看到符号名和他们的重定位地址的映射关系。对于导出符号表,angr中没有相应对象与之对应。一个对象中需要重定位的相应导入符号可以用.symbol获取。重定位将会写入的地址可以通过访问一个Symbol的地址的任何方式获取,你还可以通过.owner_obj来获得一个请求重定位的对象的引用。

# 重定位没有良好的漂亮打印,所以这些地址是python内部的,与我们的程序无关
>>> proj.loader.shared_objects['libc.so.6'].imports
{'__libc_enable_secure': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fce780>,
 '__tls_get_addr': <cle.backends.elf.relocation.amd64.R_X86_64_JUMP_SLOT at 0x7ff5c6018358>,
 '_dl_argv': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fd2e48>,
 '_dl_find_dso_for_object': <cle.backends.elf.relocation.amd64.R_X86_64_JUMP_SLOT at 0x7ff5c6018588>,
 '_dl_starting_up': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fd2550>,
 '_rtld_global': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fce4e0>,
 '_rtld_global_ro': <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT at 0x7ff5c5fcea20>}

如果一个导入符号不能被解析为任何导出符号,比如找不到对应的共享库文件,CLE将会自动更新loader.extern_obj来表明这个符号由CLE导出。

加载选项(Loading Options)

如果你正在使用angr.Project加载一些东西并且你想要给cle.loader实例传一个选项来创建Project,你可以直接传入关键字参数给Project的构造函数,你传入的关键字就会被传给CLE。如果你想要知道所有能够被作为选项传入的参数,你可以查看CLE的API文档。在本文档中,我们只看一些重要的并且被频繁使用的选项。

基本选项

我们已经用过了auto_load_libs选项——它控制CLE是否自动加载共享库文件,默认是自动加载的。另外,有一个相反的选项except_missing_libs,这个选项如果被设置为true,将在二进制包含无法解析的共享库时抛出一个异常。

你可以传入一个字符串列表给force_load_libs选项,每一个被列出的字符串将会被当做一个不可解析共享库依赖,或者你可以传入一个字符串列表给skip_libs来防止列表列出的共享库的被作为依赖添加。另外,你可以传入一个字符串列表给custom_ld_path选项,这个选项中的字符串会被作为额外的搜索共享库文件的路径,这些路径将会比任何默认路径先被搜索,默认路径包括:被加载文件所在的路径,当前工作路径,系统库路径。

Per-Binary 选项

如果你想要对一个特定的二进制对象设置一些选项,CLE也能满足你的需求。参数main_optslib_opts接收一个以python字典形式存储的选项组。main_opts接收一个形如{选项名1:选项值1,选项名2:选项值2……}的字典,而lib_opts接收一个库名到形如{选项名1:选项值1,选项名2:选项值2……}的字典的映射。

这些选项的内容因不同的后台而异,下面是一些通用的选项:

  • backend —— 使用哪个后台,可以是一个对象,也可以是一个名字(字符串)
  • custom_base_addr —— 使用的基地址
  • custom_entry_point —— 使用的入口点
  • custom_arch —— 使用的处理器体系结构的名字
>>> angr.Project('examples/fauxware/fauxware', main_opts={'backend': 'blob', 'arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})
<Project examples/fauxware/fauxware>
后台(Backends)

CLE目前有能够静态加载ELF,PE,CGC,Mach-O和ELF核心转储文件的后台,并且支持使用IDA加载二进制和加载文件到一个平坦地址空间。大部分情况下,CLE将会自动检测二进制文件来决定使用哪个后台,所以除非你在做一些很奇怪的工作,一般情况下你不需要指定使用哪个后台。

你可以和上一节描述的一样,用传入一个键的方式强制CLE使用指定的后台。一些后台不能被自动检测,因此必须用custom_arch指定。键值不需要匹配任何一个架构:根据你给出的任意架构的任意通用标识符,angr都能够识别出你指的是哪个架构。

为了指定使用的后台,使用下表列出的名字:

名字描述是否需要用custom_arch指定?
elf基于PyELFTools的ELF文件静态加载器不需要
pe基于PEFile的静态PE文件加载器不需要
mach-oMach-O文件的静态加载器,不支持动态链接或者变基不需要
cgcCyber Grand Challenge系统中的二进制文件的静态加载器不需要
backedcgc允许指定内存和寄存器支持的CGC二进制文件静态加载器不需要
elfcoreELF核心转储文件的静态加载器不需要
ida启动ida来分析文件需要
blob按照平坦模式加载文件到内存中需要

符号函数摘要集(Symbolic Function Summaries)

默认情况下,Project都会尝试用SimProcedures这个符号摘要集(symbolic summaries)替换主程序的外部调用。SimProcedure使用
pyhton函数高效地模拟外部库函数对state的影响。我们已经在SimProcedure中实现了一个完整的函数集
。这些内建过程(procedures)能够在angr.SIM_PROCEDURES字典中获得,这个字典是一个两层结构,第一层的键是包名(libc,posix,win32,stubs),第二层的键是库函数的名字。用SimProcedure替代实际库函数的执行虽然存在一些潜在的不准确性,但是可以让你的分析更加可控,

当找不到某个函数的摘要时:

  • 如果auto_load_libs的值为True(默认值),那么真正的库函数就会被执行。这可能是你想要的,也可能不是,取决于实际的函数是什么。比如说,一些libc函数十分复杂,难以分析,并且极有可能导致用于确定执行的路径的状态数爆炸。
  • 如果auto_load_libsFalse,那么外部函数就是“未解析”的状态,并且Project对象将会将它们解析为叫做ReturnUnconstrained的通用“stub” SimProdurce。就如它的名字所描述的:每次这个符号被调用,它都会将一个唯一的无约束符号作为返回值。
  • 如果use_sim_procedures(这是angr.Project的参数,不是cle.loader的参数)是False(默认是True),那么只有外部对象提供的符号才会被SimProcedures替代,并且他们将会被一个ReturnUnconstrainedstub替代。
  • 你可以给angr.Project传以下参数来指定一个不想被SimProcedure替代的符号:
    exclude_sim_procedures_listexclude_sim_procedures_func
  • 查看angr.Project._register_object的源码来获取精确的算法。
钩子(Hooking)

angr使用python函数摘要替换库函数代码的机制叫做Hooking,而且你也可以这么做!在执行一次模拟(simulation)时,每一步执行angr都会检查当前地址是否被下了钩子(hooked),并且如果检查到钩子,就会执行钩子函数,而不是那个地址里的二进制码。使用APIproj.hook(addr,hook)可以完成hook,这里参数中的hook是一个SimProcedure实例。你可以用.is_hooked.unhook.hook_by属性来管理你project中的钩子,这些属性的含义就如字面的意思,这里就不解释了。

通过把proj.hook(addr)作为一个函数装饰器(function decorator)。,你可以指定你自己的函数作为hook函数。如果你这么做了,你还可以指定一个可选的关键词参数length来决定在你的hook函数执行结束之后,程序跳过多少字节的机器码再继续执行。

>>> stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'] # this is a CLASS
>>> proj.hook(0x10000, stub_func())  # hook的实例

>>> proj.is_hooked(0x10000)            # 这些函数可以很好的自我解释
True
>>> proj.hooked_by(0x10000)
<ReturnUnconstrained>
>>> proj.unhook(0x10000)

>>> @proj.hook(0x20000, length=5)
... def my_hook(state):
...     state.regs.rax = 1

>>> proj.is_hooked(0x20000)
True

此外,你可以使用proj.hook_symbol(name,hook),提供一个符号名称作为第一个参数,这样第二个参数指定的钩子函数会被下到这个符号被调用的每个地址中。它的一个很重要的用途是用来扩展angr的内建库SimProcedure的行为。因为这些库函数仅仅是一些类,你可以写它们的子类,重写它们的方法,并且把这些子类使用在hook中。

求解引擎(Solver Engine)

angr的强大能力不仅因为它是一个模拟器,还因为它能够使用我们称为“符号变量”的东西参与程序的执行。它可以使用一个符号,也就是一个名字,来代替程序执行中的确切数值。然后,在使用符号进行数学运算的过程中,将会产生一棵树(这棵树在编译理论中被称为抽象语法树或AST)。这些AST可以被解释为SMT解析器中的约束条件,就像Z3做的一样。为了回答这样一个问题:“已知经过一系列操作之后得到的输出,输入应该满足怎样的条件?”,在这里,你将会学习如何使用angr来解决这个问题。

Working with Bitvectors

让我们得到一个虚拟的project和state,这样我们就可以和数字玩了。

>>> import angr, monkeyhex
>>> proj = angr.Project('/bin/true')
>>> state = proj.factory.entry_state()

位向量(Bitvectors)就是有很多位的序列,可以解释为数学上的有界整数。让我们先生成一些位向量:

# 64位的位向量,值为1和100
>>> one = state.solver.BVV(1, 64)
>>> one
 <BV64 0x1>
>>> one_hundred = state.solver.BVV(100, 64)
>>> one_hundred
 <BV64 0x64>

# 创造一个值为9,27位的位向量
>>> weird_nine = state.solver.BVV(9, 27)
>>> weird_nine
<BV27 0x9>

正如你所看到的,你可以有任何位序列,称它们为位向量。你也可以用它们做数学题:

>>> one + one_hundred
<BV64 0x65>

# 你可以提供正常的python整数,他们将被强制转换到适当的类型:
>>> one_hundred + 0x100
<BV64 0x164>

# 正常含有算术应用的语义。。?(The semantics of normal wrapping arithmetic apply)
>>> one_hundred - one*200
<BV64 0xffffffffffffff9c>

你不能做诸如one+weird_nine这样的操作。对不同长度的位向量做数学运算,将会导致一个类型错误。然而,你可以扩展这个weird_nine,让他具有合适的比特长度:

>>> weird_nine.zero_extend(64 - 27)
<BV64 0x9>
>>> one + weird_nine.zero_extend(64 - 27)
<BV64 0xa>

zero_extend将会用0扩展位向量的高位。为了在扩展时保持二进制补码表示的有符号整数的值,你还可以使用sign_extend来带符号扩展一个位向量。

现在让我们混入一些符号参与计算:

# 创建一个名为“x”的位向量符号,长度为64位
>>> x = state.solver.BVS("x", 64)
>>> x
<BV64 x_9_64>
>>> y = state.solver.BVS("y", 64)
>>> y
<BV64 y_10_64>

xy现在是“符号变量”,这跟咱们小学学的未知数是一个意思。注意,你提供的符号名(第一个参数)的后面被添加了一个递增的计数值。你可以对这些符号变量做数学计算,想做多少就做多少,但是你不会得到一个数值的结果,而是得到一个AST:

>>> x + one
<BV64 x_9_64 + 0x1>

>>> (x + one) / 2
<BV64 (x_9_64 + 0x1) / 0x2>

>>> x - y
<BV64 x_9_64 - y_10_64>

从技术概念上说xy甚至one都是AST——即使只有一层。为了理解这一点,让我们学习如何处理AST。

每一个AST都有.op和一个.args

  • .op是一个定义了要执行的运算的属性

  • .args是参与运算的操作数。

除非op是字符串"BVV"或"BVS",所有的args都是AST。AST的终结节点是BVV或BVS。

>>> tree = (x + 1) / (y + 2)
>>> tree
<BV64 (x_9_64 + 0x1) / (y_10_64 + 0x2)>
>>> tree.op
'__floordiv__'
>>> tree.args
(<BV64 x_9_64 + 0x1>, <BV64 y_10_64 + 0x2>)
>>> tree.args[0].op
'__add__'
>>> tree.args[0].args
(<BV64 x_9_64>, <BV64 0x1>)
>>> tree.args[0].args[1].op
'BVV'
>>> tree.args[0].args[1].args
(1, 64)

从现在开始,我们将会使用“位向量”这个词来表示一个最上层运算产生一个位向量的AST。通过AST还可以表示其他数据类型,包括浮点数和布尔类型(我们很快会看到)。

符号约束(Symbolic Constraints)

在任意两个类型相似的AST之间执行比较操作将生成另一个AST——不是位向量,而是一个符号布尔值。

>>> x == 1
<Bool x_9_64 == 0x1>
>>> x == one
<Bool x_9_64 == 0x1>
>>> x > 2
<Bool x_9_64 > 0x2>
>>> x + y == one_hundred + 5
<Bool (x_9_64 + y_10_64) == 0x69>
>>> one_hundred > 5
<Bool True>
>>> one_hundred > -5
<Bool False>

从上述例子中你可以看出AST间的比较默认是无符号的。例子中的-5将会被包装为<BV64 0xfffffffffffffffb>,这个数被解释为无符号数之后,显然大于100 。如果你想让比较是带符号的,你可以使用one_hundred.SGT(-5)(SGT意思是“signed greater-than”)。在本章的最后可以找到完整的AST操作列表。

上面的例子还阐述了一个使用angr的要点——你不应该直接在一个if或while语句中使用两个变量的比较结果作为判断标准,因为这个比较结果可能不是一个确定的真值。即使比较的结果为真,if one > one_hundred将会产生异常。你应该使用solver.is_truesolver.is_false,它们将会测试出一个具体的真假值,而不执行约束求解。

>>> yes = one == 1
>>> no = one == 2
>>> maybe = x == y
>>> state.solver.is_true(yes)
True
>>> state.solver.is_false(yes)
False
>>> state.solver.is_true(no)
False
>>> state.solver.is_false(no)
True
>>> state.solver.is_true(maybe)
False
>>> state.solver.is_false(maybe)
False

约束求解(Constraint Solving)

你可以用加入一个约束条件到一个state中的方法(state.solver.add),将每一个符号化的布尔值作为一个关于符号变量合法性的断言。之后可以通过使用state.solver.eval(symbol)对各个断言进行评测来求出一个合法的符号值(若有多个合法值,返回其中的一个)。

>>> state.solver.add(x > y)
>>> state.solver.add(y > 2)
>>> state.solver.add(10 > x)
>>> state.solver.eval(x)
4

通过添加这些约束到一个state中,我们已经迫使约束求解器考虑我们添加的断言,求解器返回的值必须满足这些断言。如果你运行上面的例子,你可能会得到x的不同值,但是这个值将必然大于3(因为y > 2且x >= y 且 x <= 10)。此外,如果你接着用state.solver.eval(y)求解y,你将会得到一个被之前求解出的x的值限制的y值。如果在两个查询之间不添加任何约束,那么结果将是一致的。

从这里,很容易看到如何完成我们在本章开始时提出的任务——查找产生给定输出的输入。

# 获得一个没有约束的新状态
>>> state = proj.factory.entry_state()
>>> input = state.solver.BVS('input', 64)
>>> operation = (((input + 4) * 3) >> 1) + input
>>> output = 200
>>> state.solver.add(operation == output)
>>> state.solver.eval(input)
0x3333333333333381

再次提醒,这样的求解方式只适用于位向量的语义下(也就是之前提到的AST)。如果我们在整数域上做上面的操作,将会是无解的。

如果我们将两个互相矛盾或相反的约束加入一个state.solver中,比如没有一个值能够满足所有约束,那么这个state就变成了unsatisfiable,或者unsat,并且对这样的state求解会导致异常。你可以用state.satisfiable()检查一个state是否可解,接着上面的例子:

>>> state.solver.add(input < 2**32)
>>> state.satisfiable()
False

你还可以加入更复杂的表达式,而不仅是包含一个符号变量的表达式:

# 新状态
>>> state = proj.factory.entry_state()
>>> state.solver.add(x - y >= 4)
>>> state.solver.add(y > 0)
>>> state.solver.eval(x)
5
>>> state.solver.eval(y)
1
>>> state.solver.eval(x + y)
6

这里我们可以看到,eval是一个通用的方法,它在考虑整个state(的约束)的情况下,将任意位向量转换为python基本类型。这也是为什么我们使用eval来将具体的位向量转换为python的int类型的原因。

还需要注意的是符号变量x和y可以在新的state中被使用,尽管它在旧的state中被创建。符号变量不和state绑定,它们是自由存在的。

浮点数(Floating point numbers)

z3(一个约束求解的python库)已经提供了对IEEE754浮点数标准的支持,并且因此angr也能够使用它们(因为angr集成了z3)。创建浮点数向量和创建一个向量主要的不同在于,浮点数向量的第二个参数不是位向量宽度,而是一个sort。你可以使用FPVFPS来创建一个浮点值和符号。

# 新状态
>>> state = proj.factory.entry_state()
>>> a = state.solver.FPV(3.2, state.solver.fp.FSORT_DOUBLE)
>>> a
<FP64 FPV(3.2, DOUBLE)>

>>> b = state.solver.FPS('b', state.solver.fp.FSORT_DOUBLE)
>>> b
<FP64 FPS('FP_b_0_64', DOUBLE)>

>>> a + b
<FP64 fpAdd('RNE', FPV(3.2, DOUBLE), FPS('FP_b_0_64', DOUBLE))>

>>> a + 4.4
<FP64 FPV(7.6000000000000005, DOUBLE)>

>>> b + 2 < 0
<Bool fpLT(fpAdd('RNE', FPS('FP_b_0_64', DOUBLE), FPV(2.0, DOUBLE)), FPV(0.0, DOUBLE))>

所以这里有一些需要解释的地方——首先对于浮点数向量的显示并不是很好,但是抛开这个不谈,大多数的浮点数操作实际上都有第三个参数,它在你使用二进制运算符时被隐式地添加——这个参数是舍入模式。IEEE754定义了多个舍入模式(向最近的数舍入,向0舍入,舍入到正数等等),所以z3必须支持它们。如果你想要对某个操作(比如solver.fpAdd)指定舍入模式,你就得在使用该操作时显示声明一个舍入模式(solver.fp.RM_*中的一个)作为参数。

浮点数符号的约束和求解工作按照和整型符号相同的方式工作,但是使用eval将会返回一个浮点值:

>>> state.solver.add(b + 2 < 0)
>>> state.solver.add(b + 2 > -1)
>>> state.solver.eval(b)
-2.4999999999999996

这很好,但是有时候我们需要能够直接和浮点数的位向量形式直接交互。你可以使用raw_to_bvraw_to_fp将位向量解析为浮点数,反之亦可:

>>> a.raw_to_bv()
<BV64 0x400999999999999a>
>>> b.raw_to_bv()
<BV64 fpToIEEEBV(FPS('FP_b_0_64', DOUBLE))>

>>> state.solver.BVV(0, 64).raw_to_fp()
<FP64 FPV(0.0, DOUBLE)>
>>> state.solver.BVS('x', 64).raw_to_fp()
<FP64 fpToFP(x_1_64, DOUBLE)>

这些转换保留了位模式,就像把一个int指针转为浮点指针(或相反)一样。然而,如果你想尽量不丢失精度,当你想将一个浮点数转为int(或反过来)你可以使用另一组方法:val_to_fpval_to_bv。由于浮点数的浮点特性,这些方法必须将目标值的位宽或种类作为参数:

>>> a
<FP64 FPV(3.2, DOUBLE)>
>>> a.val_to_bv(12)
<BV12 0x3>
>>> a.val_to_bv(12).val_to_fp(state.solver.fp.FSORT_FLOAT)
<FP32 FPV(3.0, FLOAT)>

这些方法还可以加一个signed参数,指定源或目的位向量是否是有符号的。

更多的解决方法(More Solving Methods)

eval将会给出一个符合约束条件的可行解,但是如果你想要多个可行解时怎么办呢?你如何确定这个解是不是唯一的?解析器为你提供了一些通用的解决方案:

  • solver.eval(expression) 将会解出一个可行解
  • solver.eval_one(expression)将会给出一个表达式的可行解,若有多个可行解,则抛出异常。
  • solver.eval_upto(expression, n)将会给出最多n个可行解,如果不足n个就给出所有的可行解。
  • solver.eval_exact(expression, n)将会给出n个可行解,如果解的个数不等于n个,将会抛出异常。
  • solver.min(expression)将会给出最小可行解
  • solver.max(expression)将会给出最大可行解

另外,上述这些方法都可以接收如下关键字参数:

  • extra_constraints可以传入元组形式的约束条件。这些约束将会在本次求解中被考虑,但是不加入state中。
  • cast_to可以接收一个参数来指定把结果映射到哪种数据类型。目前这个参数只能是str,它将会以字符串形式展示返回的结果。例如:state.solver.eval(state.solver.BVV(0x41424344, 32), cast_to=bytes) will return b'ABCD'.

小结

内容真多!读完本章之后,你应该能够创建和操作位向量、布尔值和浮点数来形成操作树,之后用附加在state上的约束求解器,根据约束条件集,求得一个(或多个)可行解。希望你读完本章能够体会到用AST来表示运算以及约束求解器的强大。

附录中,你能够找到可以对AST进行的所有操作。

项目状态(Program State)

到目前为止,我们只在最基础的层次上使用angr模拟的程序状态(SimState对象)来展示angr操作的核心概念。在这里,你将会学习更多的state对象的结构,并且学习多样、有用的方式和它交互。

回顾:读、写内存和寄存器

如果你按照顺序阅读本文档(至少在文档的第一部分你应该这么做),那么你应该已经看到了访问内存和寄存器的基本方法。state.regs对象通过以各个寄存器名命名的属性,提供对各寄存器的读写权限;state.mem提供了对内存的读写权限,你可以通过下标索引符号访问各个内存,可以使用内存对象的属性来指定内存应该被作为什么类型的数据处理。

另外,现在你应该知道什么是AST,并且理解了任何位向量形式的AST都可以被存储在内存或寄存器中。

下面是一些关于从state是拷贝和执行数据操作的快捷例子:

>>> import angr, claripy
>>> proj = angr.Project('/bin/true')
>>> state = proj.factory.entry_state()

# 将rsp赋值给rbp
>>> state.regs.rbp = state.regs.rsp

# 将rdx赋值给内存地址0x1000
>>> state.mem[0x1000].uint64_t = state.regs.rdx

# 废弃rbp
>>> state.regs.rbp = state.mem[state.regs.rbp].uint64_t.resolved

# add rax, qword ptr [rsp + 8]
>>> state.regs.rax += state.mem[state.regs.rsp + 8].uint64_t.resolved

基础执行(Basic Execution)

在前面的文档中,我们展示了如何使用Simulatin Manager(SM)来做一些基础执行。在下一章我们将会展示SM的全部能力,但是现在我们使用一个简单得多的接口state.step()来展示符号执行是如何工作的。这个方法会执行一步符号执行操作,并返回一个Simsuccessors。和正常的模拟不同,符号执行可以产生几种可以按照多种方式分类的后继状态。现在我们只关心这个对象的.successors属性,它是包含程序执行一步之后的所有“正常”后继状态的列表。

为什么使用列表而不是单个继承状态?angr的符号执行过程是仅仅取出编译在程序中的要执行的独立的指令,并且执行它来改变Simstate。当一行诸如if(x > 4)的语句被执行时,如果x是个符号(而没有具体数值),那么会发生什么呢?在angr底层的某个地方,比较x > 4将会被执行,且结果将会是<Bool x_32_1 > 4>

这很好,那么接下来的问题是,我们是要“true”分支,还是要“false”分支呢?答案是,我们两个都要!我们产生两个完全分开的状态——一个用于模拟条件为真,另一个用于模拟条件为假。在第一个状态中,我们添加x > 4为约束条件,而在第二个状态中,我们添加!(x > 4)为约束条件。用这样的方式,就能够确保不论何时我们使用这两个后继状态之一,状态中的约束条件都能够保证约束求解器解出的任何结果都是一个使得程序按照该状态指向的程序路径执行的合法输入。

为了演示这一点,让我们使用一个伪造的固件镜像 作为例子。如果你看过这个程序的源码,你将会看到它的授权机制有一个后门;任意用户名,都能通过验证,只要密码是“SOSNEAKY”。此外,用户输入的第一层比较判断是后门代码的判断,所以如果我们一直执行,直到我们得到不止一个继承状态,它们中的一个将会包含使得用户输入通过后门判断的限制条件。下面是代码实现:

>>> proj = angr.Project('examples/fauxware/fauxware')
>>> state = proj.factory.entry_state(stdin=angr.SimFile)  # 现在先忽略这个问题——为了方便学习,我们正在禁用一个更复杂的默认设置
>>> while True:
...     succ = state.step()
...     if len(succ.successors) == 2:
...         break
...     state = succ.successors[0]

>>> state1, state2 = succ.successors
>>> state1
<SimState @ 0x400629>
>>> state2
<SimState @ 0x400699

不要直接看这些状态的约束——我们刚刚执行过的代码中包括库函数strcmp的执行,用符号化模拟这个函数很复杂,并且导致了结果中的约束条件也很复杂。

我们模拟的程序从标准输入中读入数据,angr默认把标准输入看做一个无限符号数据流。为了得到满足约束的可能输入值,我们需要获取标准输入的实际内容的引用。我们将会在本章的后半部分介绍我们的文件和输入系统是如何工作的,但是现在只使用state.posix.stdin.load(0, state.posix.stdin.size)来获取从stdin读取的所有内容的位向量。

>>> input_data = state1.posix.stdin.load(0, state.posix.stdin.size)

>>> state1.solver.eval(input_data, cast_to=bytes)
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00SOSNEAKY\x00\x00\x00'

>>> state2.solver.eval(input_data, cast_to=bytes)
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x00\x80N\x00\x00 \x00\x00\x00\x00'

正如你看到的,为了进入state1指向的分支,你必须提供一个密码“SOSNEAKY”给后门代码。为了进入state2指向的分支,你必须给出不是“SOSNEAKY”的输入。z3已经从数以万计的符合条件的字符串中提取了一个符合条件的字符串给我们。

Fauxware是使用angr第一次成功符号执行的程序(2013年)。通过使用angr找到这个后门的同时,你也在参与一项伟大的传统:理解如何使用符号执行从二进制文件中提取有意义的信息。

状态预置(State Presets)

到目前为止,每当我们想要使用state,我们都用这样一个语句创建:project.factory.entry_state()。这只是factory提供的多个构造函数中的一个:

  • .blank_state() 构造一个“空状态”,它的大多数数据都是未初始化的。当使用未初始化的的数据时,一个不受约束的符号值将会被返回。
  • .entry_state构造一个已经准备好从函数入口点执行的状态。
  • .full_init_state()构造一个已经执行过所有与需要执行的初始化函数,并准备从函数入口点执行的状态。比如,共享库构造函数(constructor)或预初始化器。当这些执行完之后,程序将会跳到入口点。
  • .call_state()构造一个已经准备好执行某个函数的状态。

你可以通过对这些构造函数传参来构建自定义状态:

  • 所有这些构造函数都可以获取一个addr作为参数来从一个指定地点开始。
  • 如果你在一个可以获取命令行参数或环境参数的环境下运行,你可以传入一个args列表和一个存储环境参数的env字典给entry_steate()full_init_state。列表和字典里的值可以是字符串或位向量,并且被序列化存入state中,作为模拟执行的命令行参数和环境参数。默认的args是一个空列表,所以如果你要分析一个至少需要args[0]的程序,那么你应该提供它。

注:一般.exe和.elf文件的args[0]都是程序名。

  • 如果你希望argc被符号化,你可以传入一个位向量argcentry_statefull_init_state构造函数。但是要小心:如果你这么做了,那么你还得在结果状态中添加一条约束,即argc的值不能大于你传入args中的参数的个数。

  • 为了使用call_state,你应该用call_state(addr,arg1,arg2,arg3...)这样的方式,其中addr是要调用的函数的地址,argN是要调用的函数的第N个参数:可以是python整型、字符串、或者数组、或者位向量。如果你想申请一块内存并把指向内存的指针传入一个对象,那么你应该将指针用PointerWrapper包装一下,例如,angr.PointerWrapper("point to me")。这个API的调用结果可能有点不可预测,我们还在努力改进它。

  • 为了指定使用call_state来调用函数时的调用约定,你可以传入SimCCInstance 作为cc参数。

    我们正努力选择一个比较好的默认参数,但是在特定情况下需要你来告诉angr使用哪种调用约定。

还有更多能用于上述所有构造函数的选项,在本章的末尾会列出来。

低级内存接口

state.mem接口对于从内存中以指定类型取出数据是很方便的,但是如果你想手动在某块内存中做存取操作,这个接口就显得很笨拙了。实际上,state.mem只是一堆用于正确访问下层内存存储的逻辑,而内存存储是一块填充着位向量数据的平坦地址空间:state.memory。你可以直接对state.memory使用.load(addr,size)store(addr,val)方法:

>>> s = proj.factory.blank_state()
>>> s.memory.store(0x4000, s.solver.BVV(0x0123456789abcdef0123456789abcdef, 128))
>>> s.memory.load(0x4004, 6) # load大小以字节为单位,取6字节
<BV48 0x89abcdef0123>

可以看到,数据按照“大端”字节序存取,因为state.memory的主要目的是存取没有附加语义的大块数据。然而,如果你想对存取的数据执行字节交换,那么你可以传入关键字参数endness——如果你指定小端顺序,字节交换就会发生。字节序类型必须是在archinfo包中angr用于存储CPU架构陈述性信息的枚举变量Endness的成员之一。另外,被分析的程序的字节序类型可以在arch.memory_endness中找到——比如state.arch.memory_endness

>>> import archinfo
>>> s.memory.load(0x4000, 4, endness=archinfo.Endness.LE)
<BV32 0x67453201>

寄存器也有低级接口,state.registers:它和state.memory使用完全相同的API,但是要解释它的行为需要深入angr用于无缝操作各个CPU架构的抽象结构。比较简短的解释是它仅仅是一个寄存器文件,其中包含着archinfo中定义的寄存器和偏移的映射。

状态选项

你可以对angr内部做许多调整,这些调整在某些情况下会优化程序执行,而有些情况下则正相反。这些调整可以通过状态选项控制。

对于每个SimState对象,都有一个state,option集合,里面存储着所有所有已经开启的状态选项。每个选项(只是一个字符串)都以某种分钟的方式控制这angr执行引擎的行为。所有可选的选项,以及它们在不同类型的状态下的默认值都可以在这个附录中找到。你可以通过angr.options获取一个独立的(状态)选项来添加到某个state中。这些独立的(状态)选项都被大写字母命名,当然你有可能想要使用多个选项对象的组合,这些组合用小写字母命名。

当你通过任何一个构造函数构造一个SimState时,你可以传入关键字参数add_optionsremove_options,它们需要以选项集合的形式传入来修改默认的初始选项集合。

# 示例:启用延迟解决,这是一个导致尽可能不频繁地检查状态可满足性的选项。
# 对设置的更改将传播到这一行之后从此状态创建的所有后续状态。
>>> s.options.add(angr.options.LAZY_SOLVES)

# 创建一个启用延迟解决方案的新状态
>>> s = proj.factory.entry_state(add_options={angr.options.LAZY_SOLVES})

# 在不启用简化选项的情况下创建新状态
>>> s = proj.factory.entry_state(remove_options=angr.options.simplification)

第一行启动了“lazy solves”选项,这个选项将会导致对约束条件是否满足的检测尽可能慢地执行;这个改变将会对从这一行代码之后由这个状态产生的所有衍生状态有效。

第二行代码在初始化状态的时候加入了“lazy solves”选项。

第三行代码创建了一个没有simplification选项的状态。

状态插件(State Plugins)

除了刚刚讨论过的选项集,所有存储在SimState中的东西实际上都存储在附加在state上的“插件”中。到目前为止我们讨论的几乎所有state的属性都是一个插件——memoryregistersmemregssolver等等。这种设计带来了代码的模块化和能够便捷地为模拟状态的其他方面实现新的数据存储,或者提供插件的替代实现能力。

比如说,通常memory插件模拟一个平坦地址空间,但是在分析中可以选择开启“抽象内存”插件来支持state.memory,“抽象内存”使用新的数据类型表示地址,以模拟浮动的独立内存空间映射。反过来,插件可以减少代码的复杂性:state.memorystate.registers实际上是同一个插件的不同实例,因为寄存器也是用一块地址空间模拟的。

全局插件(The globals plugin)

state.global是一个很简单的插件:它实现了标准python dict接口,允许你在state中存储任意数据。

history插件

state.history是一个很重要的插件,它记录一个状态的执行路径。实际上它是一个链表,每一个节点代表一次执行——你可以使用state.history.parent.parent等等来回溯这个链表。

为了更方便地操作这个结构,history插件还提供了多个高效的迭代器来覆盖某些值的历史,这些值被存储为history.recent_NAME且它们的迭代器就是history.NAME。例如: for addr in state.history.bbl_addrs: print hex(addr) 将会打印出这个二进制的基础块地址跟踪信息。然而state.history.recent_bbl_addrs是最近执行的基础块地址的列表,state.history.parent.recent_bbl_addrs是上一个state最近执行的基础块地址的列表,等等。如果你需要快速获取这些值的平面列表,你可以使用.hardcopy,比如,state.history.bbl_addrs.hardcopy。但是请记住,基于索引的访问是在迭代器上实现的。

下面是一些存储在history插件中的值的简要列表:

  • history.descriptions是描述state上每轮执行的状态的字符串列表。
  • history.bbl_addrs是state已经执行过的基本块的地址列表。这里可能每轮执行一个以上,并且不是所有的地址都有对应的二进制代码——一些地址可能已经被SimProcedure hook了
  • history.jumpkinds是状态历史中每个控制流转换的处置列表,就像VEX枚举字符。
  • history.guards是一个state当前所走路径需要满足的条件列表。
  • history.events是一些执行过程中发生的“有趣的事情”的列表,比如说符号跳转条件、程序弹出一个消息框或者程序执行到退出代码退出。
  • history.action通常是空的,但如果你加入angr.options.refs选项到state中,它将填充程序对所有内存、寄存器、临时值访问的日志。
调用栈插件

angr会跟踪所模拟的程序的调用栈。在每条call指令处,一个栈帧会被加载到被跟踪的调用栈的栈顶,并且当栈指针低于最上层被调用的栈帧时,一个栈帧就被弹出。这使得angr能够稳健地存储当前模拟的函数的局部数据。

和history一样,调用栈仍然是链表结构的,但是没有提供调用栈的专门的迭代器——取而代之,你可以直接使用state.callstack来获取每一个没有被弹出的栈帧,顺序是最近调用优先。如果你想要最高的栈帧,那就是stata.callstack

  • callstack.func_addr是当前正在执行的函数地址。
  • callstack.call_site_addr是调用当前函数的基本块地址。
  • callstack.stack_ptr是从当前函数开始时的栈顶指针。(即ebp)
  • callstack.ret_addr是当前函数将要返回的地址。

拷贝和合并

state支持快速拷贝,使你可以尝试不同的可能性:

>>> proj = angr.Project('/bin/true')
>>> s = proj.factory.blank_state()
>>> s1 = s.copy()
>>> s2 = s.copy()

>>> s1.mem[0x1000].uint32_t = 0x41414141
>>> s2.mem[0x1000].uint32_t = 0x42424242

state还可以被合并到一起:

# merge将返回一个元组。第一个元素是合并的状态
# 第二个元素是描述状态标志的符号变量
# 第三个元素是一个布尔值,描述是否进行了合并
>>> (s_merged, m, anything_merged) = s1.merge(s2)

# 现在这个表达式可以解析为“AAAA”*或“BBBB”
>>> aaaa_or_bbbb = s_merged.mem[0x1000].uint32_t

模拟管理器(Simulation Managers)

SM(Simulation Managers)是angr中最重要的控制接口,它使你能够同时控制一组状态(state)的符号执行,应用搜索策略来探索程序的状态空间。现在我们就来学习如何使用它。

SM使你可以顺畅地控制多个状态。在SM中,状态都被组织在“stash”里,你可以对它们做执行一步、过滤、合并以及随意移动等操作。比如说,这允许你使两个不同的“stash”中的状态以不同的速度执行,然后再将它们合并。对大多数操作而言,默认的stash是activestash,当你初始化一个新的SM时,你的状态就放在那里面。

单步执行

SM的最基本能力就是让一个stash中的所有状态都执行一个基本块。使用.step()可以做到这一点:

>>> import angr
>>> proj = angr.Project('examples/fauxware/fauxware', auto_load_libs=False)
>>> state = proj.factory.entry_state()
>>> simgr = proj.factory.simgr(state)
>>> simgr.active
[<SimState @ 0x400580>]

>>> simgr.step()
>>> simgr.active
[<SimState @ 0x400540>]

当然,stash模式的真正强大之处在于,当一个状态执行到一个符号分支条件时,这个分支后的两个继承状态都会出现在stash中,而且你可以使它们同步地单步执行,如果你想仔细控制程序执行而且你只想执行到没有状态可以继续执行,你可以直接使用.run()方法:

# 单步执行直到第一个符号分支
>>> while len(simgr.active) == 1:
...    simgr.step()

>>> simgr
<SimulationManager with 2 active>
>>> simgr.active
[<SimState @ 0x400692>, <SimState @ 0x400699>]

# 单步执行,直到一切结束
>>> simgr.run()
>>> simgr
<SimulationManager with 3 deadended>

现在我们得到了3个“deadend”状态!如果一个状态在执行后不能够产生任何后继状态,例如它执行了一个exit的系统调用,那么这个状态就会被放入deadendstash。

Stash 管理

让我们看看如何使用Stash。

使用.move()将状态在不同stash间移动,这个方法接收参数from_stashto_stashfilter_func(这个参数是可选的,默认将stash中的所有状态都移动)。例如,我们可以将输出带有特定字符串的状态都移动到指定的stash中:

>>> simgr.move(from_stash='deadended', to_stash='authenticated', filter_func=lambda s: b'Welcome' in s.posix.dumps(1))
>>> simgr
<SimulationManager with 2 authenticated, 1 deadended>

我们可以通过要求将状态移入某个新的stash的方法来创建一个新的stash。在这个stash中的所有状态的标准输出中都有一个“Welcome”字符串,在目前这是一个很好的状态分类指标。

每一个stash都是一个列表,你可以通过索引或者迭代器的方式访问到每一个独立的状态,但是也有一些替代的方法来访问每个状态。如果你用“one_stash名”的方式访问stash,那么你会获得这个stash中的第一个状态;如果你用“mp_stash名”的方式访问stash,那么你将会得到mulpyplexed版本的stash:

>>> for s in simgr.deadended + simgr.authenticated:
...     print(hex(s.addr))
0x1000030
0x1000078
0x1000078

>>> simgr.one_deadended
<SimState @ 0x1000030>
>>> simgr.mp_authenticated
MP([<SimState @ 0x1000078>, <SimState @ 0x1000078>])
>>> simgr.mp_authenticated.posix.dumps(0)
MP(['\x00\x00\x00\x00\x00\x00\x00\x00\x00SOSNEAKY\x00',
    '\x00\x00\x00\x00\x00\x00\x00\x00\x00S\x80\x80\x80\x80@\x80@\x00'])

当然,steprun,以及任何其它的可以在一个stash的路径上操作的方法,都可以指定一个stash参数,指定对哪个stash做操作。

SM还提供了很多有趣的工具来帮助你管理你的stash。我们现在不会深入每一个工具的使用,但是你可以查看API文档。(文档链接未完成)

stash类型

你可以使用任何你喜欢的stash,但是有一些被用于分类特殊状态的stash,它们是:

Stash描述
active这个stash中的状态会被默认执行,除非执行前指定了特定要执行的stash
deadended当一个状态因为某些原因不能继续执行时,它就进入"deadend"stash。这些原因包括没有合法指令可以执行、所有子状态(的约束条件)都不可满足、或者一个非法的PC指针。
pruned当使用LAZY_SOLVES时,不到万不得已时不会检测state是否可满足。当在LAZY_SOLVES模式下一个状态被发现不可满足时,这个状态的父状态就会被检查,以找出状态历史中最开始的那个不可满足的状态,这个状态的所有子孙状态都是不可满足的(因为一个不可满足的状态不会变为可满足的状态),因此这些状态都需要被剪切掉,并且放入“pruned”stash中。
unconstrained如果save_unconstrained选项被提供给SM的构造函数,那么被认为"不受约束"的状态(比如指令指针被用户输入或者其他符号来源控制的状态)就被放在这个stash中。
unsat如果save_unsat选项被提供给SM的构造函数,那么被认定不可满足的状态(比如某个状态有两个互相矛盾的约束条件)会被放在这里。

There is another list of states that is not a stash: errored. If, during execution, an error is raised, then the state will be wrapped in an ErrorRecord object, which contains the state and the error it raised, and then the record will be inserted into errored. You can get at the state as it was at the beginning of the execution tick that caused the error with record.state, you can see the error that was raised with record.error, and you can launch a debug shell at the site of the error with record.debug(). This is an invaluable debugging tool!

简单探索(Simple Exploration)

符号执行最普遍的操作是找到能够到达某个地址的状态,同时丢弃其他不能到达这个地址的状态。SM为使用这种执行模式提供了捷径:.explore()方法。

当使用find参数启动.explore()方法时,程序将会一直执行,直到发现了一个和find参数指定的条件相匹配的状态。find参数的内容可以是想要执行到的某个地址、或者想要执行到的地址列表、或者一个获取state作为参数并判断这个state是否满足某些条件的函数。当activestash中的任意状态和find中的条件匹配的时候,它们就会被放到foundstash中,执行随即停止。之后你可以探索找到的状态,或者决定丢弃它,转而探索其它状态。你还可以按照和find相同的格式设置另一个参数——avoid。当一个状态和avoid中的条件匹配时,它就会被放进avoidedstash中,之后继续执行。最后,num_find参数指定函数返回前需要找到的符合条件的状态的个数,这个参数默认是1。当然,如果activestash中已经没有状态可以执行,那么不论有没有找到你指定的状态个数,都会停止执行。

让我们来简单看一个crackme的例子:

首先我们加载这个二进制文件:

>>> proj = angr.Project('examples/CSCI-4968-MBE/challenges/crackme0x00a/crackme0x00a')

然后我们创建一个SM:

>>> simgr = proj.factory.simgr()

现在我们符号执行直到我们找到一个符合我们要求的state(比如标准输出中输出“Congrats”):

>>> simgr.explore(find=lambda s: b"Congrats" in s.posix.dumps(1))
<SimulationManager with 1 active, 1 found>

现在我们就可以从这个状态中获得flag啦~:

>>> s = simgr.found[0]
>>> print(s.posix.dumps(1))
Enter password: Congrats!

>>> flag = s.posix.dumps(0)
>>> print(flag)
g00dJ0B!

探索技术(Exploration Techniques)

angr封装了几个功能使你能够自定义SM的行为,这些功能被称为"探索技术"。为什么你会需要探索技术呢?一个典型的例子是,你想修改程序状态空间的探索模式——默认是“每个state只执行一次”的广度优先搜索策略。但如果使用探索技术,你可以实现诸如深度优先搜索。然而这些探索技术能做的事(instrumentation power of these techniques)远比这些灵活——你可以完全改变angr单步执行的行为。在之后的章节里,我们会介绍如何编写你自己的探索技术。

调用simgr.use_technique(tech)使用特定的探索技术,其中tech是ExplorationTechnique的子类的实例。angr內建的探索技术可以在angr.exploration_techniques中找到.

这里是一些內建探索技术的简介:

  • DFS:深度优先搜索。每次只保持一个状态是active的,并把其余状态都放在deferredstash中,直到当前active状态达到deadenderrors
  • Explorer:这个技术实现了.explore()的功能,允许你寻找或避开某些指定的地址。
  • LengthLimiter: 设置每个状态能够执行的路径长度的上限。
  • LoopSeer:用一个循环次数的近似值来丢弃那些似乎会多次执行某个循环的状态,并把它们放入spinningstash中;当没有其它可执行的状态时,再把它们拉出来继续执行。
  • ManualMergepoint: 将程序中的某个地址作为合并点,当某个状态到达那个地址时,会被短暂地存储,之后,在指定时延内到达同一地址的状态都会被合并到这个状态。
  • MemoryWatcher: 监视在simgr步骤之间系统上有多少内存可用,如果内存不足就停止探索。
  • Oppologist:这个“operation apologist”是一个特别有趣的技术——它被开启后,当angr执行到了一个不被支持的指令(比如一个奇怪的或者外部的浮点SIMD操作)时,它将会具体化这条指令的所有输入,并且使用独角兽引擎(unicorn engine)来执行这条指令,这使得程序执行得以延续。
  • Spiller: 当activestash中有过多状态时,这项技术将会把其中的一些dump到硬盘上来保证低内存消耗。
  • Threading: 对程序的单步执行加入线程级并发支持。由于python全局解释器的锁定,这并没有什么帮助,但是如果你有一个程序在分析时花费了大量时间在angr的本地代码依赖上,这个技术可能会起到加速效果。
  • Tracer: 一种使程序按照从其他资源获得动态追踪记录执行的探索技术(类似于复现某次执行过程?)。这个动态跟踪仓库
    里有一些工具能够产生这些路径。
  • Veritesting::是一篇CMU论文中提及的自动化定义合并点的方法的实现。这很有用,你可以通过在SM的构造函数中传入veritesting=True来启用它。注意这个技术通常不能和其余技术很好地兼容,因为它使用了入侵的方式来实现静态符号执行。

执行引擎(Execution Engines)

当你用angr进行一次单步执行时,必须有一个东西来切实地将程序执行一步(即一个基本块,下面简称执行一个基本块为step)。angr使用一系列引擎(SimEngine的子类)来模拟被执行的代码对输入状态产生的影响。angr的执行内核将按序尝试列表中可用的引擎,并取出第一个能够处理当前step的引擎。下面是按序列出的默认引擎列表:

  • failure engine:当前一次step将我们带到一个不可继续执行的状态时,故障引擎启动。
  • syscall engine:当前一次step以一个系统调用结束时启动。
  • hook engine:当前地址被hook时启动。
  • unicorn engine:当UNICORN选项被开启并且输入状态中没有符号化的数据时启动。
  • VEX engine:作为最终的回调函数被调用。

SimSuccessors

实际上按顺序尝试上述列表中的执行引擎的代码位于project.factory.successors(state, **kwargs)中,这个方法会将接收到的参数传给每一个引擎。它是state.step()simulation_manager.step()的核心。它返回一个我们之前已经简要讨论过的SimSuccessor对象。SimSuccessor的目的是对产生的后继状态进行一个简单的分类,并将这些状态分别存储在不同的属性(列表类型)中,这些属性是:

属性条件指令指针描述
successorsTrue(可以是约束为True的符号表达式;译者注:按我理解就是状态的约束条件可满足)可以是带符号的指令,但是解的个数必须小于或等于256个;详见unconstrained_successors由引擎执行某个状态后产生的普通的、可满足的状态。它的指令指针可以是符号化的(例如,以用户输入为判断条件的跳转指令),因此这个列表中存储的状态可能实际产生多个后继状态
unsat_successorsFalse(可以是约束为False的符号表达式;译者注:即符号约束条件不可满足)可以是带符号的指令不可满足的后继状态。这些状态的约束条件不可能被满足(例如:不可能执行的跳转,或者必须被默认执行的跳转。)
flat_successorsTrue(可以是约束为True的符号表达式)具体指令(不带符号)正如之前强调的,在successors列表中的状态中的指令指针可以是带符号的,这就带来一个问题:在执行一次step时(比如在SimEngineVEX.process中向前执行一步),我们假设一个state只能够代表代码中单独一段代码的执行结果,但是如果前一个状态是带符号的,那么执行结果应该如何表示呢?为了解决这个问题,当在successors列表中遇到一个带有符号化指令指针的状态时,angr会计算出所有可能的符号状态的具体值(最多256种可能,如果超过这个限制,将会被放入其他属性的列表中),我们称这个计算过程为flattening。在flat_successors中的每个状态都有着不带符号且互不相同的指令指针。例如,如果在successors列表中发现一个状态的指令指针指向X+5,且X的约束条件是X > 0x800000X < 0x800010,那么我们会将它“flatten”为16个不同的flat_sucessors状态,这16个状态包含的指令指针值从0x800006一直到0x800015
unconstrained_successorsTrue(可以是约束为True的符号表达式)符号化的指令指针(符号表达式的解的个数多于256个)在上面描述的flattening过程中,如果发现一个状态内的符号表达式组的可行解多于256个,我们就假设这个指令指针的值是一个不受约束的数据(例如,用户输入导致的栈溢出)这个假设通常是不合理的(译者注:wtf?!)。像这样的状态就会被放在unconstrained_successors列表中,而不是successors列表中
all_successors任何条件可以是符号化的successors+unsat_successors+unconstrained_successors

断点(Breakpoints)

和其他执行引擎一样,angr支持断点的设置。这就很酷了!一个断点可以这么下:

>>> import angr
>>> b = angr.Project('examples/fauxware/fauxware')

# 得到一个state
>>> s = b.factory.entry_state()

# 添加一个断点。在内存写之前,这个断点将被放到ipdb中。
>>> s.inspect.b('mem_write')

#另一方面,我们可以在一个内存写操作发生后马上触发一个断点。
# 允许自定义断点触发后的回调函数(需要ipdb库的支持)
>>> def debug_func(state):
...     print("State %s is about to do a memory write!")

>>> s.inspect.b('mem_write', when=angr.BP_AFTER, action=debug_func)

# 触发断点后进入ipython的交互界面。
>>> s.inspect.b('mem_write', when=angr.BP_AFTER, action=angr.BP_IPYTHON)

除了“内存写”断点外,还有许多其他种类的断点。这里是一个断点事件触发列表,你可以设置在这些事件发生前还是发生后触发断点:

事件类型事件含义
mem_read内存正在被读取
mem_write内存正在被写
reg_read寄存器正在被读
reg_write寄存器正在被写
tmp_read一个临时值(立即数?)正在被读
temp_write一个临时值正在被写
expr一个表达式正在被建立(比如一次数学计算的结果,或者IR中的常量(a constant in the IR))
statement一个IR statement正在被解释执行(translate)
instruction一个新的(本地native)指令正在被解释执行
lrsb一个新的基本块正在被解释执行
constraints一个新的约束正在被加入某个状态中
exit一个继承状态正由一次执行中产生
symbolic_variable一个新的符号变量正在被创建
call一个call指令正在被执行
address_concretization一个符号化的内存值正在被解析

对于上述不同的事件,可以应用不同的属性(来限制断点触发的条件):

事件类型属性名适用的时机属性含义
mem_readmem_read_addressBP_BEFOR 或 BP_AFTER正在被读取的内存的地址
mem_readmem_read_lengthBP_BEFOR 或 BP_AFTER读取的内存的长度
mem_readmem_read_exprBP_AFTER地址中的表达式
mem_writemem_write_addressBP_BEFOR 或 BP_AFTER正在被写入的内存地址
mem_writemem_write_lengthBP_BEFOR 或 BP_AFTER写入内存的长度
mem_writemem_write_exprBP_BEFOR 或 BP_AFTER写入内存的表达式
reg_readreg_read_offsetBP_BEFOR 或 BP_AFTER被读取的寄存器的偏移
reg_readreg_read_lengthBP_BEFOR 或 BP_AFTER被读取寄存器的值的长度
reg_readreg_read_exprBP_BEFOR 或 BP_AFTER被读取的寄存器中的表达式
reg_writereg_write_lengthBP_BEFOR 或 BP_AFTER被写入寄存器数据的长度
reg_writereg_write_exprBP_BEFOR 或 BP_AFTER被写入寄存器的表达式
tmp_readtmp_read_numBP_BEFOR 或 BP_AFTER被读入的临时值的长度
tmp_readtmp_read_exprBP_AFTER被读入的临时表达式
tmp_writetmp_write_numBP_BEFOR 或 BP_AFTER被写入临时值的数
tmp_writetmp_write_exprBP_AFTER被写入临时值的表达式
exprexprBP_AFTER表达式的值
statementstatementBP_AFTER 或BP_BEFORIR在其所在的基本块中的索引值(即断在当前基本块中的索引值)
instructioninstructionBP_BEFORE 或 BP_AFTER本地指令的地址
irsbaddressBP_BEFORE 或 BP_AFTER基本块地址
constraintsadded_constraintsBP_BEFORE 或 BP_AFTER被加入的约束的列表
callfunction_addressBP_BEFORE 或 BP_AFTER被调用的函数名
exitexit_targetBP_BEFORE 或 BP_AFTER代表SimExit的目标的表达式
exitexit_guardBP_BEFORE 或 BP_AFTER代表SimExit的限制的表达式
exitjumpkindBP_BEFORE 或 BP_AFTER代表SimExit的种类的表达式
symbolic_variablesymbolic_nameBP_BEFORE 或 BP_AFTER正在被创建的符号变量的名字。解析引擎可能改变这个名字(通过在后面添加唯一的ID和长度)。检查symbolic_expr来得到最终的符号表达式
symbolic_variablesymbolic_sizeBP_BEFORE 或 BP_AFTER正在被创建的符号变量的长度
symbolic_variablesymbolic_exprBP_AFTER代表新的符号变量的符号表达式
address_concretizationaddress_concretization_strategyBP_BEFORE 或 BP_AFTER被用于解析地址的SimConcretizationStrategy。断点处理函数可以改变将要被应用于解析当前地址的策略。如果你的断点处理函数被置为None,这个策略就会被忽略
address_concretizationaddress_concretization_actionBP_BEFORE 或 BP_AFTER用于记录内存操作的SimAction对象
address_concretizationaddress_concretization_memoryBP_BEFORE 或 BP_AFTER被操作的SimMemory对象
address_concretizationaddress_concretization_exprBP_BEFORE 或 BP_AFTER代表正在被解析的地址的AST。断点处理函数可以改变这个来影响正在被解析的地址
address_concretizationaddress_concretization_add_constraintsBP_BEFORE 或 BP_AFTER约束是否应该别加入到这次读取中
address_concretizationaddress_concretization_resultBP_AFTER被解析的地址列表(整型数)。断点处理函数可以覆盖这个来产生不同的解析结果。

你可以在合适的断点回调函数中通过通过state.inspect来访问这些属性,你甚至可以改变这些值来影响这些属性的后续使用!

>>> def track_reads(state):
...     print('Read', state.inspect.mem_read_expr, 'from', state.inspect.mem_read_address)
...
>>> s.inspect.b('mem_read', when=angr.BP_AFTER, action=track_reads)

另外,这些属性都可以作为inspect.b的关键字参数来使用,这可以使得断点更加准确

# 将会在程序可能(考虑到符号化的地址)往0x1000地址处写之前断下
>>> s.inspect.b('mem_write', mem_write_address=0x1000)

# 将会在程序只能往内存0x1000处写数据之前断下
>>> s.inspect.b('mem_write', mem_write_address=0x1000, mem_write_address_unique=True)

# 将会在指令地址0x8000执行后生效,但是只有0x1000是从内存中读出的表达式的可行解
>>> s.inspect.b('instruction', when=angr.BP_AFTER, instruction=0x8000, mem_read_expr=0x1000)

实际上,我们甚至可以指定一个函数作为条件:

# 展示如何用函数表示复杂条件,用这种方式几乎可以做任何事! 例子中将会保证断下时RAX的值是0x41414141 并且
# 从地址0x8004开始的基本块在这个state的执行历史中
>>> def cond(state):
...     return state.eval(state.regs.rax, cast_to=str) == 'AAAA' and 0x8004 in state.inspect.backtrace

>>> s.inspect.b('mem_write', condition=cond)

state.eval 或为state.solver.eval

注意内存读取mem_read断点

mem_read断点在执行程序或二进制分析读取内存时被触发。如果您在mem_read上使用断点,并且还使用state.mem从内存地址加载数据,那么您应该知道,在技术上读取内存时,断点将被触发。

因此,如果希望从内存加载数据而不触发已经设置的任何mem_read断点,那么可以使用state.memory.load和关键字参数disable_actions=Trueinspect=False

对于state.find也是如此,您可以使用相同的关键字参数来防止触发mem_read断点。

分析(Analyses)

angr的目标是使得对二进制程序的分析更加简单。学习至此,angr允许你将分析代码打包为一个通用的格式,以便于应用到任何project的分析中。我们将会在之后的部分中教你如何写你自己的分析代码,但是基本的思想是所有的分析都在project.analyses中(例如,project.analyses.CFGFast())且可以被作为一个方法来调用,返回一个分析结果的实例。

內建分析函数

名字描述
CFGFast构建一个快速的程序控制流图
CFGEmulated就构建一个精确的程序控制流图
VFG对程序中的每个方法都执行VSA,创建一个值流图(Value Flow Graph)并检测栈变量
DFG为每个在CFG中出现的基本块构建数据流图
BackwardSlice有目的地为程序计算一个Backward Slice
Identifier定义CGC二进制文件中的通用库函数
更多!angr有许多的分析函数,大多数都可以使用!如果你想要知道如何使用某个分析函数,你可以在github上提交一个issue来获取相应的文档

容错(Resilience)

分析函数可以有较好的容错性,并且基本上能够catch和记录所有错误。这些错误被记录为analysiserrors或者named_errors属性,具体被记录为哪种属性取决于这个错误是如何被抓出的。然而,你可能想要以“fail fast”模式运行一次分析,从而忽略那些错误。为了达到这个目的,你可以给analysis的构造函数传入一个fail_fast=True的参数。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Forgo7ten

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值