一些碎碎念 本来是想学z3的,后来发现都要用 virtualenvwrapper ,既然都是符号执行,那就先试试angr的水吧。
本来是挺好配置环境的,但是之前那个ubuntu有问题怎么都配不上,无奈重装了一个虚拟机,捏妈,装了一晚上,加上下载中文包,啥也没干,被迫摆烂一天重新开始学。
不过令我高兴的是,新装的ubuntu可以支持和原系统共享剪贴板,好好好,终于可以永久告别啥指令都要建个文档用文件互相传的日子喽。
angr介绍 angr 是一个基于符号执行和模拟执行的二进制框架,可以用在很多的场景,比如逆向分析,漏洞挖掘等。
符号执行
假如我们遇到一道逆向分析题,正常的流程应该是:拖入IDApro一键F5反编译然后硬刚代码写exp得到flag使得该程序输入该flag可以输出“success”等提示字样。(bushi
假如有一堆if语句,分别是if(a>b),if(b>c)……(等等,这不是差分约束么)
于是如果我有一个东西,可以自动分析这些语句的逻辑构造一个输入可以让程序跑到指定的一条语句,比如printf(“Oooooooops”);那么是不是就可以自动获取flag了?
更详细的解释是:根据符号执行,我们把input语句看作是获取一个符号值,分析该程序的每一个分支进行之后所得到的表达式,如果我们对其增加约束,由于angr里内置了一个z3求解器,所以我们就可以得到想要的输入值。
virtualenvwrapper 首先是环境配置。virtualenvwrapper是一个虚拟的python环境,假如你再安装angr前安装了z3,那么二者会发生一些冲突,因此需要把二者的环境隔离开,创建一个新的虚拟环境。
安装virtualenvwrapper 第一步:
1 sudo apt-get install python-dev libffi-dev build-essential virtualenvwrapper
正常都是没有问题的,如果这一步有问题建议直接remake,就像某彩笔一样。
然后配置环境变量:
先把“显示隐藏文件选项”勾选上,然后直接搜索.bashrc文件,打开后,在末尾加上
1 2 export WORKON_HOME=$HOME/Python-workhome source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
保存后source ~/.bashrc
然后mkvirtualenv --python=$(which python3) angr && pip install angr
安装即可,SG大爹说 但其实好像也没多长时间(捂脸)
virtualenvwrapper的操作命令 workon: 列出已有环境(我们只创建了叫angr的环境)
workon angr: 切换到环境angr
deactivate: 退出环境
rmvirtualenv: 删除环境 (这个就不试了)
使用初探 例题来自, https://github.com/jakespringer/angr_ctf
【例 1.1 - explore】00_angr_find (explore的find方法)
一个比较简单的程序,用angr尝试解一下。
我们发现只要程序运行到puts("Good Job")
这一行那么我们就得到了flag,找到该语句地址:
题目名字就是hint,简单find,因为该程序计算量不大,即使自己分析也是可行的,此题仅作为练手。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import angrimport syspath = '/home/ep/桌面/00_angr_find' project = angr.Project(path) init_state = project.factory.entry_state() simulation = project.factory.simgr(init_state) aim_addr = 0x804867D simulation.explore(find = aim_addr) if simulation.found: solution_state = simulation.found[0 ] print (solution_state.posix.dumps(sys.stdin.fileno())) else : print ('Failed.' )
进入angr环境后,运行得到flag。
【例 1.2 - explore】01_angr_avoid (explore的avoid方法) IDApro打开后发现
main函数里的内容太多了显示不下,留意函数名
1 2 3 4 void avoid_me () { should_succeed = 0 ; }
1 2 3 4 5 6 7 int __cdecl maybe_good (char *s1, char *s2) { if ( should_succeed && !strncmp (s1, s2, 8u ) ) return puts ("Good Job." ); else return puts ("Try again." ); }
题目名字就是hint,和例1一样,加上avoid约束即可:
1 2 3 4 5 6 import angrproj = angr.Project('/home/ep/桌面/01_angr_avoid' ) state = proj.factory.entry_state() simgr = proj.factory.simgr(state) simgr.explore(find=0x80485E0 , avoid=0x80485A8 ) print (simgr.found[0 ].posix.dumps(0 ))
【例 1.3 - explore】02_angr_find_condition (以特定输出为出口) 反编译后发现汇编代码只能用文本模式打开,可以看到有一堆puts(“Good Job.”)语句,可以反编译出正常代码说明这些函数都是一样的,但是对应的地址却有很多个,这样在使用angr的时候没有办法给出准确的addr,常规的写法已经不可行了。
好在 ,explore函数的find参数除了地址外,也可以是一个携带SimState参数的函数,我的理解是它不仅可以根据地址,也可以根据特定的输出来判定程序的出口。
根据C代码,发现我们需要该程序输出Good Job
,不输出Try again
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int __cdecl main(int argc, const char **argv, const char **envp){ int i; int j; char s1[20 ]; char s2[20 ]; unsigned int v8; v8 = __readgsdword(0 x14u); for ( i = 0 ; i <= 19 ; ++i ) s2[i] = 0 ; qmemcpy(s2, "VXRRJEUR" , 8 ); printf("Enter the password: " ); __isoc99_scanf("%8s" , s1); for ( j = 0 ; j <= 7 ; ++j ) s1[j] = complex_function(s1[j], j + 8 ); if ( !strcmp(s1, s2) ) puts("Good Job." ); else puts("Try again." ); return 0 ; }
于是exp还可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 import angrdef 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 ) proj = angr.Project('/home/ep/桌面/02_angr_find_condition' ) state = proj.factory.entry_state() simgr = proj.factory.simgr(state) simgr.explore(find=is_successful, avoid=should_avoid) print (simgr.found[0 ].posix.dumps(0 ))
还可以借助python的lambda公式来写脚本,其实也可以理解为函数
1 2 3 4 5 6 7 8 9 10 import angrproj = angr.Project('/home/ep/桌面/02_angr_find_condition' ) state = proj.factory.entry_state() simgr = proj.factory.simgr(state) simgr.explore( find=lambda state : b'Good Job.' in state.posix.dumps(1 ), avoid=lambda state: b'Try again.' in state.posix.dumps(1 ) ) print (simgr.found[0 ].posix.dumps(0 ))
运行得到flag:
【例 2.1 - symbolic】03_angr_symbolic_registers (对寄存器符号化) 反编译后,源代码逻辑比较简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int __cdecl main (int argc, const char **argv, const char **envp) { __int64 user_input; int v5; int v6; int v7; int v8; printf ("Enter the password: " ); user_input = get_user_input(); v7 = HIDWORD(user_input); v5 = complex_function_1(user_input); v6 = complex_function_2(); v8 = complex_function_3(v7); if ( v5 || v6 || v8 ) puts ("Try again." ); else puts ("Good Job." ); return 0 ; }
但是complex函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 unsigned int __cdecl complex_function_1 (int a1) { return (((((((((((((((((((((a1 + 17062705 ) ^ 0xB168C552 ) + 647103529 ) ^ 0x9F14CFD7 ) - 548738866 ) ^ 0xF78063EF ) - 1352480098 ) ^ 0x5D1F4C6 ) - 57802472 ) ^ 0xB6F70BF8 ) - 1347645151 + 648671421 ) ^ 0x3D5082FE ) - 9365053 ) ^ 0xD0150EAD ) + 1067946459 ) ^ 0xE6E03877 ) - 359192087 + 961945065 ) ^ 0xE1EECD69 ) - 1817072919 ) ^ 0x6B86ECF5 ) - 449212884 ) ^ 0x2012CCDB ; }
没法逆向,考虑用angr。
当然强行explore也是能出的:
1 2 3 4 5 6 7 8 import angrproj = angr.Project('/home/ep/桌面/03_angr_symbolic_registers' ) state = proj.factory.entry_state() simgr = proj.factory.simgr(state) simgr.explore(find=0x80489E9 ,avoid=0x80489DC ) print (simgr.found[0 ].posix.dumps(0 ))
但是根据题目要求,本题需要用手动符号化与约束求解。
由红色方框中的指令可以看出,get_user_input之后,输入的三个数分别存在寄存器eax,ebx,ecx中,可以把这三个寄存器符号化:
1 2 3 4 5 6 a = claripy.BVS('password1' , 32 ) b = claripy.BVS('password2' , 32 ) c = claripy.BVS('password3' , 32 ) state.regs.eax = a state.regs.ebx = b state.regs.edx = c
在angr中,无论是具体值还是符号量都有相同的类型——claripy.ast.bv.BV,也就是BitVector的意思,BV后面的数字表示这个比特向量的位数。
BV可以通过claripy这个模块创建:
1 2 3 4 >>> claripy.BVV(666 , 32 ) <BV32 0x29a > >>> claripy.BVS('sym_var' , 32 ) <BV32 sym_var_97_32>
然后让程序跑到0x080489E6这个地址:
1 simgr.explore(find=0x80489E6 )
这样在确定了目标地址的状态下,angr内置的求解器会自动保存路径约束 ,从而解出三个输入值。
1 2 3 4 solver = simgr.found[0 ].solver print (f'password0: {hex (solver.eval (a))} ' )print (f'password1: {hex (solver.eval (b))} ' )print (f'password2: {hex (solver.eval (c))} ' )
放上完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import angrimport claripyproj = angr.Project('/home/ep/桌面/03_angr_symbolic_registers' ) state = proj.factory.blank_state(addr=0x8048980 ) a = claripy.BVS('password0' , 32 ) b = claripy.BVS('password1' , 32 ) c = claripy.BVS('password2' , 32 ) state.regs.eax = a state.regs.ebx = b state.regs.edx = c simgr = proj.factory.simgr(state) simgr.explore(find=0x80489E6 ) solver = simgr.found[0 ].solver print (f'{hex (solver.eval (a))} ' )print (f'{hex (solver.eval (b))} ' )print (f'{hex (solver.eval (c))} ' )
注意这里的初始state是通过blank_state函数而不是entry_state函数获得的:
1 state = proj.factory.blank_state(addr=0x8048980 )
因为在0x8048980之前的指令对我们的求解其实是没有任何作用的,包括get_user_input函数,因为接下来我们就要将get_user_input函数的结果符号化了,而不是让angr自动帮我们符号化通过scanf读取的数据。同时, 根据x86汇编常识,像eax,ebx,ecx这样的寄存器是上下文无关 的,也就是说一个函数内不会引用在函数外部设置的eax,ebx或者ecx的值,而是在函数内部对寄存器重新初始化并使用 ,因此也不用担心寄存器的初始化问题。
所以我们用blank_state获取的状态,即时初始的一些寄存器处于未初始化状态也是丝毫没有影响的,因为它们马上就会被初始化:
1 2 >>> state.regs.ecx<BV32 reg_c_3_32{UNINITIALIZED}>
至于esp,angr会给他们一个默认的初值,使接下来的函数调用不会爆炸:
1 2 >>> state.regs.esp<BV32 0x7fff0000 >
ebp的初始值仍然是未初始化的,但对我们后续的符号执行没有影响,不管它就行:
1 2 >>> state.regs.ebp<BV32 reg_1c_3_32{UNINITIALIZED}>
【例 2.2 - symbolic】04_angr_symbolic_stack (对栈空间内的数据符号化) 反编译得到C语言代码,代码逻辑比较简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 int handle_user () { int v1; int v2[3 ]; __isoc99_scanf("%u %u" , v2, &v1); v2[0 ] = complex_function0(v2[0 ]); v1 = complex_function1(v1); if ( v2[0 ] == 1999643857 && v1 == -1136455217 ) return puts ("Good Job." ); else return puts ("Try again." ); }
在【例 2.1】中我们了解了寄存器的符号化,接下来再看看如何对栈空间内的数据进行符号化 。
还是先从scanf语句后开始看,用blank_state 跳过输入
1 state = proj.factory.blank_state(addr=0x8048694 )
在【例 2.1】文末中我们知道通过blank_state获取初始状态,ebp的值是未约束的。
因此在本题中,我们要向栈中push符号值,并且通过ebp索引这些符号值(比如[ebp+var_10][ebp+var_C]
),所以我们得让ebp有一个正确的初值。
前置芝士:
intel系统中栈是向下生长的(栈越扩大其值越小,堆恰好相反)
(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。 (2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
根据上述的定义,在通常情况下ESP是可变的,随着栈的生产而逐渐变小,而EBP寄存器是固定的,只有当函数的调用后,发生入栈操作而改变。
在上述的定义中使用ESP来标记栈的底部,他随着栈的变化而变化
pop ebp; 出栈 栈扩大4byte 因为ebp为32位
push ebp; 入栈,栈减少4byte
add esp, 0Ch; 表示栈减小12byte
sub esp, 0Ch; 表示栈扩大12byte
由于以下遇到的寄存器为32位,因此每push一次会偏移4byte
而之所以说是正确的初值,是因为我们跳过了函数开头对栈的调整 ,因此我们还需要手动调整ebp的值来保证栈帧平衡:
汇编语言补充知识:mov是赋值,比如mov ebp,esp,是指将esp中的内容赋值给ebp,lea是赋地址,lea eax,[ebp+var_10]就是把该地址赋给eax
因此ebp的偏移量为0x18+4+4+4+4=40
1 state.regs.ebp = state.regs.esp + 40
通过对scanf之后汇编语言的分析,我们发现输入值应该存在[ebp+var_C]和[ebp+var_10]两个位置
于是esp的值执行调整,使我们接下来push进去的符号值恰好在[ebp+var_10]和[ebp+var_C] 这两个位置,push完之后要把esp调回来即可,放上完整脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import angrimport claripyproj = angr.Project('/home/ep/桌面/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 password0 = claripy.BVS('password0' , 32 ) password1 = claripy.BVS('password1' , 32 ) state.stack_push(password0) state.stack_push(password1) state.regs.esp = state.regs.ebp - 40 simgr = proj.factory.simgr(state) simgr.explore(find=0x80486E1 ) solver = simgr.found[0 ].solver print (f'password0: {hex (solver.eval (password0))} ' )print (f'password1: {hex (solver.eval (password1))} ' )
得到输入值
【例 2.3 - symbolic】05_angr_symbolic_memory (对内存符号化) 代码逻辑还是比较简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int __cdecl main (int argc, const char **argv, const char **envp) { int i; memset (user_input, 0 , 0x21 u); printf ("Enter the password: " ); __isoc99_scanf("%8s %8s %8s %8s" , user_input, &unk_A1BA1C8, &unk_A1BA1D0, &unk_A1BA1D8); for ( i = 0 ; i <= 31 ; ++i ) *(_BYTE *)(i + 169583040 ) = complex_function(*(char *)(i + 169583040 ), i); if ( !strncmp (user_input, "NJPURZPCDYEAXCSJZJMPSOMBFDDLHBVN" , 0x20 u) ) puts ("Good Job." ); else puts ("Try again." ); return 0 ; }
可以看出输入是存在一个user_input
全局变量里,且起始地址为0A1BA1C9 :
我们只需要对user_input
进行符号化即可,也就是对0xA1BA1C0开始的32字节内存进行符号化,符号化方式有两种:
1 2 3 4 5 6 7 8 password0 = claripy.BVS('password0' , 64 ) password1 = claripy.BVS('password1' , 64 ) password2 = claripy.BVS('password2' , 64 ) password3 = claripy.BVS('password3' , 64 ) state.mem[0xA1BA1C0 ].uint64_t = password0 state.mem[0xA1BA1C0 + 8 ].uint64_t = password1 state.mem[0xA1BA1C0 + 16 ].uint64_t = password2 state.mem[0xA1BA1C0 + 24 ].uint64_t = password3
或者根据上图的地址(两种方法等效):
1 2 3 4 state.memory.store(0x0A1BA1C0 , flag_0) state.memory.store(0x0A1BA1C8 , flag_1) state.memory.store(0x0A1BA1D0 , flag_2) state.memory.store(0x0A1BA1D8 , flag_3)
如果我们要获取内存中的数据(具体值或者符号值),可以这样用:
1 2 >>> state.mem[0xA1BA1C0 ].uint64_t.resolved<BV64 password0_0_64>
下面放上完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import angrimport claripyproj = angr.Project('/home/ep/桌面/05_angr_symbolic_memory' ) state = proj.factory.blank_state(addr=0x80485FE ) a = claripy.BVS('flag0' , 64 ) b = claripy.BVS('flag1' , 64 ) c = claripy.BVS('flag2' , 64 ) d = claripy.BVS('flag3' , 64 ) state.mem[0xA1BA1C0 ].uint64_t = a state.mem[0xA1BA1C0 + 8 ].uint64_t = b state.mem[0xA1BA1C0 + 16 ].uint64_t = c state.mem[0xA1BA1C0 + 24 ].uint64_t = d simgr = proj.factory.simgr(state) simgr.explore(find=0x804866A ) solver = simgr.found[0 ].solver print (f'flag0: {solver.eval (a, cast_to=bytes )} ' )print (f'flag1: {solver.eval (b, cast_to=bytes )} ' )print (f'flag2: {solver.eval (c, cast_to=bytes )} ' )print (f'flag3: {solver.eval (d, cast_to=bytes )} ' )
当然也可以把四个数据放在一起写,但是结果和上面那个方法是相反的,应该是端序问题,不太清楚。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import angrimport claripyproj = angr.Project('/home/ep/桌面/05_angr_symbolic_memory' ) state = proj.factory.blank_state(addr=0x80485FE ) flag = claripy.BVS('flag' , 64 * 4 ) state.memory.store(0xA1BA1C0 , flag) simgr = proj.factory.simgr(state) simgr.explore(find=0x804866A ) solver = simgr.found[0 ].solver print (f'flag: {solver.eval (flag, cast_to=bytes )} ' )
这里我们在使用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)
给出最大可行解
【例 2.4 - symbolic】06_angr_symbolic_dynamic_memory (对动态内存的符号化) 这题使用了malloc函数来动态分配内存,因此输入的地址就不是固定的了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int __cdecl main (int argc, const char **argv, const char **envp) { char *v3; char *v4; int v6; int i; buffer0 = (char *)malloc (9u ); buffer1 = (char *)malloc (9u ); memset (buffer0, 0 , 9u ); memset (buffer1, 0 , 9u ); printf ("Enter the password: " ); __isoc99_scanf("%8s %8s" , buffer0, buffer1, v6); for ( i = 0 ; i <= 7 ; ++i ) { v3 = &buffer0[i]; *v3 = complex_function(buffer0[i], i); v4 = &buffer1[i]; *v4 = complex_function(buffer1[i], i + 32 ); } if ( !strncmp (buffer0, "UODXLZBI" , 8u ) && !strncmp (buffer1, "UAORRAYF" , 8u ) ) puts ("Good Job." ); else puts ("Try again." ); free (buffer0); free (buffer1); return 0 ; }
可以看到输入是两个长度为8的字符串buffer0和buffer1。虽然malloc是随机分配的,但是由于angr其实并没有真正运行过源程序 ,因此我们可以直接指定一段地址区间存储该字符串
1 2 3 4 5 6 7 8 flag0 = claripy.BVS('flag0' , 64 ) flag1 = claripy.BVS('flag1' , 64 ) state.memory.store(0x8274600 , flag0) state.memory.store(0x8274608 , flag1) state.mem[0x8274600 ].uint64_t = flag0 state.mem[0x8274608 ].uint64_t = flag1
然后找到buffer0和buffer1的真实地址并改成之前选定的假地址:
1 2 3 4 5 6 7 state.memory.store(0xABCC8A4 , 0x8274600 , endness = proj.arch.memory_endness) state.memory.store(0xABCC8AC , 0x8274608 , endness = proj.arch.memory_endness) state.mem[0xABCC8A4 ].uint32_t = 0x8274600 state.mem[0xABCC8AC ].uint32_t = 0x8274608
接下来的操作和之前就一样了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import angrimport claripyproj = angr.Project('/home/ep/桌面/06_angr_symbolic_dynamic_memory' ) state = proj.factory.blank_state(addr=0x8048696 ) flag0 = claripy.BVS('flag0' , 64 ) flag1= claripy.BVS('flag1' , 64 ) state.memory.store(0xABCC8A4 , 0x8274600 , endness = proj.arch.memory_endness) state.memory.store(0xABCC8AC , 0x8274608 , endness = proj.arch.memory_endness) state.memory.store(0x8274600 , flag0) state.memory.store(0x8274608 , flag1) simgr = proj.factory.simgr(state) simgr.explore(find=0x8048759 ) solver = simgr.found[0 ].solver print (f'flag0: {solver.eval (flag0, cast_to=bytes )} ' )print (f'flag1: {solver.eval (flag1, cast_to=bytes )} ' )
【例 2.5 - symbolic】07_angr_symbolic_file (对文件内容符号化) C代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { int i; memset (buffer, 0 , sizeof (buffer)); printf ("Enter the password: " ); __isoc99_scanf("%64s" , buffer); ignore_me((int )buffer, 0x40 u); memset (buffer, 0 , sizeof (buffer)); fp = fopen("OJKSQYDP.txt" , "rb" ); fread(buffer, 1u , 0x40 u, fp); fclose(fp); unlink("OJKSQYDP.txt" ); for ( i = 0 ; i <= 7 ; ++i ) *(_BYTE *)(i + 134520992 ) = complex_function(*(char *)(i + 134520992 ), i); if ( strncmp (buffer, "AQWLCTXB" , 9u ) ) { puts ("Try again." ); exit (1 ); } puts ("Good Job." ); exit (0 ); }
在这题中我们要忽略scanf,直接对文件的内容进行符号化。要对文件内容进行符号化,首先我们要创建一个模拟的文件SimFile
,文件名为’OJKSQYDP.txt’,内容为8字节的符号值,大小为0x40字节,然后插入到state的文件系统(FileSystem) 中。
1 2 3 4 5 6 7 8 9 10 11 12 import angrimport claripyproj = angr.Project('/home/ep/桌面/07_angr_symbolic_file' ) state = proj.factory.blank_state(addr=0x80488D3 ) flag = claripy.BVS('flag' , 64 ) sim_file = angr.SimFile(name='OJKSQYDP.txt' , content=flag, size=0x40 ) state.fs.insert('OJKSQYDP.txt' , sim_file) simgr = proj.factory.simgr(state) simgr.explore(find=0x80489AD ) solver = simgr.found[0 ].solver print (f'flag: {solver.eval (flag, cast_to=bytes )} ' )
【例 3.1 - constraints】08_angr_constraints (手动添加约束避免路径爆炸) C代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int __cdecl main (int argc, const char **argv, const char **envp) { int i; qmemcpy(&password, "AUPDNNPROEZRJWKB" , 16 ); memset (&buffer, 0 , 0x11 u); printf ("Enter the password: " ); __isoc99_scanf("%16s" , &buffer); for ( i = 0 ; i <= 15 ; ++i ) *(_BYTE *)(i + 134520912 ) = complex_function(*(char *)(i + 134520912 ), 15 - i); if ( check_equals_AUPDNNPROEZRJWKB(&buffer, 16 ) ) puts ("Good Job." ); else puts ("Try again." ); return 0 ; }
找到关键函数check_equals_AUPDNNPROEZRJWKB
1 2 3 4 5 6 7 8 9 10 11 12 13 _BOOL4 __cdecl check_equals_AUPDNNPROEZRJWKB (int a1, unsigned int a2) { int v3; unsigned int i; v3 = 0 ; for ( i = 0 ; i < a2; ++i ) { if ( *(_BYTE *)(i + a1) == *(_BYTE *)(i + 134520896 ) ) ++v3; } return v3 == a2; }
可以看出,字符长度为16, 只有和目标串全部匹配一边并且相同才会让v3==v2,而不是只要配对失败就退出循环,根据搜索树 ,每判断一位字符就出现了两个分支,直到到达第16层,出现了$2^{16}$个分支才能最终确定每一位是什么(如果逐字符判断,如果哪一位不对直接退出循环,就相当于加了个剪枝,效率就大大提升,但是本体的写法比较丑),因此对于这种逻辑非常简单,并且会引起路径爆炸 的代码我们可以选择手动添加约束并且让程序停止在该代码之前来解决。
完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import angrimport claripyproj = angr.Project('/home/ep/桌面/08_angr_constraints' ) state = proj.factory.blank_state(addr=0x8048622 ) flag = claripy.BVS('flag' , 16 * 8 ) buffer_addr = 0x804A050 state.memory.store(buffer_addr, flag) simgr = proj.factory.simgr(state) simgr.explore(find=0x8048669 ) found = simgr.found[0 ] found.add_constraints(found.memory.load(buffer_addr, 16 ) == b'AUPDNNPROEZRJWKB' ) print (found.solver.eval (flag, cast_to=bytes ))
路径爆炸的概念与数学中的指数爆炸 概念类似,即某些情况下符号执行的路径/状态以指数级增长。
从08_angr_constraints
中我们发现,即使是非常简单的比较函数,也可能让angr产生指数级的路径(216条路径),耗费大量时间甚至根本跑不出来,这就是符号执行的重大缺陷之一——路径爆炸。这既是我们在用符号执行解决实际问题时需要注意的问题,也是一种可以用来抗符号执行的思路。
应对路径爆炸的方法有很多,甚至还有专门的论文来讲述缓解路径爆炸的方法,在上一题中我们学到了最简单的一种:避开会产生路径爆炸的函数,用手动添加约束替代,这是最简单,也是非常好用的一种方法。总的来说,读者们在使用符号执行引擎时,要识别那些会导致路径爆炸的代码,并巧妙绕过。
【例 4.1 - hooks】09_angr_hooks (对call指令进行hook避免路径爆炸) 反编译发现不同于【例 3.1】,本题的check出现在中间,因此无法使用上题手动添加约束的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __cdecl main (int argc, const char **argv, const char **envp) { _BOOL4 v3; int i; int j; qmemcpy(password, "XYMKBKUHNIQYNQXE" , 16 ); memset (buffer, 0 , 0x11 u); printf ("Enter the password: " ); __isoc99_scanf("%16s" , buffer); for ( i = 0 ; i <= 15 ; ++i ) *(_BYTE *)(i + 134520916 ) = complex_function(*(char *)(i + 134520916 ), 18 - i); equals = check_equals_XYMKBKUHNIQYNQXE(buffer, 16 ); for ( j = 0 ; j <= 15 ; ++j ) *(_BYTE *)(j + 134520900 ) = complex_function(*(char *)(j + 134520900 ), j + 9 ); __isoc99_scanf("%16s" , buffer); v3 = equals && !strncmp (buffer, password, 0x10 u); equals = v3; if ( v3 ) puts ("Good Job." ); else puts ("Try again." ); return 0 ; }
由于check_equals函数本身的流程非常简单,那么我们就可以用hook(钩子)技术将check_equals
函数替换为一个等效的并且不会导致路径爆炸的函数 ,然后再进行符号执行 ,先找到代替换函数的入口地址:
1 2 3 4 5 6 7 @proj.hook(addr=0x80486B3 , length=5 ) def my_check_equals (state ): buffer_addr = 0x804A054 buffer = state.memory.load(buffer_addr, 16 ) state.regs.eax = claripy.If(buffer == b'XYMKBKUHNIQYNQXE' , claripy.BVV(1 , 32 ), claripy.BVV(0 , 32 ))
这里解释一下claripy.If(lambda,a,b)
,该表达式的意思是如果表达式labmda成立,则返回a,也就是数字1(32位),否则返回b,也就是数字0(32位)。
解决了路径爆炸以后,直接explore即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import angrimport claripyproj = angr.Project('/home/ep/桌面/09_angr_hooks' ) @proj.hook(addr=0x80486B3 , length=5 ) def my_check_equals (state ): buffer_addr = 0x804A054 buffer = state.memory.load(buffer_addr, 16 ) state.regs.eax = claripy.If(buffer == b'XYMKBKUHNIQYNQXE' , 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 ) state = proj.factory.entry_state() simgr = proj.factory.simgr(state) simgr.explore(find = aim_out, avoid = avoid_out) print (simgr.found[0 ].posix.dumps(0 ))
【例4.2 - hooks】10_angr_simprocedures (对函数本身进行hook避免路径爆炸) 反编译感觉没什么毛病:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 _BOOL4 __cdecl check_equals_ORSDDWXHZURJRBDH (int a1, unsigned int a2) { int v3; unsigned int i; v3 = 0 ; for ( i = 0 ; i < a2; ++i ) { if ( *(_BYTE *)(i + a1) == *(_BYTE *)(i + 134529096 ) ) ++v3; } return v3 == a2; } int __cdecl main (int argc, const char **argv, const char **envp) { int i; char s[17 ]; unsigned int v6; v6 = __readgsdword(0x14 u); memcpy (&password, "ORSDDWXHZURJRBDH" , 0x10 u); memset (s, 0 , sizeof (s)); printf ("Enter the password: " ); __isoc99_scanf("%16s" , s); for ( i = 0 ; i <= 15 ; ++i ) s[i] = complex_function(s[i], 18 - i); if ( check_equals_ORSDDWXHZURJRBDH(s, 16 ) ) puts ("Good Job." ); else puts ("Try again." ); return 0 ; }
但是一看汇编傻眼了:有一堆check函数,代码逻辑相同,但是地址不同,无法对call指令进行hook了。
于是要引入新方法——SimProcedures(模拟程序)。
我们用一个SimProcedure的子类MyCheckEquals模拟check_equals_ORSDDWXHZURJRBDH函数的功能,SimProcedure中的run函数由子类实现,其接收的参数与C语言中的参数保持一致,返回为对应原函数的返回值。 根据反编译的信息,参数分别是输入字符串的首地址和大小,返回值是0或1 。
1 2 3 4 class MyCheckEquals (angr.SimProcedure): def run (self, buffer_addr, length ): buffer = self.state.memory.load(buffer_addr, length) return claripy.If(buffer == b'ORSDDWXHZURJRBDH' , claripy.BVV(1 , 32 ), claripy.BVV(0 , 32 ))
那么我们如何让程序不跑原来的check函数而是转而去跑我们自己用python写的run函数呢?
1 proj.hook_symbol(symbol_name='check_equals_ORSDDWXHZURJRBDH' , simproc=MyCheckEquals())
只需要调用hook_symbol函数对程序中名为check_equals_ORSDDWXHZURJRBDH的函数进行hook即可。
后面还是老方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import angrimport claripyclass MyCheckEquals (angr.SimProcedure): def run (self, buffer_addr, length ): buffer = self.state.memory.load(buffer_addr, length) return claripy.If(buffer == b'ORSDDWXHZURJRBDH' , 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/ep/桌面/10_angr_simprocedures' ) proj.hook_symbol(symbol_name='check_equals_ORSDDWXHZURJRBDH' , simproc=MyCheckEquals()) state = proj.factory.entry_state() simgr = proj.factory.simgr() simgr.explore(find = aim_out, avoid = avoid_out) print (simgr.found[0 ].posix.dumps(0 ))
【例 4.3 - hooks】11_angr_sim_scanf (对多个参数的scanf函数进行hook) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int __cdecl main (int argc, const char **argv, const char **envp) { int i; char s[20 ]; unsigned int v7; v7 = __readgsdword(0x14 u); memset (s, 0 , sizeof (s)); qmemcpy(s, "SUQMKQFX" , 8 ); for ( i = 0 ; i <= 7 ; ++i ) s[i] = complex_function(s[i], i); printf ("Enter the password: " ); __isoc99_scanf("%u %u" , buffer0, buffer1); if ( !strncmp (buffer0, s, 4u ) && !strncmp (buffer1, &s[4 ], 4u ) ) puts ("Good Job." ); else puts ("Try again." ); return 0 ; }
可以看出scanf函数有多个参数,其实现版本的angr已经可以支持了,这里强行hook一下:
1 2 3 4 5 6 7 8 9 10 11 import angrproj = angr.Project('/home/ep/桌面/11_angr_sim_scanf' ) proj.hook_symbol("__isoc99_scanf" , angr.SIM_PROCEDURES['libc' ]['scanf' ]()) state = proj.factory.entry_state() simgr = proj.factory.simgr(state) simgr.explore( find=lambda state : b'Good Job.' in state.posix.dumps(1 ), avoid=lambda state: b'Try again.' in state.posix.dumps(1 ) ) print (simgr.found[0 ].posix.dumps(0 ))
【例 5.1 - veritesting】12_angr_veritesting (第三种解决路径爆炸的方法) 反编译后发现还是一个路径爆炸的题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 int __cdecl complex_function (int a1, int a2) { if ( a1 <= 64 || a1 > 90 ) { puts ("Try again." ); exit (1 ); } return (a1 - 65 + 2 * a2) % 26 + 65 ; } int __cdecl main (int argc, const char **argv, const char **envp) { int v3; int v5; int v6; int v7; int v8; int v9; const char **v10; int v11; int v12; int v13; int v14; int v15; int v16; int i; int v18; _DWORD v19[9 ]; unsigned int v20; int *p_argc; p_argc = &argc; v10 = argv; v20 = __readgsdword(0x14 u); memset ((char *)v19 + 3 , 0 , 0x21 u); printf ("Enter the password: " ); ((void (__stdcall *)(const char *, char *, int , int , int , int , int , const char **, int , int , int , int , int , int , _DWORD))__isoc99_scanf)( "%32s" , (char *)v19 + 3 , v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v16, v18, v19[0 ]); v15 = 0 ; for ( i = 0 ; i <= 31 ; ++i ) { v3 = *((char *)v19 + i + 3 ); if ( v3 == complex_function(75 , i + 93 ) ) ++v15; } if ( v15 != 32 || (_BYTE)v20 ) puts ("Try again." ); else puts ("Good Job." ); return 0 ; }
Veritesting意为路径归并,具体原理由于链接挂了不得而知,这里只介绍操作,我们只需要在构建simgr的时候添加一个veritesting=True
参数即可:
1 simgr = proj.factory.simgr(state, veritesting=True )
只需要这神奇的一步就可以依靠veritesting原理自动避免路径爆炸。
1 2 3 4 5 6 7 8 9 10 import angrproj = angr.Project('/home/ep/桌面/12_angr_veritesting' ) state = proj.factory.entry_state() simgr = proj.factory.simgr(state, veritesting=True ) simgr.explore( find=lambda state : b'Good Job.' in state.posix.dumps(1 ), avoid=lambda state: b'Try again.' in state.posix.dumps(1 ) ) print (simgr.found[0 ].posix.dumps(0 ))
这种写法与上面的写法是等价的:
1 2 simgr = proj.factory.simgr(state) simgr.use_technique(angr.exploration_techniques.Veritesting())
但是据官方:Versitesting通常与其他exploration_techniques不兼容。
利用veritesting方法避免路径爆炸相对于前两种方法而言比较慢,但是该程序半分钟也能跑完:
【例 6.1 - library】 13_angr_static_binary (识别静态链接库函数) 以下摘自百度百科:
静态编译,就是编译器在编译可执行文件的时候,将可执行文件需要调用的对应静态库 (.a或.lib)中的部分提取出来,链接到可执行文件中去,使可执行文件在运行的时候不依赖于动态链接库。
区分对比
与动态编译的区别
动态编译的可执行文件 需要附带一个的动态链接库 。在执行时,需要调用其对应动态链接库中的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点一是哪怕是很简单的程序,只用到了链接库中的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库 ,则用动态编译的可执行文件就不能运行。
静态编译就是编译器 在编译可执行文件的时候,将可执行文件需要调用的对应静态库 (.a或.lib)中的部分提取出来,链接到可执行文件中去,使可执行文件在运行的时候不依赖于动态链接库。所以其优缺点与动态编译 的可执行文件正好互补。
就比如这个题,反编译后发现有一堆函数:
虽然反编译的main函数和【例 1.1】是一样的,由于此时是静态库,angr无法从动态库种获取链接,因此用原来的脚本无法运行,因此需要我们手动hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import angrproj = angr.Project('/home/ep/桌面/13_angr_static_binary' ) proj.hook_symbol('printf' , angr.SIM_PROCEDURES['libc' ]['printf' ]()) proj.hook_symbol('__isoc99_scanf' ,angr.SIM_PROCEDURES['libc' ]['scanf' ]()) proj.hook_symbol('strcmp' , angr.SIM_PROCEDURES['libc' ]['strcmp' ]()) 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=lambda state : b'Good Job.' in state.posix.dumps(1 ), avoid=lambda state: b'Try again.' in state.posix.dumps(1 ) ) print (simgr.found[0 ].posix.dumps(0 ))
【例 6.2 - library】14_angr_shared_library ( 对动态链接库中单个的函数进行符号执行 ) 反编译发现一个叫validate 的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int __cdecl main (int argc, const char **argv, const char **envp) { char s[16 ]; unsigned int v5; v5 = __readgsdword(0x14 u); memset (s, 0 , sizeof (s)); printf ("Enter the password: " ); __isoc99_scanf("%8s" , s); if ( validate(s, 8 ) ) puts ("Good Job." ); else puts ("Try again." ); return 0 ; }
本题除了14_angr_shared_library 这个文件外还有一个lib14_angr_shared_library.so ,而validate 函数就是从这个外部库文件中导入的,我们对这个库文件反编译可以查看validate 的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 _BOOL4 __cdecl validate (char *s1, int a2) { char *v3; char s2[20 ]; int j; int i; if ( a2 <= 7 ) return 0 ; for ( i = 0 ; i <= 19 ; ++i ) s2[i] = 0 ; qmemcpy(s2, "PVBLVTFT" , 8 ); for ( j = 0 ; j <= 7 ; ++j ) { v3 = &s1[j]; *v3 = complex_function(s1[j], j); } return strcmp (s1, s2) == 0 ; }
动态链接库都是地址无关的可执行文件(position-independent executable,PIE),若不手动指定PIE的基质,angr会将符号执行的基址指定为默认的0x400000 ,当然也可以自行指定基址,方法如下:
1 2 3 4 5 6 base = 0x400000 proj = angr.Project('/home/ep/桌面/lib14_angr_shared_library.so' , load_options={ 'main_opts' : { 'base_addr' : base } })
接下来找到程序的入口和出口:
可以看出validate函数的地址是0x6D7 ,也就是相对于基准地址偏移了0x6D7 。
而程序结束是return strcmp(s1, s2) == 0;
条件句,以retn结束,返回值保存在寄存器eax中,我们需要得到的是eax == 1 这个条件。
注释版脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import angrimport claripybase = 0x400000 proj = angr.Project( "./lib14_angr_shared_library.so" , load_options={ "main_opts" : { "custom_base_addr" : base } } ) buffer = claripy.BVV(0 , 32 ) validate_addr = base + 0x6d7 state = proj.factory.call_state(validate_addr, buffer, 8 ) flag = claripy.BVS("flag" , 8 *8 ) state.memory.store(buffer,flag) aim_addr = base + 0x783 simgr = proj.factory.simgr(state) simgr.explore(find = aim_addr) sol = simgr.found[0 ] sol.add_constraints(sol.regs.eax != 0 ) print (sol.solver.eval (flag, cast_to=bytes ))