angr学习笔记

大爹说angr是用来自动解题的,上手学习一个,顺便整几个例题试试水

参考了这位爷这位爷的文章🙇‍♂️🙇‍♂️🙇‍♂️

安装

首先是环境依赖

sudo apt-get install python-dev libffi-dev build-essential virtualenvwrapper

然后是virtualenvwrapper初始化

上述安装过程virtualenvwrapper默认在/usr/share/virtualenvwrapper,如果没找到的话建议本地搜一下文件名

我们在~/.bashrc最后加入

export WORKON_HOME=$HOME/Python-workhome
source /usr/share/virtualenvwrapper/virtualenvwrapper.sh

(如果没找到.bashrc,我强烈建议你检查一下自己有没有勾上Show Hidden Files😅)
HOME/Python-workhome就是准备放置虚拟环境的地址,source /usr/local/bin/virtualenvwrapper.sh启动virtualenvwrapper.sh脚本

然后source ~/.bashrc

就可以mkvirtualenv --python=$(which python3) angr && pip install angr来安装了

这个安装过程可能会持续很久,建议切出去打几把游戏(bushi)

我雀魂打了1个半庄才安装好

这样安装好后我们有了一个叫angr的虚拟环境(virtualenvwrapper干的),angr就在该虚拟环境下运行了

virtualenvwrapper的操作命令有
workon: 列出已有环境(我们只创建了叫angr的环境)
workon angr: 切换到环境angr
deactivate: 退出环境
rmvirtualenv: 删除环境

简单的例子

angr是进行符号执行算法的一种玩意,可以用来全自动进行逆向工程。

我们来理解一下符号执行算法

比如这个程序

int main()
{
	scanf("%d", &input);
	if (input - 100 >= 0) printf("Success!");
	else puts("Failed.");
}

对于这个程序,我们想要达到Success!的话,需要input满足条件input - 100 >= 0,那么,所需条件即\(input-100\geq0\)

再比如下面的程序

int main()
{
	scanf("%d", &input);
	if (input - 100 >= 0)
	{
		if (input <= 200) printf("Success!");
		else printf("Failed.");
	}
	else puts("Failed.");
}

对于这个程序,我们想要达到Success!的话,需要input满足以下条件

\[\left\{ \begin{array}{} input-100\geq0\\ input\leq200 \end{array} \right. \]

解出这个不等式就可以知道input所需的条件了

那么,有没有一种可能,我把所有的if语句全部拿出来,形成一个不等式组,解出这个不等式,就能够得到我们想要的输出了。

angr就是为了实现这种方法而诞生的

例题实战 - angr_ctf

例题全部来自:https://github.com/jakespringer/angr_ctf

dist文件夹内是已经编译好的二进制文件以及对应题目的解题思路和注释

挑几道试试手吧

angr_explore

explore()是angr中最简单粗暴也是最常用的函数

00_angr_find

直接放一个exp,写点注释吧

import angr
import sys

path = '/home/iplayforsg/Desktop/Workspace/00_angr_find' # 被执行文件的路径
project = angr.Project(path) # 以被执行文件创建angr项目

init_state = project.factory.entry_state() # entry_state()可以创建一个默认的初始状态,告诉angr该从哪开始

simulation = project.factory.simgr(init_state) # 以该初始状态创建一个模拟管理器,这个管理器还有很多工具可以帮助搜索和执行二进制文件

aim_addr = 0x804867D # 希望得到的输出的VA

simulation.explore(find = aim_addr) # 开始执行,直到达到上述VA或者探索完所有可能的路径,simulation.explore()会建立一个名为simulation.found的状态列表
                                    # 该状态列表用来表示是否找到了期望的输出,若找到则为true,反之为false

if simulation.found: # 检查是否得到了解决方案
    solution_state = simulation.found[0]
    print(solution_state.posix.dumps(sys.stdin.fileno())) # 输出angr执行到该状态时的输入,也就是我们需要的flag

else: # 没有得到解决方案
    print('Failed.')

拿到的flag是JXWVXRKX

我们来试试

我超,这angr好几把神奇

01_angr_avoid

这个题的主函数特别庞大,我们根本没法反编译,那我们考虑直接查找字符串找到出口

那首先必须should_succeed为1才算成功

我们看一下should_succeed的交叉引用

那这个avoid_me()一定不能被执行,这就可以用到angr的explore的第二个参数来添加约束了

import angr
import sys

proj = angr.Project('/home/iplayforsg/Desktop/Workspace/01_angr_avoid')

state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)

aim_addr = 0x80485E5
void_addr = 0x80485A8

simgr.explore(find = aim_addr, avoid = void_addr) # avoid是explore()的第二个参数,也就是避免到达这个地址

if simgr.found:
    sol_state = simgr.found[0]
    print(sol_state.posix.dumps(sys.stdin.fileno()))

else:
    print('Failed.')

跑出来是HUJOZMYS,试一下

大成功

02_angr_find_condition

看伪代码还没什么,但是一看左下角的graph overview

有点吓人,那我们只能推测是ida自动优化了一坨一坨功能相同的重复代码,才让伪代码显得如此简洁

目标是Good Job. 想要避开Try Again, 但是由于是ida优化了很多代码,这些优化的代码里面很可能也有Good Job/Try Again,所以我们不能仅仅使用伪代码中的输出的地址

这时就不能像00和01题那样使用单一地址判断了,那么现在我们需要知道一点:explore()的find参数除了地址外,还可以是携带SimState参数的函数

import angr

def aim_out(state): # 判断当前状态的输出流中是否包含b'Good Job.',如果包含则表示到达目的地址,可以打印对应输出
    return b'Good Job.' in state.posix.dumps(1)

def avoid_out(state):
    return b'Try again.' in state.posix.dumps(1)

proj = angr.Project('/home/iplayforsg/Desktop/Workspace/01_angr_avoid')

state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)

simgr.explore(find = aim_out, avoid = avoid_out) # 这就是函数传参!

if simgr.found:
    sol_state = simgr.found[0]
    print(sol_state.posix.dumps(0))

else:
    print('Failed.')

这次跑的就有点久,但是还是跑出来咯

有些时候就算很确定只有一处输出,如果直接用地址跑不出来的话,建议用用输出流,万一就出了呢

angr_symbolic

angr在默认情况下只会符号化从输入流中读取的数据,实际情况往往需要我们符号化其他数据,如寄存器、某块内存甚至是某个文件。如何手动符号化并且利用angr内置的约束求解器对符号值进行约束求解也是一个大问题。

03_angr_symbolic_registers

这个题的伪代码也很简单,但是complex_func()里面看着很难受

3个都是这样的,那我肯定不会去硬逆,考虑angr直接爆破

只用explore()也是可以跑出来的

import angr

def aim_out(state):
    return b'Good Job.' in state.posix.dumps(1)

def avoid_out(state):
    return b'Try Again.' in state.posix.dumps(1)

proj = angr.Project('/home/iplayforsg/Desktop/Workspace/03_angr_symbolic_registers')

state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)

simgr.explore(find = aim_out, avoid = avoid_out)

if simgr.found:
    sol = simgr.found[0]
    print(sol.posix.dumps(0))

else:
    print('OOps!')

但这题目里既然带了个symbolic,我们还是试试手动符号化求解吧

从汇编入手,这里我们可以看到我们的输入被写进了eax, ebx, edx三个寄存器,那么我们将这三个寄存器符号化

# 定义三个位向量,即三个输入
input_0 = claripy.BVS('input_0', 32) # 32位寄存器
input_1 = claripy.BVS('input_1', 32)
input_2 = claripy.BVS('input_2', 32)

# 把三个输入分别赋给三个寄存器
state.regs.eax = input_0 # regs.eax访问的是eax寄存器
state.regs.ebx = input_1
state.regs.edx = input_2

看一下完整代码吧

import angr
import claripy

def aim_out(state):
    return b'Good Job.' in state.posix.dumps(1)

def avoid_out(state):
    return b'Try Again.' in state.posix.dumps(1)


proj = angr.Project('/home/iplayforsg/Desktop/Workspace/03_angr_symbolic_registers')

init_addr = 0x8048980 # call get_user_input的下一行地址,
state = proj.factory.blank_state(addr = init_addr) # 这个状态的效果相当于跳过输入,直接执行下一条指令,因为我们后面会把存储输入的寄存器符号化,所以可以不管输入流了
simgr = proj.factory.simgr(state)

# 定义三个位向量,即三个输入
input_0 = claripy.BVS('input_0', 32) # 32位寄存器
input_1 = claripy.BVS('input_1', 32)
input_2 = claripy.BVS('input_2', 32)

# 把三个输入分别赋给三个寄存器
state.regs.eax = input_0 # regs.eax访问的是eax寄存器
state.regs.ebx = input_1
state.regs.edx = input_2

simgr = proj.factory.simgr(state)
simgr.explore(find = aim_out, avoid = avoid_out)


if simgr.found:
    sol = simgr.found[0]
    flag_0 = sol.solver.eval(input_0)
    flag_1 = sol.solver.eval(input_1)
    flag_2 = sol.solver.eval(input_2)
    print('{:x} {:x} {:x}'.format(flag_0, flag_1, flag_2))
          
else:
    print('OOps!')

而且,这次的速度显然比上一个exp快得多了

值得注意的是,我们使用了blank_state()跳过了输入,直接从0x8048980这个地址开始符号执行。

.blank_state()会构造一个“空状态”,它的大多数数据都是未初始化的。当使用未初始化的的数据时,一个不受约束的符号值将会被返回

根据x86汇编常识,像eax,ebx,ecx这样的寄存器是上下文无关的,也就是说一个函数内不会引用在函数外部设置的eax,ebx或者ecx的值,而是在函数内部对寄存器重新初始化并使用,所以我们用blank_state获取的状态,即时初始的一些寄存器处于未初始化状态也是丝毫没有影响的,因为它们马上就会被初始化。

>>> state.regs.ecx
<BV32 reg_c_3_32{UNINITIALIZED}>

至于esp,angr会给他们一个默认的初值,使接下来的函数调用不会爆炸

>>> state.regs.esp
<BV32 0x7fff0000>

ebp的初始值仍然是未初始化的,但对我们后续的符号执行没有影响,不管它就行

>>> state.regs.ebp
<BV32 reg_1c_3_32{UNINITIALIZED}>

另外,我们还用到了符号位向量(bitvector),我们可以简单地将其理解为约束求解的自变量,即一个方程中的"x"

在这里我们使用claripy通过BVS()方法生成三个位向量。此方法有两个参数:第一个是angr用来引用位向量的名称,第二个是位向量本身的大小(以位为单位)。

04_angr_symbolic_stack

这个题目想让我们学栈空间内的数据的符号化,那就来试试吧

伪代码逻辑十分简单,那么看汇编吧

scanf后一条指令地址是0x8048694,可以将其传参进blank_state()来初始化

上一个题末尾说过,blank_state()不会初始化ebp

这个题向栈内push了一堆的符号值,并且要通过ebp来索引这些符号值,比如[ebp + var_10], [ebp + var_C],这时我们需要让ebp有一个正确的初值

我们用blank_state()跳过了函数开头对栈的调整,因此我们还需要手动调整ebp的值

上一个题末尾还说过,blank_state()是会初始化esp的,所以我们只需要计算出ebp相对于esp的偏移量即可将ebp调整至正确的状态

sub esp, 18h
sub esp, 4
...
push eax
...
push eax
push offset aUU

那么此时ebp的偏移量为0x18 + 4 + 4 + 4 + 4 = 40

接下来,容易发现,我们的输入值是存储在[ebp + var_10]和[ebp + var_C]这两个位置的,那么我们需要对esp的值执行调整,使得接下来push进去的符号值恰好在这两个位置,当然,push完了得把esp调整回来(可以用ebp的偏移量)

import angr
import claripy

proj = angr.Project('/home/iplayforsg/Desktop/Workspace/04_angr_symbolic_stack')
state = proj.factory.blank_state(addr = 0x8048694)

state.regs.ebp = state.regs.esp + 40
state.regs.esp = state.regs.ebp - 0xC + 4
flag_0 = claripy.BVS('flag_0', 32)
flag_1 = claripy.BVS('flag_1', 32)
state.stack_push(flag_0)
state.stack_push(flag_1)
state.regs.esp = state.regs.ebp - 40

simgr = proj.factory.simgr(state)
simgr.explore(find = 0x80486E1, avoid = 0x80486CF)
if simgr.found:
    sol = simgr.found[0].solver
    flag0 = sol.eval(flag_0)
    flag1 = sol.eval(flag_1)
    print('flag_0: {}'.format(flag0))
    print('flag_1: {}'.format(flag1))
else:
    print('OOps!')

05_angr_symbolic_memory

这个题想让我们学符号化输入进内存的变量的方法

代码逻辑很简单,符号化全局变量user_input即可

import angr
import claripy

proj = angr.Project('/home/iplayforsg/Desktop/Workspace/05_angr_symbolic_memory')
state = proj.factory.blank_state(addr = 0x80485FE)

flag_0 = claripy.BVS('flag_0', 64) # 一个变量输入8个字符,一个字符8 bit,共计64 bit
flag_1 = claripy.BVS('flag_1', 64)
flag_2 = claripy.BVS('flag_2', 64)
flag_3 = claripy.BVS('flag_3', 64)

state.memory.store(0x0A1BA1C0, flag_0)  # 指向输入在内存中的地址
state.memory.store(0x0A1BA1C8, flag_1)
state.memory.store(0x0A1BA1D0, flag_2)
state.memory.store(0x0A1BA1D8, flag_3)

simgr = proj.factory.simgr(state)
simgr.explore(find = 0x804866A, avoid = 0x804865B)

if simgr.found:
    sol = simgr.found[0].solver
    flag0 = sol.eval(flag_0, cast_to = bytes)
    flag1 = sol.eval(flag_1, cast_to = bytes)
    flag2 = sol.eval(flag_2, cast_to = bytes)
    flag3 = sol.eval(flag_3, cast_to = bytes)
    print('flag_0: {}'.format(flag0.decode('utf-8')))
    print('flag_1: {}'.format(flag1.decode('utf-8')))
    print('flag_2: {}'.format(flag2.decode('utf-8')))
    print('flag_3: {}'.format(flag3.decode('utf-8')))

else:
    print('OOps!')

另外,如果我们想获取内存中的数据的具体值/符号值,可以这么写

>>> state.mem[0xA1BA1C0].uint64_t.resolved
<BV64 password0_0_64>

这里我们在使用eval时多了一个参数cast_to,该参数可以指定把结果映射到哪种数据类型。

顺便,我们引申一些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) 给出最大可行解

state.memory.store(0x0A1BA1C0, flag_0) 这几句句写成 state.mem[0xA1BA1C0].uint64_t = flag_0的形式也行,但是这样跑出来的数据输出是RNGHTXAN EWPTFSVJ CWHUAGML ULAPCDMX,这玩意每组flag的hex值刚好和正确的flag的hex值反了,推测是端序问题,我也不知道怎么解决捏,呜呜

06_angr_symbolic_dynamic_memory

看这题名字也该知道是想让我们搞定动态内存了

动态内存会引出一个问题:现在我们输入的地址不是固定的了

代码逻辑仍然十分简单,我们依然考虑跳过输入,来处理buffer0/buffer1

我们需要明白一点:angr(至少目前用到过的为止)并没有真正的运行过二进制文件,它仅仅是在模拟其运行状态,所以,我们没必要给他分配内存到堆中。

依据这一点,我们可以在堆栈中搞两个fake_addr,并保存到buffer0,buffer1,程序实际执行的时候就会把 malloc返回的地址保存到这里。最后我们把符号位向量保存到伪造的地址里。

import angr
import sys
import claripy

proj = angr.Project('/home/iplayforsg/Desktop/Workspace/06_angr_symbolic_dynamic_memory')
state = proj.factory.blank_state(addr = 0x8048696)

flag_0 = claripy.BVS('flag_0', 64) # 8位8字节的
flag_1 = claripy.BVS('flag_1', 64)

fake_heap_0 = 0xFFFFC93C # 其实可以直接在buffer的.bss段选定一块未使用过的区域
fake_heap_1 = 0xFFFFC94C
mmr_addr0 = 0xABCC8A4 # buffer0
mmr_addr1 = 0xABCC8AC # buffer1

state.memory.store(mmr_addr0, fake_heap_0, endness = proj.arch.memory_endness) # angr默认大端序,endness = project.arch.memory_endness把他调成小端序
state.memory.store(mmr_addr1, fake_heap_1, endness = proj.arch.memory_endness)

state.memory.store(fake_heap_0, flag_0)
state.memory.store(fake_heap_1, flag_1)

simgr = proj.factory.simgr(state)
simgr.explore(find = 0x8048756, avoid = 0x8048744)


if simgr.found:
    sol = simgr.found[0].solver
    flag0 = sol.eval(flag_0, cast_to = bytes)
    flag1 = sol.eval(flag_1, cast_to = bytes)
    print('flag_0: {}'.format(flag0))
    print('flag_1: {}'.format(flag1))
else:
    print('OOps!')

另外,我们引申一下memory.store()的一些定义(一般就是个.store(addr, data))

def store(self, addr, data, size=None, condition=None, add_constraints=None, endness=None, action=None, inspect=True, priv=None, disable_actions=False):
        """
        Stores content into memory.
        :param addr:        A claripy expression representing the address to store at. #内存地址
        :param data:        The data to store (claripy expression or something convertable to a claripy expression).#写入的数据
        :param size:        A claripy expression representing the size of the data to store. #大小
        ...

07_angr_symbolic_file

很显然,我们读取的数据会存入OJKSQYDP.txt,然后再从该文件中读取数据存入buff,并不需要我们自己新建文件。

我们需要使用angr模拟一个文件系统,其中该文件被我们自己的模拟文件所替代,然后将该文件进行符号化处理

好像没啥好多说的,看代码吧

import angr
import claripy

proj = angr.Project('/home/iplayforsg/Desktop/Workspace/07_angr_symbolic_file')
state = proj.factory.blank_state(addr = 0x80488D3)

flag_0 = claripy.BVS('flag_0', 64)
sim_file = angr.SimFile(name = 'OJKSQYDP.txt', content = flag_0, size = 0x40)
state.fs.insert('OJKSQYDP.txt', sim_file)

simgr = proj.factory.simgr(state)
simgr.explore(find = 0x80489AD, avoid = 0x8048996)

if simgr.found:
    sol = simgr.found[0].solver
    flag0 = sol.eval(flag_0, cast_to = bytes)
    print('flag_0: {}'.format(flag0))

else:
    print('OOps!')

angr_constraints

为什么angr这么强的东西没有在实际应用中产生很大的作用呢,除了难以建立约束关系以外,符号执行技术在复杂程序中最大的问题就是:路径爆炸问题。

这一节主要是来应对路径爆炸,路径爆炸的概念与数学中的指数爆炸概念类似,即某些情况下符号执行的路径/状态以指数级增长。

08_angr_constraints

很容易看出,check()函数内不是比对失败就立刻退出循环,而是一直循环到最后才进行比对。显然,如果直接调用explore()会导致\(2^{16}\) 个分支的出现,产生路径爆炸问题。

但实际上这个比较函数非常简单,仅仅进行了逐字符比对,这根本没有必要去让angr跑,我们可以手动添加这个约束,然后进行求解。

一个简单的约束是:在执行check()之前,我们的字符串是否为AUPDNNPROEZRJWKB(第一行的password和check函数的名字已经给足了提示)

import angr
import claripy

proj = angr.Project('/home/iplayforsg/Desktop/Workspace/08_angr_constraints')
state = proj.factory.blank_state(addr = 0x8048622)

flag = claripy.BVS('flag', 16 * 8) # len(flag) = 16, sizeof(char) = 8
state.memory.store(0x804A050, flag) # buffer
simgr = proj.factory.simgr(state)
simgr.explore(find = 0x8048673) # 这次只用找到check()执行的地方就行了,check()想要进行的比对我们直接添加约束即可


if simgr.found:
    sol = simgr.found[0]
    aim_vec = sol.memory.load(0x804A050, 16) # .load(addr, size)接口可读取内存数据
    sol.add_constraints(aim_vec == b'AUPDNNPROEZRJWKB') # .add_constraints()是一个简单的约束添加方式
    print(sol.solver.eval(flag, cast_to = bytes))
else:
    print('OOps!')

从这个题我们可以发现,即使是很简单的循环比较,也会让angr产生指数级的分支,最终导致angr进程killed,这也是一种抗符号执行的思路。

我们也发现了一种简单而有效的对抗路径爆炸的方法:避开会产生路径爆炸的函数,并用约束条件来代替该函数

angr_hooks

另外一种避免路径爆炸的方式是hook,形象地理解一下,hook就是把一段代码用"钩子"钩住,然后替换为我们自己的代码

钩子编程(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。

简单来说就是用我们自己设计的函数去取代被hook的函数

09_angr_hooks

这个题和上一个题思路没啥区别,但是上个题我们是通过添加约束条件来减少分支,这个题我们直接尝试用hook改写check()

对于hook,我们举一个简单的例子

>>> @project.hook(0x12345, length = 5)
... def set_rax(state):
...     state.regs.rax = 1

proj.hook(addr,hook)中,第一个参数即需要hook的函数地址,第二个参数即在完成hook操作后应该跳过多少字节,具体数字由hook的地址的指令长度确定。

在此题中,我们要hook的函数是check(),在hex窗口中我们可以看到机器指令长度为5个字节,即第二个参数的值应为5


import angr
import sys
import claripy

project = angr.Project('/home/iplayforsg/Desktop/Workspace/09_angr_hooks')
state = project.factory.entry_state()


@project.hook(0x80486B3, length = 5)
def my_check(state):

    buffer = state.memory.load(0x804A054, 16)
    aim_str = 'XYMKBKUHNIQYNQXE'

    state.regs.eax = claripy.If(buffer == aim_str, claripy.BVV(1, 32), claripy.BVV(0, 32))
    # 原来的check()中,eax寄存器作为返回值,成功则返回1,失败则返回0。寄存器是32位的,所以参数是32

simgr = project.factory.simgr(state)
simgr.explore(find = 0x8048768, avoid = 0x8048756)

if simgr.found:
    sol_state = simgr.found[0]
    print(sol_state.posix.dumps(0))
else:
    print('OOps!')

10_angr_simprocedures

这个题看反编译还挺简单的,但是在ida view中我们可以发现

实际上程序调用了特别多次check(),伪代码给他优化了才看起来如此简洁

那上个题我们是通过hook函数地址来实现的,这个题显然不能直接通过hook地址来实现

这里又要引入新东西了:SimProcedure(按字面意思理解就是“模拟程序”)

SimProcedure用Python编写的我们自己的函数代替了原来函数。 除了用Python编写之外,该函数的行为与用C编写的任何函数基本相同。参数self之后的任何参数都将被视为要替换的函数的参数, 参数将是符号位向量。

另外,Python可以以常用的Python方式返回,angr将以与原来函数相同的方式对待它。

这就是我们要hook的函数,不难发现第一个参数是待比对的字符串的首地址指针,第二个参数是字符串长度。基于此,我们来写自己的check()

class MyCheck(angr.SimProcedure): # 定义一个继承angr.SimProcedure的类,从而利用angr的SimProcedures

    def check(self, addr, length): # 这里和上一个题就是一样的了
        buffer = self.state.memory.load(addr, length)
        aim_str = 'ORSDDWXHZURJRBDH'
        return claripy.If(buffer == aim_str, claripy.BVV(1, 32), claripy.BVV(0, 32))

那拼一下就能拼出我们的exp了

import angr
import claripy

class MyCheck(angr.SimProcedure): # 定义一个继承angr.SimProcedure的类,从而利用angr的SimProcedures

    def run(self, addr, length): # 这里和上一个题就是一样的了
        buffer = self.state.memory.load(addr, length)
        aim_str = 'ORSDDWXHZURJRBDH'
        return claripy.If(buffer == aim_str, claripy.BVV(1, 32), claripy.BVV(0, 32))

def aim_out(state):
    return b'Good Job.' in state.posix.dumps(1)

def avoid_out(state):
    return b'Try again.' in state.posix.dumps(1)

proj = angr.Project('/home/iplayforsg/Desktop/Workspace/10_angr_simprocedures')
proj.hook_symbol(symbol_name = 'check_equals_ORSDDWXHZURJRBDH', simproc = MyCheck())

state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)

simgr.explore(find = aim_out, avoid = avoid_out)

if simgr.found:
    print(simgr.found[0].posix.dumps(0))
else:
    print('OOps!')

11_angr_sim_scanf

angr_ctf给出的这个题出现的原因是

# This time, the solution involves simply replacing scanf with our own version,
# since Angr does not support requesting multiple parameters with scanf.

实际上现在angr已经可以支持多参数的scanf了,所以这个题可以直接explore()

但是来都来了,我们顺便学习一下如何用hook重写库函数scanf

import angr
import claripy

class MyScanf(angr.SimProcedure): # scanf要向内存写入数据,那么用.store()把位向量写入字符串的内存区

    def run(self, str, addr0, addr1):

        scanf0 = claripy.BVS('scanf0', 32)
        scanf1 = claripy.BVS('scanf1', 32)

        self.state.memory.store(addr0, scanf0, endness = proj.arch.memory_endness) # 改变端序
        self.state.memory.store(addr1, scanf1, endness = proj.arch.memory_endness)

        self.state.globals['sol'] = (scanf0, scanf1)

def aim_out(state):
    return b'Good Job.' in state.posix.dumps(1)

def avoid_out(state):
    return b'Try again.' in state.posix.dumps(1)


proj = angr.Project("/home/iplayforsg/Desktop/Workspace/11_angr_sim_scanf")
state = proj.factory.entry_state()

proj.hook_symbol(symbol_name = '__isoc99_scanf', simproc = MyScanf())

simgr = proj.factory.simgr(state)

simgr.explore(find = aim_out, avoid = avoid_out)

if simgr.found:
    sol = simgr.found[0].globals['sol']
    scanf0_sol = simgr.found[0].solver.eval(sol[0])
    scanf1_sol = simgr.found[0].solver.eval(sol[1])
    print(scanf0_sol)
    print(scanf1_sol)
        
else:
    print('OOps!')

程序内唯一的新东西是self.state.globals['sol'] = (scanf0, scanf1),在python的语法中,此处的scanf0和scanf1是作为MyScanf()的局部变量,为了angr的求解器调用该位向量,我们需要将其转化为全局变量,故我们调用带有全局状态的globals插件来保存对我们的符号值的引用。

globals插件允许使用列表,元组或多个键的字典来存储多个位向量

angr_veritesting

除了手动约束和hook以外,还有一种防止路径爆炸的方法是veritesting(路径归并)

简单来说就是 Veritesting结合了静态符号执行与动态符号执行,减少了路径爆炸的影响,在angr里我们只要在构造simgr时启用Veritesting了就行

大爹的blog中已经给出了相关论文Enhancing Symbolic Execution with Veritesting,但是好像链接挂了,有兴趣的xdm可以自己去找找(

12_angr_veritesting

很容易发现for循环内路径爆炸了

但是这次我们直接让veritesting为true就行了,体验一下这玩意的强大

import angr

def aim_out(state):
    return b'Good Job.' in state.posix.dumps(1)

def avoid_out(state):
    return b'Try again.' in state.posix.dumps(1)

proj = angr.Project("/home/iplayforsg/Desktop/Workspace/12_angr_veritesting")
state = proj.factory.blank_state()

simgr = proj.factory.simgr(state, veritesting = True)

simgr.explore(find = aim_out, avoid = avoid_out)

if simgr.found:
    print(simgr.found[0].posix.dumps(0))

else:
    print("OOps!")

跑的很快,大概20s就能出

另外,根据官方的说法,Versitesting通常与其他exploration techniques不兼容

Note that it frequenly doesn't play nice with other techniques due to the invasive way it implements static symbolic execution.

angr_library

13_angr_static_binary

提出一个概念:静态编译

我们知道,一般的程序编译时都是将其需要的模块编译成动态链接库,在启动程序时这些模块不会被加载,而是在运行时用到哪个就调用哪个。

静态编译则是在编译程序时将所有需要的模块全部编译进程序中,当启动这个程序时,所有模块都被直接加载进来。

在ida里面就看得出来这玩意是静态编译了

我们需要了解一个angr的特性:angr通常会自动用SimProcedure来代替标准库函数,以此提高程序效率。

但是在本题中,我们所调用的库函数都因为静态编译而变成了静态函数,angr不能实现自动替换。那么,我们考虑手动hook所有调用的库函数,SimProcedure中已经包含了这些库函数,我们只需找到调用的库函数的地址(或者更简单地,直接用其函数名进行hook),并将其hook掉即可。

import angr

def aim_out(state):
    return b'Good Job.' in state.posix.dumps(1)

def avoid_out(state):
    return b'Try again.' in state.posix.dumps(1)

proj = angr.Project("/home/iplayforsg/Desktop/Workspace/13_angr_static_binary")

proj.hook_symbol("printf", angr.SIM_PROCEDURES['libc']['printf']())
# 也可以写成 proj.hook(0x804ED40, angr.SIM_PROCEDURES['libc']['printf']()),这种就是用地址进行hook,但是在函数调用较多的情况下还是直接用函数名比较方便
proj.hook_symbol("__isoc99_scanf", angr.SIM_PROCEDURES['libc']['scanf']())
proj.hook_symbol("puts", angr.SIM_PROCEDURES['libc']['puts']())

proj.hook_symbol("__libc_start_main", angr.SIM_PROCEDURES['glibc']['__libc_start_main']()) # 别忘了这玩意


state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)
simgr.explore(find = aim_out, avoid = avoid_out)

if simgr.found:
    print(simgr.found[0].posix.dumps(0))

else:
    print("OOps!")

14_angr_shared_library

这个题多了个文件叫lib14_angr_shared_library.so,是动态库文件

这个validate()就是从库文件内导入的函数了,再用ida分析库文件

那还是写exp加注释吧

import angr
import claripy


# 本题我们考虑直接对动态链接库进行求解,但库文件是地址无关的可执行文件(position-independent executable,PIE),需要装载地址才能执行,所以我们得指定一个基地址
path = "/home/iplayforsg/Desktop/Workspace/lib14_angr_shared_library.so"
base = 0x400000 # angr符号执行的基址默认为0x400000
proj = angr.Project(path, load_options = {
    'main_opts':{
        'base_addr': base
    }
})
# 我们将要调用call_state来构造一个已经准备好执行的validate()的状态,故我们需要设定好需要传递的参数

# 通过BVV(value,size)创建一个缓冲区buf的向量作为参数char *s1
buf_p = claripy.BVV(0x3000000, 4 * 8) # sizeof int
# ida里面可以看到validate()的起始地址是.text:000006D7,那么偏移量就是0x6D7了
validate_addr = base + 0x6D7

state = proj.factory.call_state(validate_addr, buf_p, claripy.BVV(8, 32)) # 利用BVV(value, size)传入参数int a2,需要比较的字符串的正确长度是8,相应的寄存器是32位的

# 用BVS(name, size)创建一个符号位向量,作为符号化的传入字符串传入之前设定的缓冲区地址中
flag = claripy.BVS('flag', 8 * 8)
state.memory.store(buf_p, flag)

simgr = proj.factory.simgr(state)
# .text:00000783     retn
aim_addr = base + 0x783
simgr.explore(find = aim_addr)

if simgr.found:
    # 注意我们前面explore()的地址是retn的地址,此时我们想利用最后return的判断条件为真来达到约束效果,值是放在eax寄存器的
    sol = simgr.found[0]
    sol.solver.add(sol.regs.eax == 1)
    print(sol.solver.eval(flag, cast_to = bytes))
else:
   print("OOps!")

其中pre-binary选项是一个新东西。

如果你想要对一个特定的二进制对象设置一些选项,CLE也能满足你的需求在加载二进制文件时可以设置特定的参数,使用 main_optslib_opts 参数进行设置。

  • backend — 指定 backend
  • base_addr — 指定基址
  • entry_point — 指定入口点
  • arch — 指定架构

参数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('example/233/233', main_opts={'backend': 'blob', 'arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})
<Project example/233/233>

angr_overflow

没学过pwn捏,先看到这里吧,后面接触了pwn再来学学

马上抓个pwn手来学angr

posted @ 2022-01-17 20:09  iPlayForSG  阅读(539)  评论(1编辑  收藏  举报