符号执行
什么是符号执行?
是一种计算机科学领域的程序分析技术,通过采用抽象的符号代替精确值作为程序输入变量,得出每个路径抽象的输出结果。这一技术在硬件、底层程序测试中有一定的应用符号执行,能够有效的发现程序中的漏洞。
基本概念
即初始参数用变量代替,模拟程序执行过程,维护执行到各个位置时的状态(用各个变量之间的代数关系表示)。
符号状态(Symbolic State)
当前状态所有参数的集合,用 σ σσ 表示。集合中的每个元素用表示初始参数的变量表示。
路径约束(Path Constraint)
到达当前路径需要表示初始参数满足的关系,通常用 PC 表示。
案例
#include <bits/stdc++.h>
using namespace std;
int main() {
int x, y, z;
cin >> x >> y;
z = 2 * y;
if (x == z) {
if (x > y + 10) {
cout << "Path-1";
} else {
cout << "Path-2";
}
} else {
cout << "Path-3";
}
return 0;
}
用$ x_{sim} , , ,y_{sim}$分别表示初始输入的参数 x,y。如果执行到Path-1,则
σ = { x = x s i m , y = y s i m , z = 2 ⋅ y s i m } σ={\{x=xsim,y=ysim,z=2⋅ysim}\} σ={x=xsim,y=ysim,z=2⋅ysim} σ是一个表示当前所有值的状态的集合,里面的值都是用初始输入+常量来表示的。
P C = ( x s i m = 2 ⋅ y s i m ) ∧ ( x s i m > y s i m + 10 ) PC=(x_{sim}=2⋅y_{sim})∧(x_{sim}>y_{sim}+10) PC=(xsim=2⋅ysim)∧(xsim>ysim+10) 这里表示要走到当前路径需要满足的条件
约束求解
即根据符号执行求得的执行到目标位置时的状态,反推出初始时假设的各个变量的值。
例如上面计算出执行到 Path-1 时的 σ σσ 和 PC 。如果执行到 Path-1 则应当满足 PC 为真,进一步推出 $x _{s i m} = 22 , y _{s i m} = 11
$为一组合法解。
局限性
路径爆炸
大多数符号执行方法不适用于大型程序:随着程序规模的扩大,程序中有意义的路径成指数级扩大。有些程序中存在无尽循环或递归调用,这更大大增加了有意义路径,提高了符号执行难度。 为解决此问题,马坚强(音译,Kin-Keung Ma)等人提出使用启发式路径搜索算法提高代码覆盖率;马特·斯塔特斯(Matt Staats)等人提出并行执行独立路径的方法来降低符号执行耗时; 库兹涅佐夫等人则提出了合并相似分支的方法缓解路径爆炸问题。
待测程序输入变量的特点
由于利用路径分析进行程序分析,对于输入变量范围大,但程序分支较少的程序,符号执行方法比对输入变量进行分析的方法(如动态程序分析)具有较强优势。但是对于输入变量变化范围小,程序分支多的程序,符号执行的效率较低。
内存地址存在别名
由于符号执行根据内存地址分析变量及其变化,对于有内存地址别名的程序,符号执行引擎将难以区分不同别名,因此执行结果可能有偏差。(指针分析中的别名分析)
数组的处理
由于数组是大量不同值(如内存地址)的集合,符号执行引擎需要选择将数组作为一个单独的完整变量处理还是将每一个数组元素作为单独的变量处理。由于引擎无法得知程序中每个数组的意义,动态确定每个数组的类型十分具有挑战性。
存在运行环境交互
当某一程序(如下代码所示)与超出符号执行引擎控制的运行环境有交互(如进行系统调用并获取系统调用返回信息等)时,符号执行将难以完成:
int main()
{
FILE *fp = fopen("doc.txt");
...
if (condition) {
fputs("some data", fp);
} else {
fputs("some other data", fp);
}
...
data = fgets(..., fp);
}
该程序将打开一个文件,并根据条件将不同类型的数据写入该文件,然后回读已写入的数据。 从理论上讲,符号执行引擎将在第5行产生两个路径并在第11行返回与在condition
变量中的值一致的数据。然而文件操作被实现为内核中的系统调用,符号执行工具无法控制其行为。 解决这一挑战的主要方法是:
**在符号执行过程中直接执行系统调用:**这种方法的优点是实现起来很简单;缺点是这种调用“并不是符号执行”,其返回内容是实际运行的真实值。
对运行环境建模: 引擎使用模型模拟系统调用,其优点是,能够得到正确的符号执行结果;缺点是需要实现和维护许多可能用到的系统调用模型。 KLEE[14]、Cloud9和Otter[15] 等工具通过实现文件系统操作、套接字、IPC等模型采用了这种方法。
**创建整个运行环境的状态分支:**基于虚拟机技术的符号执行工具通过创建整个VM状态的分支来解决环境问题。比如S2E[16]中,每个符号执行状态都是一个独立的虚拟机快照。这种方法的空间复杂度较高,对内存的消耗很大
angr入门
angr简介
angr是一个支持多处理架构的用于二进制文件分析的工具包,它提供了动态符号执行的能力以及多种静态分析的能力。项目创建的初衷,是为了整合此前多种二进制分析方式的优点,并开发一个平台,以供二进制分析人员比较不同二进制分析方式的优劣,并根据自身需要开发新的二进制分析系统和方式。也正是因为angr是一个二进制文件分析的工具包,因此它可以被使用者扩展,用于自动化逆向工程、漏洞挖掘等多个方面。
安装angr
网上都有,自己百度,尽量linux下,要么虚拟机要么服务器,记得开虚拟环境,不然会影响别的软件运行
1、首先是安装virtualenvwrapper(是python的一个工具库,用于创建一个虚拟的python运行环境,没有python要先安装python)
安装命令:sudo apt-get install python-dev libffi-dev build-essential virtualenvwrapper
2、然后创建虚拟环境
命令:python3 -m venv angr-env,其中angr-env为创建的虚拟环境名
3、启动虚拟环境
首先来到虚拟环境所在的目录下,运行命令:source angr-env/bin/activate
4、在虚拟环境下安装angr
命令为:pip3 install angr
注:直接下载速度会有点慢,建议在清华源镜像进行下载,命令为:
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple/ angr
5、检验angr是否安装成功,(在虚拟环境下)先使用python3运行python命令行,然后使用import angr,若不报错则安装成功
此外,如果想直接用workon命令打开虚拟环境,还需要配置环境变量,我没配,我是使用步骤三的方法运行虚拟环境
建议参考网址:https://blog.csdn.net/qq_45323960/article/details/124392412?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169910488016800182168170%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=169910488016800182168170&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-2-124392412-null-null.142v96control&utm_term=angr%E7%94%A8%E6%9D%A5hook&spm=1018.2226.3001.4187
教程中使用的例题为github上面的一个项目
使用命令:git clone https://github.com/jakespringer/angr_ctf.git
下载下来后进入目录 cd angr_ctf/dist
其中dist目录是题目文件编译好的二进制文件,然后就可以将要做的题目拖到ida中反编译,并编写相应的python文件使用angr来解题
其中所有的题目在参考网址中都有解答
也可以直接在b站搜索angr教程,有详细解答,b站用的也是github上面的那个angr_ctf项目里面的题目
使用angr
以下面的程序为例,介绍angr的基本语法:
//gcc example-1.c -o example-1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void encrypt(char *flag) {
for (int i = 0; i < 13; i++) {
flag[i] ^= i;
flag[i] += 32;
}
}
// flag{G00dJ0b}
int main() {
char flag[100] = {};
scanf("%s", flag);
if (strlen(flag) != 13) {
printf("Wrong length!\n");
exit(0);
}
encrypt(flag);
if (!strcmp(flag, "\x86\x8d\x83\x84\x9f\x62\x56\x57\x8c\x63\x5a\x89\x91")) {
printf("Right!\n");
} else {
printf("Wrong!\n");
}
return 0;
}
加载二进制文件
你总得选定你分析的文件是什么,这一步很简单:
import angr
proj = angr.Project('example-1')
符号执行状态——SimStae
前面介绍的符号执行的时维护的状态在 angr 中对应为 SimStae 类。其中初始状态需要我们来创建。
state = proj.factory.entry_state()
angr 中许多类的都需要通过 factory 获得,factory 是工厂的意思,可以理解为 proj 的 factory 给用户生产了许多类的实例,这里生产的实例是 SimState ,entry_state 函数用来获取程序入口点的状态,也就是初始状态。
state 中维护这内存、寄存器以及文件结构等参数。在初始状态时,我们可以设置其中一些参数,可以是参数也可以是具体数值。
在angr中,无论是具体值还是符号量都有相同的类型——claripy.ast.bv.BV
,也就是BitVector的意思,BV后面的数字表示这个比特向量的位数。
- 创建 32bit 常量数值 666
claripy.BVV(666, 32)
- 创建 32bit 变量 sym_var
claripy.BVS('sym_var', 32)
在本题目中,可以设置 stdin
参数。
sym_flag = claripy.BVS('flag', 60 * 8)
state = proj.factory.entry_state(stdin=sym_flag)
符号执行引擎——Simulation Managers
符号执行的具体过程,即分支推断+求解,由SimulationManagers来实现:
simgr = proj.factory.simgr(state) #这里要输入初始状态哦
angr 符号执行的过程类似于 bfs 的过程。在到达一个条件分枝时,一个状态会分裂出多个状态分别进入各个分枝中。simgr.active
为当前存在的所有状态,simgr.step()
相当于所有状态都向下执行一步。
直接求解
我们可以直接设置需要执行到的目标位置
simgr.explore(find=0x40133D)
这里需要区分是否开启了PIE,如果开启了,则程序的默认地址是从0x400000开始的,所以你的相对地址+基地址才能运行成功。不过也可以自己制定基地址:
#创建angr地址时指定基地址
proj = angr.Project('example-1', load_options={
'main_opts' : {
'base_addr' : 0x400000
}
})
接下来需要写求出来的这些解要放到哪里(有可能不只有一个解法哦):
found = simgr.found[0]
print(found.posix.dumps(0))
此时的状态被保存到了 found这 个数组中,可以通过 simgr.found[0]
获取当前的状态,其中 dumps()
的参数 0,1,2 分别表示 stdin
,stdout
,stderr
的内容,直接输出即可,不需要前面设置 stdin 参数为变量。
完整代码
import angr
proj = angr.Project('example-1')
state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)
simgr.explore(find=0x40133D)
found = simgr.found[0]
print(found.posix.dumps(0))
# b'flag{G00dJ0b}\x00\x00\x02Z\x0e\x02\xe4\x00\x19\x08\x89\x00\x00\x01\x00\x00\x1a\x00\x1a\x02\x00<\x02\x00)\x00\x19\x04\x00\x19\x89\x89\x89\x01\x01\x0e\x01*\x8a\x00\x00\x02\x00\x10\x00\x00J'
求解参数
上面直接求解的去情况,是一种非常暴力的行为,就是直接从二进制程序的入口点开始,直接莽到指定位置,但是在某些需要自己设置参数的情况下则不行了。
import angr
import claripy
#导入二进制文件,创建angr项目
proj = angr.Project('example-1')
#创建一个变量,变量名为flag,大小为60*8
sym_flag = claripy.BVS('flag', 60 * 8)
#设置初始状态,这里设置的入口点输入为flag
state = proj.factory.entry_state(stdin=sym_flag)
#创建符号执行引擎
simgr = proj.factory.simgr(state)
#设置终点
simgr.explore(find=0x40133B)
#设置求解器
solver = simgr.found[0].solver
#添加求解器限制条件
solver.add(simgr.found[0].regs.eax == 0)
#求解
print(solver.eval(sym_flag, cast_to=bytes))
# b'flag{G00dJ0b}\x00\x02\x89\x01J\x01\x8a\x89\x08\x02\x00I\x89\x02\x00\x00*\x01\x00\x01\x01\x01\x00\x01\x02\x02@\x00\x00\x00\x89)\x19\x003\x00\x01J\x01\x01*\x02\x00\x00\x00\x01\x00'
其他奇技淫巧
-
avoid
使用avoid可以规避某些路径,即执行过程中不走这个路径
simgr.explore(find=0x0000000, avoid=0x11111111)
- 自定义find和void
如果在不知道具体要去的地址,但是会有一些其他的回显,比如输出一行话之类的,这个时候可以自定义find和void
def is_successful(state):
return b'Good Job.' in state.posix.dumps(1)
def should_avoid(state):
return b'Try again.' in state.posix.dumps(1)#0:stdin,1:stdout,2stderr
simgr.explore(find=is_successful, avoid=should_avoid)
自定义find和void
如果在不知道具体要去的地址,但是会有一些其他的回显,比如输出一行话之类的,这个时候可以自定义find和void
def is_successful(state):
return b'Good Job.' in state.posix.dumps(1)
def should_avoid(state):
return b'Try again.' in state.posix.dumps(1)#0:stdin,1:stdout,2stderr
simgr.explore(find=is_successful, avoid=should_avoid)