angr符号执行框架,是我一直想要理解学习的,今天就来实际学习体验一下。

根据网上教的,应该是在GitHub - jakespringer/angr_ctf 网站上面下载对应的习题,编写对应的python进行练习。

前置要求:安装angr库,我已经满足,接下来就开始进行捣鼓。

angr的原理我也不太能解释清楚,所以直接开干吧。(默认python代码均引入angr库)

因此,使用 angr 一般分为如下步骤:

  1. 创建 Project,预设 state
  2. 创建位向量和符号变量,保存在内存/寄存器/文件或其他地方(简单的题目直接可以省略)
  3. 将 state 添加到 SM 中
  4. 运行,探索满足条件的路径
  5. 约束求解获取执行结果

1-angr-find

用这个作为入门吧。分析一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [esp+1Ch] [ebp-1Ch]
char s1[9]; // [esp+23h] [ebp-15h] BYREF
unsigned int v6; // [esp+2Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
printf("Enter the password: ");
__isoc99_scanf("%8s", s1);
for ( i = 0; i <= 7; ++i )
s1[i] = complex_function(s1[i], i);
if ( !strcmp(s1, "JACEJGCS") )
puts("Good Job.");
else
puts("Try again.");
return 0;
}

根据学习的知识,首先应该创建一个project。

p = angr.Project("./00",auto_load_libs='false')

为了提高效率,不自动加载依赖库。

然后应该创建一个state,就是作为一个镜像(Project 仅完成二进制文件加载,执行操作实际围绕 SimState 对象(程序状态)开展。 类似于做了一个镜像)

要创建状态,需借助 Project 对象的 factory,它还可用于创建模拟管理器、基本块

state=p.factory.entry_state()

上述方式只是预设了程序开始分析时的状态,我们要分析程序就必须要让它到达下一个状态,这就需要
模拟管理器的帮助(简称 SM)

sm = p.factory.simgr(state)

可以通过 step() 方法来让处于 active 的 state 执行一个基本块,这种操作不会改变 state 本身(类比 docker)state只是一个镜像而已,进入SM才有执行的可能性:

可以使用 explorer 方法去执行某个状态,直到找到目标指令或者 active 中没有状态为止,它有如下参数:

  • find:传入目标指令的地址或地址列表,或者一个用于判断的函数
  • avoid:传入要避免的指令的地址或地址列表,或者一个用于判断的函数,用于减少路径

第一关还是比较简单的,下面就是脚本:

1
2
3
4
5
6
7
import angr
p = angr.Project("./00")
state=p.factory.entry_state()
sm = p.factory.simgr(state)
sm.explore(find=0x8048675)
if sm.found:
print(sm.found[0].posix.dumps(0))

2-angr-avoid

这一关卡需要进行avoid。因为主函数太大了!为什么主函数大?一个函数“avoid me”在主函数被反复调用。只需要增加avoid参数即可。

1
2
3
4
5
6
7
import angr
p = angr.Project("./01")
state=p.factory.entry_state()
sm = p.factory.simgr(state)
sm.explore(find=0x80485B5,avoid=0x80485A8)
if sm.found:
print(sm.found[0].posix.dumps(0))

3-angr-find-condition

这里引入新的概念:利用函数,传给find和avoid,什么意思呢?

函数的return表示“找到这个函数的条件”dump(1)表示标准输出。

这是一个具体实例:

1
2
3
4
def find(state):
return b'Good Job.' in state.posix.dump(1)#标准输出
def avoid(state):
return b'Try again.' in state.posix.dump(1)

程序在正确的时候会提示字符串,所以可以从这里入手。给explore传参只需要传递函数名即可

1
2
3
4
5
6
7
8
9
10
11
import angr
p = angr.Project("./02")
state=p.factory.entry_state()
sm = p.factory.simgr(state)
def find(state):
return b'Good Job.' in state.posix.dumps(1)
def avoid(state):
return b'Try again.' in state.posix.dumps(1)
sm.explore(find=find,avoid=avoid)
if sm.found:
print(sm.found[0].posix.dumps(0))

4-angr_symbolic_registers

开始介绍怎么给寄存器传入对应的初始量了。这一题很有意思,输入做了拆分,通过ecx寄存器做中间人将值最终放在eax、ebx、edx寄存器。

所以我们需要求出三个未知量,自然需要做出调整,这里介绍angr里面的变量类型。我们想要从程序的某个地方开始执行,就需要用到blank_state(addr=0xxxx)。我们可以跳过拆分输入的环节,直接设置符号变量让他求解。

BVS设置为32位,因为一个Int占32位。

不要忘记再给寄存器传入我们的参数,按照源程序的顺序给即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import angr
import claripy
p = angr.Project("./03")
state=p.factory.blank_state(addr=0x8048980)
x = claripy.BVS("x",32)
y = claripy.BVS("y",32)
z = claripy.BVS("z",32)
state.regs.eax=x
state.regs.ebx=y
state.regs.edx=z
sm = p.factory.simgr(state)
def find(state):
return b'Good Job.' in state.posix.dumps(1)
def avoid(state):
return b'Try again.' in state.posix.dumps(1)
sm.explore(find=find,avoid=avoid)
if sm.found:
x=sm.found[0].solver.eval(x)
y=sm.found[0].solver.eval(y)
z=sm.found[0].solver.eval(z)
print(f"{hex(x)} {hex(y)} {hex(z)}")

说实话,跟着教程走还是比较好理解的,记住关键的函数名字即可。

5-angr_symbolic_stack

这个题目开始模拟栈。

这里的scanf的参数又直接压到了栈上。

我们只需要关心求解的未知数在栈上的布局即可,其他的栈内容无需操心,也不会影响大局。

还需要知道的是,blank_state会初始化esp,但是不会初始化ebp,我们可以用esp给ebp赋值,进而初始化栈。

根据ida的栈布局,确定esp相对位置。刚开始肯定是要esp刚好指向两个未知变量栈的下面。这样子push完之后也恰好满足原函数的栈布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import angr
import claripy
p = angr.Project("./04")
state=p.factory.blank_state(addr=0x08048697)
x = claripy.BVS("x",32)
y = claripy.BVS("y",32)
state.regs.ebp=state.regs.esp
state.regs.esp -= 8#根据ida确认
state.stack_push(x)
state.stack_push(y)
sm = p.factory.simgr(state)
def find(state):
return b'Good Job.' in state.posix.dumps(1)
def avoid(state):
return b'Try again.' in state.posix.dumps(1)
sm.explore(find=find,avoid=avoid)
if sm.found:
x=sm.found[0].solver.eval(x)
y=sm.found[0].solver.eval(y)
print(f"{x} {y} ")

6-angr_symbolic_memroy

操作内存。这个程序直接使用memset函数在bss段开辟了一些地方存储输入的数据,这就需要控制内存

读写内存:state.memory.store/load。只需要往对应内存地带写入数据即可。

说明:可以利用store配合寄存器寻址,如state.regs.ebp-0x1c,也可以识别得到。

还需要注意的是,这里的是字符串,因此需要用cast_to强制转换,并需用 decode() 转换为可读字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import angr
import claripy
p = angr.Project("./05")
state=p.factory.blank_state(addr=0x08048601)
x = claripy.BVS("x",64)#这里需要改为64位,因为是8个字符
y = claripy.BVS("y",64)
z = claripy.BVS("z",64)
a = claripy.BVS("a",64)
state.memory.store(0xA1BA1C0,x)
state.memory.store(0xA1BA1C8,y)
state.memory.store(0xA1BA1D0,z)
state.memory.store(0xA1BA1D8,a)
sm = p.factory.simgr(state)
def find(state):
return b'Good Job.' in state.posix.dumps(1)
def avoid(state):
return b'Try again.' in state.posix.dumps(1)
sm.explore(find=find,avoid=avoid)
if sm.found:
x=sm.found[0].solver.eval(x,cast_to=bytes).decode()
y=sm.found[0].solver.eval(y,cast_to=bytes).decode()
z=sm.found[0].solver.eval(z,cast_to=bytes).decode()
a=sm.found[0].solver.eval(a,cast_to=bytes).decode()
print(f"{x} {y} {z} {a}")