angr使用笔记

前言

         一般来说,二进制文件有两种分析方法:静态分析、动态分析。

         静态分析是指:直接阅读二进制编码。静态分析的特点是:覆盖全面。但是,静态分析有它的缺点。如前面所说,二进制文件本身并不好理解,甚至像天书一样。因此,静态分析想得到精确的结果,则需要进行长时间的分析,使分析时间变长。

         态分析不阅读二进制代码,而是直接把二进制文件扔到实际环境下执行,但是,由于设备的输入信息可以是任意的,因此动态分析不可能做到面面俱到。也就是说,分析结果虽然非常准确,但不能全面覆盖。

         打CTF的Re题的时候,程序一般的流程就是接收输入,然后执行一个验证过程,如果程序通过认证,跳出一个flag,否则跳出一个error。而我们一般的思路是逆向这个认证过程,这就考察一个人的逆向思维了。

         不妨换一个思维,不找中间的认证过程,而是直接寻找认证通过状态

符号执行

         符号执行(Symbolic Execution)是一种程序分析技术。其可以通过分析程序来得到让特定代码区域执行的输入。使用符号执行分析一个程序时,该程序会使用符号值作为输入,而非一般执行程序时使用的具体值。在达到目标代码时,分析器可以得到相应的路径约束,然后通过约束求解器来得到可以触发目标代码的具体值。

什么是Angr

         angr是一个用于分析二进制文件的python框架。它专注于静态和符号分析,使其适用于各种任务。他寄生于2层环境中,第一层为angr环境,是一个利用virtualenvwrapper软件模拟出来的python环境,用于解决环境的不兼容问题,第二层是真实的python环境,也就是说我们需要执行python后才能使用import angr导入模块。

一般属性

         首先,我们需要知道文件的基本属性,例如架构,文件名,入口点之类的。

1
2
3
4
5
6
7
>>> proj = angr.Project('./test1_nopie',auto_load_libs=False)
>>> proj.arch
<Arch AMD64 (LE)>
>>> proj.filename
'./test1_nopie'
>>> proj.entry
4195584

装载器

         从二进制文件加载到在虚拟地址空间中非常复杂!我们有一个叫CLE的模块来处理这个问题。称为加载程序,位于项目的.loader属性。

1
2
3
4
5
6
>>> proj.loader
<Loaded test1_nopie, maps [0x400000:0x3008000]>
>>> proj.loader.min_addr
4194304
>>> proj.loader.max_addr
50364416

工厂 {The factory}

         angr里面含有很多可用于实例化的类,其中factory类是二进制分析中最常用的类。
         Project对象只代表程序的“初始化映像”。利用编程语言的角度来解释就是,Project只是一个对象(类),并没有实例化,是抽象的。但是,当执行angr的时候,使用的是一个代表模拟程序状态的特定对象,需要使用state()方法来实例化对象。一个SimState包含一个程序的内存,寄存器,文件系统数据…任何会在执行中改变的“实时数据”都会在这个state中。

模拟化管理器 {Simulation Managers}

         simgr适用于程序模拟执行的接口,state只是表示程序执行过程中的一个状态,那么利用simgr可以使得程序执行到下一个状态。

安装angr

         首先使用命令sudo apt-get install python-dev libffi-dev build-essential安装python-dev,libffi-dev等工具。linux发行版通常会把类库的头文件和相关的pkg-config分拆成一个单独的xxx-dev,以python为例,如果你需要安装一个第三方的库,这个库里面含有调用C++(c)的API,或者程序需要连接静态库(.so)则需要安装python-dev。

         接着使用pip install virtualenvwrapper安装virtualenvwrapper。 python virtual enviroment是一个python环境管理工具,该工具能够在真实的系统中创建一个虚拟的python环境,以防止软件安装过程中对真实环境的影响,同时也能方便解决python中不同版本不兼容的问题。

         使用pip的方法安装,则安装路径是/usrname[需要替换成你的]/local/bin/,如果在后面还是找不到的话,使用find / -name virtualenvwrapper.sh查找

         接着设置环境变量export WORKON_HOME=$HOME/Python-workhome,HOME/Python-workhome就是准备放置虚拟环境的地址,比如说选择/tmp

         然后使用source /usrname[需要替换成你的]/local/bin/virtualenvwrapper.sh执行脚本。

       利用mkvirtualenv angr命令建立angr的虚拟python环境,并启动。这一步前提是执行了上一步的脚本,否则报找不到命令的错误

       最后使用sudo pip install angr安装angr,如果报错,安装sudo pip install cffi

使用angr

一般性流程

       一般常见的有以下命令:

  • 新建一个angr工程

    • proj = angr.Project(‘./CrakeMe’,auto_load_libs=false)
  • 新建一个SimState对象

    • state = p.factory.entry_state()

       SimState对象在angr其中的一个子模块SimuVEX中,它追踪且记录着符号信息、符号对应的内存信息和符号对应的寄存器信息,以及打开的文件信息等。可以通过Project.factory这个容器中的任何一个方法来获取SimState对象。这个factory有多个构造函数,如:block、entry_state等。这里使用entry_state返回一个初始化到二进制entry point的SimState对象。

  • 创建一个simgr对象,创建一个模拟器用来模拟程序执行

    • simgr = proj.factory.simgr(state)
  • 利用IDA查找两个分支,然后去探索,用explore执行模拟器,find和avoid用来作为约束条件

    • simgr.explore(find=0x300602,avoid=0x40060E)
  • 打印爆破出来的结果

    • print simgr.found[0].solver.eval(argv1)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      import angr
      import claripy # 接收命令行参数
      proj = angr.Project('./ais3_crackme',auto_load_libs=False) # 创建项目
      argv1 = claripy.BVS('argv1',50*8) # 设置输入
      state = proj.factory.entry_state(args=['./ais3_crackme',argv1]) # 设置入口点
      simgr = proj.factory.simgr(state) # 初始化运行器
      simgr.explore(find=0x300602,avoid=0x40060E) # 探索
      print simgr.found[0].solver.eval(argv1) # 打印结果
      print simgr.found[0].solver.eval(agrv1,cast_to=str) # 转化为asscii

获取输入

  • 无输入
  • 命令行输入

       当程序要求命令行参数时,我们首先需要使用claripy这个模块来定义抽象的数据。

1
import claripy

       laripy的BVS函数可以创建一个指定长度的抽象数据,BVS函数要求两个参数,第一个参数为变量名,第二个参数为变量长度。

1
argv1 = claripy('argv1',50*8)

       这样,我们就创建好了一个命令行参数,我们现在可以将程序运行到程序入口处,并获得当前的一个状态。

1
state = proj.factory.entry_state(args=['./CrakeMe',argv1])

  • 标准输入

       当程序需要从标准输入处读取数据时,需要使用read_from()函数,要注意,这个函数位于状态中,并且我们可以对输入进行一些约束以减少符号执行遍历的路径

1
2
3
for _ in xrange(5):
k = state.posix.files[0].read_from(1)
state.se.add(k!=10)

获取输出

  • 获取程序的当前输出

    1
    print simgr.found[0].posix.dumps(0)
  • 命令行参数

    1
    print simgr.found[0].solver.eval(argv1,cast_to=str)
  • 标注输出

    1
    2
    res = simgr.founf[0].posix.files[0].all_bytes()
    print simgr.found[0].solver.eval(res,cast_to=str)

CTF例子

第一个Demo

       利用如下代码:编译成一个64位的elf文件,在编译的过程中使用如下编译选项以关闭PIE选项: gcc -no-pie Test1.cpp -o test1 ,参考自:github.com/firmianay/CTF-All-In-One/blob/master/doc/4.4_gcc_sec.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
void success(){
printf("success\n");
}
void failed(){
printf("failed\n");
}
int main(void){
char name[9];
scanf("%s",name);
if(!strcmp(name,"jsk")){
success();
}else{
failed();
}
return 0;
}

       利用checksec检查文件属性,checksec利用sudo pip install pwntools安装的

       在IDA中查看反汇编代码如下:存在两个分支。

       使用如下angr脚本

1
2
3
4
5
6
import angr
proj = angr.Project("./test1",auto_load_libs=False) # 创建工程
state = proj.factory.entry_state() # 确定程序入口
simgr = proj.factory.simgr(state) # 创建一个模拟器用来模拟程序执行
simgr.explore(find=0x0000000000400643,avoid=0x000000000040064A) # 用explore执行模拟器,find和avoid用来作为约束条件
simgr.found[0].posix.dumps(0)

       结果如下:

第二个Demo

       题目来源:https://github.com/angr/angr-doc/tree/master/examples/ais3_crackme

       执行一下发现命令行选项作为输入,所有在使用angr的时候需要导入import charipy

       利用IDA看一下,发现如下分支,确定约束条件

       编写angr脚本

1
2
3
4
5
6
7
8
import angr
import charipy
proj = angr.Project('./test2',auto_load_libs=False)
argv1 = charipy.BVS('argv1',50*8) # 确定输入的大小
state = proj.factory.entry_state(args=['./test2',argv1])
simgr = proj.factory.simgr(state)
simgr.explore(find=0x0000000000400602,avoid=0x000000000040060E)
print simgr.found[0].solver.eval(argv1,cast_to=str) # 以字符形式打印flag

       结果为:

第三个Demo

       这是一个标准输入的例子,但是我的ubuntu运行不起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding:utf-8 -*-
import angr
import sys
import claripy
reload(sys)
sys.setdefaultencoding('utf8')
p = angr.Project('./ppp') # 创建项目
state = p.factory.entry_state() # 描述程序状态
for _ in xrange(4): # 获取4位标准输入
k = state.posix.files[0].read_from(1) # 固定读取标准输入最后一个字节
state.se.add(k>47) # 限制输入内容
k = state.posix.files[0].read_from(1) # 读取最后一位
state.se.add(k==10) # k==10,换行符表示结束输入
state.posix.files[0].seek(0) # 设置读取位置位起始位置
state.posix.files[0].length = 5 # 读取5位
print '[*] simulation_manager start...............'
# sm = p.factory.simulation_manager(state)
sm = p.factory.simgr(state)
sm.explore(find=0x08048689,avoid=0x0804869E)
if len(sm.found)>0: # 获取到了found
inp = sm.found[0].posix.files[0].all_bytes() # 输出
print sm.found[0].solver.eval(inp,cast_to = str)
print '[*] end angr.....'