angr符号执行框架,是我一直想要理解学习的,今天就来实际学习体验一下。
根据网上教的,应该是在GitHub - jakespringer/angr_ctf 网站上面下载对应的习题,编写对应的python进行练习。
前置要求:安装angr库,我已经满足,接下来就开始进行捣鼓。
angr的原理我也不太能解释清楚,所以直接开干吧。(默认python代码均引入angr库)
因此,使用 angr 一般分为如下步骤:
创建 Project,预设 state
创建位向量和符号变量,保存在内存/寄存器/文件或其他地方(简单的题目直接可以省略)
将 state 添加到 SM 中
运行,探索满足条件的路径
约束求解获取执行结果
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; char s1[9 ]; unsigned int v6; 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 angrp = 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 angrp = 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 angrp = 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 angrimport claripyp = 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 angrimport claripyp = 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 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 angrimport claripyp = angr.Project("./05" ) state=p.factory.blank_state(addr=0x08048601 ) x = claripy.BVS("x" ,64 ) 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} " )