Angr 学习 文章参考:angr初探 | moyaoxueの小屋 和Angr入门 和Angr:一个具有动态符号执行和静态分析的二进制分析工具-腾讯云开发者社区-腾讯云
Angr简介 angr是一个支持多处理架构的用于二进制文件分析的工具包,它提供了动态符号执行的能力以及多种静态分析的能力。项目创建的初衷,是为了整合此前多种二进制分析方式的优点,并开发一个平台,以供二进制分析人员比较不同二进制分析方式的优劣,并根据自身需要开发新的二进制分析系统和方式。
也正是因为angr是一个二进制文件分析的工具包,因此它可以被使用者扩展,用于自动化逆向工程、漏洞挖掘等多个方面。
angr 官方文档
angr_ctf项目GitHub - jakespringer/angr_ctf
angr_ctf则是一个专门针对angr的项目,里面有17个angr相关的题目。这些题目只有一个唯一的要求:你需要找出能够使程序输出“Good Job”的输入,这也是符号执行常见的应用场景。
本系列教程是angr的入门教程,将通过做angr_ctf中的题目的形式来介绍angr。
Angr初探 Angr Project angr的基本过程:
将二进制程序载入angr分析系统
将二进制程序转换成中间语言(intermediate representation, IR)
将IR语言转换成语义较强的表达形式,比如,这个程序做了什么,而不是它是什么。
执行进一步的分析,比如,完整的或者部分的静态分析(依赖关系分析,程序分块)、程序空间的符号执行探索(挖掘溢出漏洞)、一些对于上面方式的结合。
导入模块 :Project类是angr的主类,也是angr的开始,通过初始化该类的对象,可以将你想要分析的二进制文件加载进来,就像这样
angr-CLE :CLE是angr加载二进制文件的组建,在加载二进制文件的时候会分析病读取binary的信息,包括指令地址、shared library、arch information等等。
1 2 import angrproj = angr.Project('./00_angr_find' )
参数为待分析的文件路径,它是唯一必须传入的参数,此外还有一个比较常用的参数load-options,它指明加载的方式,如下:
名称
描述
传入参数
auto_load_libs
是否自动加载程序的依赖
布尔
skip_libs
希望避免加载的库
库名
except_missing_libs
无法解析库时是否抛出异常
布尔
force_load_libs
强制加载的库
库名
ld_path
共享库的优先搜索路径
路径名
少加载一些无关结果的库能够提升angr的效率,Project类中有许多方法和属性,例如加载的文件名、架构、程序入口点、大小端等
angr_IR :angr用VEX IR将指令转化为中间语言IR,分析IR并且模拟,搞清楚它是什么并且做了什么。如下的ARM指令
转化为VEX IR
1 2 3 4 5 t0 = GET:I32(16) t1 = 0x8:I32 t3 = Sub32(t0,t1) PUT(16) = t3 PUT(68) = 0x59FC8:I32
angr-Solver Engine :angr的求解引擎叫Claripy,具体这一步做什么呢,根据程序所需要的输入设置符号变量以及收集限制式等等。
project的基础属性
命令行使用时可以导入monkeyhex
转化为十六进制输出
1 2 3 4 5 6 7 8 9 proj.entry # 文件的入口点 proj.filename # 文件名 proj.arch # 文件的架构 - proj.arch.name # x86/x86-64/ARM - proj.arch.bits # 32/64 - proj.arch.bytes # bytes per instruction, eg : 4/8 - proj.arch.memory_endness # 字节序,例如 Iend_LE代表小端序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 proj.loader # 显示已加载对象,内存映射的地址范围 # <Loaded [file_name], maps [0x400000:0x5004000]> proj.loader.shared_objects # 已加载的所有共享对象,共享库或动态链接库及其内存映射 # OrderedDict([('angr', <ELF Object angr, maps [0x8048000:0x804c033]>), ('libc.so.6', <ELF Object libc.so.6, maps [0x8100000:0x83347bb]>), ('ld-linux.so.2', <ELF Object ld-linux.so.2, maps [0x8400000:0x8437a37]>), ('extern-address space', <ExternObject Object cle##externs, maps [0x8500000:0x8507fff]>), ('cle##tls', <ELFTLSObjectV2 Object cle##tls, maps [0x8600000:0x8614807]>)]) proj.loader.min_addr # 加载的二进制文件占用的内存空间的界限 # 0x8048000 proj.loader.max_addr # 0x8707fff proj.loader.main_object # 返回代表主要加载的二进制文件的对象 # <ELF Object angr, maps [0x8048000:0x804c033]> proj.loader.main_object.execstack # 返回bool,代表主二进制文件是否具有可执行堆栈 # False proj.loader.main_object.pic # 二进制文件是否为位置独立代码,若返回True,则说明开启了ASLR # False
对基本块的操作
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 block = proj.factory.block(proj.entry) # 打印入口的基本块的汇编 block.pp() """ 80490b0 endbr32 80490b4 xor ebp, ebp 80490b6 pop esi 80490b7 mov ecx, esp 80490b9 and esp, 0xfffffff0 80490bc push eax 80490bd push esp 80490be push edx 80490bf call 0x80490dd """ block.instructions # 该基本块的指令数量 # 9 block.instruction_addrs # 该基本块指令地址 # (134516912, 134516916, 134516918, 134516919, 134516921, 134516924, 134516925, 134516926, 134516927) block.capstone # 打印人类可读汇编形式(与.pp()类同) block.vex # 打印IR代码形式 """ IRSB { t0:Ity_I32 t1:Ity_I32 t2:Ity_I32 t3:Ity_I32 t4:Ity_I32 t5:Ity_I32 t6:Ity_I32 t7:Ity_I32 t8:Ity_I32 t9:Ity_I32 t10:Ity_I32 t11:Ity_I32 t12:Ity_I32 t13:Ity_I32 t14:Ity_I32 t15:Ity_I32 t16:Ity_I32 t17:Ity_I32 t18:Ity_I32 t19:Ity_I32 t20:Ity_I32 t21:Ity_I32 t22:Ity_I32 t23:Ity_I32 t24:Ity_I32 t25:Ity_I32 00 | ------ IMark(0x80490b0, 4, 0) ------ 01 | ------ IMark(0x80490b4, 2, 0) ------ 02 | PUT(ebp) = 0x00000000 03 | PUT(eip) = 0x080490b6 04 | ------ IMark(0x80490b6, 1, 0) ------ 05 | t4 = GET:I32(esp) 06 | t3 = LDle:I32(t4) 07 | t15 = Add32(t4,0x00000004) 08 | PUT(esi) = t3 09 | ------ IMark(0x80490b7, 2, 0) ------ 10 | PUT(ecx) = t15 11 | ------ IMark(0x80490b9, 3, 0) ------ 12 | t5 = And32(t15,0xfffffff0) 13 | PUT(cc_op) = 0x0000000f 14 | PUT(cc_dep1) = t5 15 | PUT(cc_dep2) = 0x00000000 16 | PUT(cc_ndep) = 0x00000000 17 | PUT(eip) = 0x080490bc 18 | ------ IMark(0x80490bc, 1, 0) ------ 19 | t8 = GET:I32(eax) 20 | t17 = Sub32(t5,0x00000004) 21 | PUT(esp) = t17 22 | STle(t17) = t8 23 | PUT(eip) = 0x080490bd 24 | ------ IMark(0x80490bd, 1, 0) ------ 25 | t19 = Sub32(t17,0x00000004) 26 | PUT(esp) = t19 27 | STle(t19) = t17 28 | PUT(eip) = 0x080490be 29 | ------ IMark(0x80490be, 1, 0) ------ 30 | t12 = GET:I32(edx) 31 | t21 = Sub32(t19,0x00000004) 32 | PUT(esp) = t21 33 | STle(t21) = t12 34 | PUT(eip) = 0x080490bf 35 | ------ IMark(0x80490bf, 5, 0) ------ 36 | t23 = Sub32(t21,0x00000004) 37 | PUT(esp) = t23 38 | STle(t23) = 0x080490c4 NEXT: PUT(eip) = 0x080490dd; Ijk_Call } """
状态State Project实际上只是将二进制文件加载进来了,要执行它,实际上是对SimState对象进行操作,它是程序的状态。用docker来比喻,Project相当于开发环境,State则是使用开发环境制作的镜像。
要创建状态,需要使用Project对象中的factory,它还可以用于创建模拟管理器和基本块(后面提到),如下:
1 init_state = p.factory.entry_state()
预设状态有四种方式如下:
预设状态方式
描述
entry_state
初始化状态为程序运行到程序入口点处的状态
blank_state(addr=)
大多数数据都没有初始化,状态中下一条指令为addr处的指令
full_init_state
共享库和预定义内容已经加载完毕,例如刚加载完共享库
call_state
准备调用函数的状态
状态包含了程序运行时的一切信息,寄存器、内存的值、文件系统以及符号变量 等,这些信息的使用等用到时再进一步说明。
entry_state和blank_state是常用的两种方式,后者通常用于跳过一些极大降低angr效率的指令,它们间的对比如下:
1 2 3 4 5 6 >>> state = p.factory.entry_state() >>> print(state.regs.rax, state.regs.rip) <BV64 0x1c> <BV64 0x4023c0> >>> state = p.factory.blank_state(addr=0x4023c0) >>> print(state.regs.rax, state.regs.rip) <BV64 reg_rax_42_64{UNINITIALIZED}> <BV64 0x4023c0>
在blank_state方式中,我们仍将地址设定为程序的入口点,然而rax中的值由于没有初始化,它现在是一个名字,也即符号变量,这是符号执行的基础,后续在细说。
此外,可以看到寄存器中的数据类型并不是int,而是BV64,它是一个位向量(Bit Vector),有关位向量的细节之后再说。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 state = proj.factory.entry_state() # <SimState @ 0x80490b0> print(state) print(state.regs.eip) print(state.regs.eax) print(state.mem[proj.entry].int.resolved) """ <BV32 0x80490b0> <BV32 0x1c> <BV32 0xfb1e0ff3> """ state.solver.eval(state.regs.eax) # 转化为python int bv = state.solver.BVV(0x1234, 32) # 反过来创建,create a 32-bit-wide bitvector with value 0x1234 bv = state.solver.BVV(0x1111, 32) # 修改寄存器值 state.regs.eax = bv state.mem[0x1000].long = 4 # 修改内存中的值 print(state.mem[0x1000].long.resolved) # <BV32 0x4>
模拟管理器 上述方式只是预设了程序开始分析时的状态,我们要分析程序就必须要让它到达下一个状态,这就需要模拟管理器的帮助(简称SM).
使用以下指令能创建一个SM,它需要传入一个state或者state的列表作为参数:
1 simgr = p.factory.simgr(state)
SM中有许多列表,这些列表被称为stash,它保存了处于某种状态的state,stash有如下几种:
stash
描述
active
保存接下来可以执行并且将要执行的状态
deadended
由于某些原因不能继续执行的状态,例如没有合法指令,或者有非法指针
pruned
与solve的策略有关,当发现一个不可解的节点后,其后面所有的节点都优化掉放在pruned里
unconstrained
如果创建SM时启用了save_unconstrained,则被认定为不受约束的state会放在这,不受约束的state是指由用户数据或符号控制的指令指针(例如eip)
unsat
如果创建SM时启用了save_unsat,则被认为不可满足的state会放在这里
默认情况下,state会被存放在active中。
stash中的state可以通过move()方法来转移,将fulter_func筛选出来的state从from_stash转移到to_stash:
1 simgr.move(from_stash='deadended', to_stash='more_then_50', filter_func=lambda s: '100' in s.posix.dumps(1))
stash是一个列表,可以使用python支持的方式去遍历其中的元素,也可以使用常见的列表操作。但angr提供了一种更高级的方式,在stash名字前加上one_,可以得到stash中的第一个状态,加上mp_,可以得到一个mulpyplexed版本的stash
此外,稍微解释一下上面代码中的posix.dumps:
state.posix.dumps(0):表示到达当前状态所对应的程序输入
state.posix.dumps(1):表示到达当前状态所对应的程序输出
上述代码就是将deadended中输出的字符串包含’100’的state转移到more_then_50这个stash中。
可以通过step()方法来让处于active的state执行一个基本块,这种操作不会改变state本身:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 >>> state = p.factory.entry_state() >>> simgr = p.factory.simgr(state) >>> print(state.regs.rax, state.regs.rip) <BV64 0x1c> <BV64 0x4023c0> >>> print(simgr.one_active) <SimState @ 0x4023c0> >>> simgr.step() <SimulationManager with 1 active> >>> print(simgr.one_active) <SimState @ 0x529240> >>> print(state.regs.rax, state.regs.rip) <BV64 0x1c> <BV64 0x4023c0>
1 2 3 4 5 6 7 8 9 10 11 import angrp = angr.Project('./00_angr_find' ) state = p.factory.entry_state() simgr = p.factory.simgr(state) print (state.regs.ax) print (simgr.one_active) simgr.step() print (simgr.one_active) print (state.regs.ax)
simgr.one_active
SimulationManager 会把状态放到不同的“stash”(active、deadended、errored 等)。
one_active
就是 当前活跃状态 ,如果只有一个,就直接取出来。
simgr.step()
让所有 active 状态都执行一步指令。
这一步之后,simgr.one_active
里的状态的 ip
(指令指针/程序计数器)会移动到下一条指令。
最后也是SM最常用的技术:探索技术(explorer techniques)
可以使用explorer方法去执行某个状态,直到找到目标指令或者active中没有状态为止,它有如下参数:
find:传入目标指令的地址或地址列表,或者一个用于判断的函数,函数以state为形参,返回布尔值
avoid:传入要避免的指令的地址或地址列表,或者一个用于判断的函数,用于减少路径
此外还有一些搜索策略,之后会集中讲解,默认使用DFS(深度优先搜索)。
explorer找到的符合find的状态会被保存在simgr.found这个列表当中,可以遍历其中元素获取状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import angrproj = angr.Project('./angr' ) state = proj.factory.entry_state() simgr = proj.factory.simulation_manager(state) print (simgr)print (simgr.active)print (simgr.active[0 ].regs.eip)print ('---' )simgr.step() print (simgr.active)print (simgr.active[0 ].regs.eip)""" 这里执行了一整个基本块(注意:smigr不会改变state的信息) <SimulationManager with 1 active> [<SimState @ 0x80490b0>] <BV32 0x80490b0> --- [<SimState @ 0x80490dd>] <BV32 0x80490dd> """
符号执行 angr作为一个二进制分析的工具包,但它通常作为符号执行工具更为出名。
符号执行就是给程序传递一个符号而不是具体的值,让这个符号伴随程序运行,当碰见分支时,符号会进入哪个分支呢?
angr的回答是全都进入!angr会保存所有分支,以及分支后的所有分支,并且在分支时,保存进入该分支时的判断条件,通常这些判断条件时对符号的约束。
当angr运行到目标状态时,就可以调用求解器对一路上收集到的约束进行求解,最终得到某个符号能够到达当前状态的值。
例如,程序接收一个int类型的输入,当这个输入大于0小于5时,就会执行某条保存在该程序中,我们希望执行的指令(例如一个后门函数backdoor),具体而言如下图所示:
angr会沿着分支按照某种策略(默认DFS)进行状态搜索,当达到目标状态(也就是backdoor能够执行的状态),此时angr已经收集了两个约束(x>0 以及x<=5),那么angr就通过这两个约束对x进行求解,解出来的x值就是能够让程序执行backdoor的输入。
在复杂的程序当中,从一个符号到backdoor的路径可能十分复杂,甚至包含一些加密解密的过程,这时就是angr大显身手的时候了。
angr在模拟执行指令时,对于遇到的分支和跳转,会全部进行保留,并且记录用于判断分支的条件(即约束),如下图所示
这些状态都是程序运行到某些阶段时的信息,包括了内存、寄存器、文件系统等多个方面,这些状态中有满足条件的状态,就会被放入到found列表当中。
而在路径搜索时,对于满足avoid条件的状态,则会被丢弃,也就是说,该状态及该状态的后续路径都不会被进行搜索,因此简化了angr的搜索路径,从而提高效率。
Angr CTF 使用angr一般分为如下步骤:
创建Project,预设state
创建位向量和符号变量,保存在内存/寄存器/文件或其他地方
将state添加到SM中
运行,探索满足条件的路径
约束求解获取执行结果
SimulationManager.explore() 这三道题目学习给explore选择参数。
00_angr_find 程序逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import angrp = angr.Project('./dist/00_angr_find' , auto_load_libs=False ) init_state = p.factory.entry_state() simgr = p.factory.simgr(init_state) target = 0x08048678 simgr.explore(find=target) if simgr.found: solution_state = simgr.found[0 ] print (solution_state.posix.dumps(0 ))
事实上,上述脚本能够解决一切有关”为了执行某条目标语句,我应该用怎样的输入”这样的问题,是一个万能脚本。区别在于由于程序的复杂程度和逻辑不同,耗费的时间不同,因此在解决这类问题上,编写angr脚本的本质是在使用angr提供的各种二进制分析方法去优化路径,提高它的运行效率。
01_angr_avoid 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import angrp = angr.Project('./dist/00_angr_find' , auto_load_libs=False ) init_state = p.factory.entry_state() simgr = p.factory.simgr(init_state) target = 0x080485E5 simgr.explore(find=target) if simgr.found: solution_state = simgr.found[0 ] print (solution_state.posix.dumps(0 ))
这样不行,因为直接崩了,中间函数太复杂太多了
可以发现该函数被main函数调用了多次,应该是导致main函数过大的原因,因此要对它进行避免,也就是使用explorer的avoid的参数。
这里需要让angr走到avoid_me
函数后就剪枝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import angrp = angr.Project('./dist/01_angr_avoid' ) init_state = p.factory.entry_state() simgr = p.factory.simgr(init_state) good = 0x080485e5 bad = 0x080485a8 simgr.explore(find= good,avoid = bad) if simgr.found: solution = simgr.found[0 ] print (solution.posix.dumps(0 ))else : raise Exception("Could not find solution" )
simgr.found[0]
遇到地址 good
→ 停下来放到 found
集合里
遇到地址 bad
→ 扔掉放到 avoid
集合里
simgr.found
就是所有到达了 “good” 的路径的列表(state 列表)。
simgr.found[0]
取出第一个满足条件的 state ,即程序在“成功位置”的状态。
solution.posix.dumps(0)
在 angr 里,state 有个 posix
接口,模拟 Linux/Unix 程序运行时的 I/O。
posix.dumps(fd)
的作用是:
把指定文件描述符(fd)的内容“dump”出来,返回字节串。
常见文件描述符号:
0
→ 标准输入 stdin
1
→ 标准输出 stdout
2
→ 标准错误 stderr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import angrproj = angr.Project('./angr' , auto_load_libs=False ) simgr = proj.factory.simgr() def should_avoid (state ): if b"Try again" in state.posix.dumps(1 ): return True if state.addr == 0x8049243 : return True return False simgr.explore(find = lambda s1: b"Good Job." in s1.posix.dumps(1 ), avoid = should_avoid) s = simgr.found[0 ] flag = s.posix.dumps(0 ) print (flag)
也能解,但是should_avoid会有额外开销
02_angr_find_condition 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import angrp = angr.Project('./dist/02_angr_find_condition' ) init_state = p.factory.entry_state() simgr = p.factory.simgr(init_state) def good (state ): tag = b'Good' in state.posix.dumps(1 ) return True if tag else False def bad (state ): tag = b'Try' in state.posix.dumps(1 ) return True if tag else False simgr.explore(find=good, avoid=bad) if simgr.found: solution = simgr.found[0 ] print (solution.posix.dumps(0 )) else : raise Exception("Could not find solution" )
Symbolic 学习怎么在寄存器,栈,堆,文件等处注入符号变量。
03_angr_symbolic_registers
里面有三个复杂功能
用IDA打开程序,get_user_input把三个输入分别放入寄存器eax、ebx、edx。我们需要跳过输入这一步,直接让Angr把用符号向量来代替输入字符串。因此,我们需要改变程序入口,直接跳转到参数入栈的位置,然后新建三个符号向量,并把三个符号向量分别放到寄存器eax、ebx、edx。
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 import angrimport claripyimport sysproject = angr.Project('./dist/03_angr_symbolic_registers' ) start_address = 0x08048980 initial_state = project.factory.blank_state(addr=start_address) password0 = claripy.BVS('password0' , 32 ) password1 = claripy.BVS('password1' , 32 ) password2 = claripy.BVS('password2' , 32 ) initial_state.regs.eax = password0 initial_state.regs.ebx = password1 initial_state.regs.edx = password2 simulation = project.factory.simgr(initial_state) def is_successful (state ): stdout_output = state.posix.dumps(sys.stdout.fileno()) return b'Good Job.' in stdout_output def should_abort (state ): stdout_output = state.posix.dumps(sys.stdout.fileno()) return b'Try again.' in stdout_output simulation.explore(find=is_successful, avoid=should_abort) if simulation.found: solution_state = simulation.found[0 ] solution0 = solution_state.solver.eval (password0) solution1 = solution_state.solver.eval (password1) solution2 = solution_state.solver.eval (password2) solution = "Solutions:{:x} {:x} {:x}" .format (solution0, solution1, solution2) print (solution) else : raise Exception('Could not find the solution' )
1 2 3 def should_abort(state): stdout_output = state.posix.dumps(1) return b'Try again.' in stdout_output
这样也行
04_angr_symbolic_stack 当符号值位于栈上时,需要提前做好栈布局,再将符号值放到栈上(push或直接内存赋值)。
state.stack_push(thing)
输入后栈为这样的,一开始esp和ebp是同一位置
esp只需要抬高0x8即可输入,上面那张图截图的时机有点问题,esp在0x17下面时更好理解(也就是未输入时)
05_angr_symbolic_memory 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 import angrimport sysp = angr.Project('./dist/05_angr_symbolic_memory' ) start_addr = 0x08048601 init_state = p.factory.blank_state(addr = start_addr) p1 = init_state.solver.BVS('p1' , 64 ) p2 = init_state.solver.BVS('p2' , 64 ) p3 = init_state.solver.BVS('p3' , 64 ) p4 = init_state.solver.BVS('p4' , 64 ) p1_addr = 0x0a1ba1c0 p2_addr = 0x0a1ba1c8 p3_addr = 0x0a1ba1d0 p4_addr = 0x0a1ba1d8 init_state.memory.store(p1_addr, p1) init_state.memory.store(p2_addr, p2) init_state.memory.store(p3_addr, p3) init_state.memory.store(p4_addr, p4) sm = p.factory.simgr(init_state) def is_good (state ): return b"Good Job" in state.posix.dumps(1 ) def is_bad (state ): return b"Try again" in state.posix.dumps(1 ) sm.explore(find=is_good, avoid=is_bad) if sm.found: found_state = sm.found[0 ] pass1 = found_state.solver.eval (p1, cast_to=bytes ) pass2 = found_state.solver.eval (p2, cast_to=bytes ) pass3 = found_state.solver.eval (p3, cast_to=bytes ) pass4 = found_state.solver.eval (p4, cast_to=bytes ) print ("Solution: {} {} {} {}" .format (pass1.decode("utf-8" ), pass2.decode("utf-8" ), pass3.decode("utf-8" ), pass4.decode("utf-8" ))) else : Exception("Solution not found" )
06_angr_symbolic_dynamic_memory
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 import angrimport sysimport claripyproject = angr.Project('./dist/06_angr_symbolic_dynamic_memory' ) initial_state = project.factory.blank_state(addr=0x8048699 ) arg1 = claripy.BVS('arg1' , 64 ) arg2 = claripy.BVS('arg2' , 64 ) addr1 = 0xABCC8A4 addr2 = 0xABCC8AC heap_ptr1 = 0x212340 heap_ptr2 = 0x312350 initial_state.memory.store(addr1, heap_ptr1, endness = 'LE' ) initial_state.memory.store(addr2, heap_ptr2, endness = 'LE' ) initial_state.memory.store(heap_ptr1, arg1) initial_state.memory.store(heap_ptr2, arg2) simgr = project.factory.simulation_manager(initial_state) def right (state ): if b'Good' in state.posix.dumps(1 ): return True else : return False def wrong (state ): if b'Try' in state.posix.dumps(1 ): return True else : return False simgr.explore(find=right, avoid=wrong) if simgr.found: solution_state = simgr.found[0 ] print (solution_state.solver.eval (arg1, cast_to=bytes )) print (solution_state.solver.eval (arg2, cast_to=bytes ))
07_angr_symbolic_file 通过angr.storage.SimFile
和state.fs.insert
来插入符号化文件。
读取输入通过ignore_me函数存储入OJKSQYDP.txt中,后续再通过从OJKSQYDP.txt中取出进行校验
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 import angrimport claripyimport sysio = angr.Project('./dist/07_angr_symbolic_file' ,auto_load_libs=False ) state_addr = 0x80488E7 init_state = io.factory.blank_state(addr = state_addr) passwd0 = claripy.BVS('passwd0' ,64 *8 ) file_name = 'OJKSQYDP.txt' simfile = angr.storage.SimFile(name = file_name,content = passwd0,size = 64 ) init_state.fs.insert(file_name,simfile) simgr = io.factory.simgr(init_state) def is_succ (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Good Job.' in std_out: return True else : return False def is_fail (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Try again.' in std_out: return True else : return False simgr.explore(find=is_succ,avoid=is_fail) if simgr.found: so_state = simgr.found[0 ] print (so_state.solver.eval (passwd0,cast_to=bytes ))
Hook 开始学怎么避免路径爆炸了,其实个人感觉hook,simProcedure,手动约束的思想都差不多。
08_angr_constraints
但是与之前直接使用strcmp校验不同,这里使用的是一个自定义的按位校验,并且由于输入是16位,就将会进行16次的循环,每次循环都将经历一次if判断
将会产生2^16 == 65536个判断分支,这么多的分支,就将会引发一个叫路径爆炸的问题,严重影响我们测试的效率
为此,我们可以自己去实现一个校验约束,直接跳过或者也可以理解为hook掉这个按位校验函数,这样就不会产生路径爆炸了
你可能会有疑问,strcmp函数在底层实现也是按位比较,为什么在前面的题目中并没有提及路径爆炸问题
原因是angr在对于strcmp这种标准库自己实现了一套hook,使用了angr实现的strcmp去替换掉了标准库中调用的strcmp函数,避免了路径爆炸,这在下文中也有提及
参考:通过Angr_CTF入门Angr | Closure
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 import angrimport claripyimport sysio = angr.Project('./dist/08_angr_constraints' ,auto_load_libs=False ) state_addr = 0x8048622 init_state = io.factory.blank_state(addr = state_addr) passwd0 = claripy.BVS('passwd0' ,16 *8 ) buffer_addr = 0x804A050 init_state.memory.store(buffer_addr,passwd0) simgr = io.factory.simgr(init_state) check_addr = 0x8048565 simgr.explore(find=check_addr) def is_succ (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Good Job.' in std_out: return True else : return False def is_fail (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Try again.' in std_out: return True else : return False if simgr.found: so_state = simgr.found[0 ] buffer_cu = so_state.memory.load(buffer_addr,16 ) key = "AUPDNNPROEZRJWKB" so_state.solver.add(buffer_cu == key) so0 = so_state.solver.eval (passwd0,cast_to=bytes ) print (format (so0.decode('utf-8' )))
09_angr_hooks
程序将获取两次输入,第一次输入经过complex_function处理后,再通过check_equals_XYMKBKUHNIQYNQXE与password进行比较;第二次输入将与经过complex_function处理后的password进行比较;并且可以看到在check_equals_XYMKBKUHNIQYNQXE中使用按位比较,将会出现路径爆炸问题
跟上题的不同之处是:这道题在到达了check地址之后还会执行,使用hook来做更方便一些。
指令长度为5
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 import angrimport claripyimport sysio = angr.Project('./dist/09_angr_hooks' , auto_load_libs=False ) init_state = io.factory.entry_state() check_addr = 0x80486B3 call_check_len = 5 @io.hook(check_addr, length=call_check_len ) def hook_check (state ): buffer_addr = 0x804A054 buffer = state.memory.load(buffer_addr, 16 ) key = "XYMKBKUHNIQYNQXE" state.regs.eax = claripy.If( buffer == key, claripy.BVV(1 , 32 ), claripy.BVV(0 , 32 ) ) def is_succ (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Good Job.' in std_out: return True else : return False def is_fail (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Try again.' in std_out: return True else : return False simgr = io.factory.simgr(init_state) simgr.explore(find=is_succ, avoid=is_fail) if simgr.found: so_state = simgr.found[0 ] so0 = so_state.posix.dumps(0 ) print (format (so0.decode('utf-8' )))
10_angr_simprocedures 与上一题类似,但是本题的check函数被多次调用,可以使用函数名进行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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import angrimport claripyimport sysio = angr.Project('./dist/10_angr_simprocedures' ,auto_load_libs=False ) init_state = io.factory.entry_state() class Hook (angr.SimProcedure): def run (self,a1,a2 ): buffer_addr = a1 buffer_len = a2 buffer = self .state.memory.load( buffer_addr, buffer_len ) key = "ORSDDWXHZURJRBDH" return claripy.If( buffer == key, claripy.BVV(1 ,32 ), claripy.BVV(0 ,32 ) ) def is_succ (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Good Job.' in std_out: return True else : return False def is_fail (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Try again.' in std_out: return True else : return False check_sym = "check_equals_ORSDDWXHZURJRBDH" io.hook_symbol(check_sym,Hook()) simgr = io.factory.simgr(init_state) simgr.explore(find = is_succ,avoid=is_fail) if simgr.found: so_state = simgr.found[0 ] so0 = so_state.posix.dumps(0 ) print (format (so0.decode('utf-8' )))
11_angr_sim_scanf
分段校验,对__isoc99_scanf进行hook
hook scanf函数来应对复杂格式的输入,向scanf的参数中存入内容,并且将值存到 globals 全局变量插件中
为什么要 hook scanf
scanf
会做格式化解析(十进制字符串 → 二进制整数) ,如果不钩住,angr 要么模拟 scanf
的实现(复杂、慢),要么你需要把 stdin 做成符号并让 scanf
自己解析出整数,这会把“解析逻辑”也引入符号约束中,显著增加复杂度与求解难度。
更直接的语义建模 :题里 scanf("%u %u", buffer0, buffer1)
的效果是“把两个 32-bit 无符号整数写到内存”。我们只关心写入后的内存值被 strncmp
比较这一事实,hook 可以直接创建两个 32-bit 的符号位向量(BVS)并写入 buffer0/1
地址 ,使后续的 strncmp
约束直接作用在这些符号上,干净利落。
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 54 55 56 57 58 59 60 import angrimport claripyimport sysio = angr.Project('./dist/11_angr_sim_scanf' ,auto_load_libs=False ) init_state = io.factory.entry_state() class Hook (angr.SimProcedure): def run (self,format_string,buffer0_addr,buffer1_addr ): scanf0 = claripy.BVS('scanf0' ,32 ) scanf1 = claripy.BVS('scanf1' ,32 ) self .state.memory.store( buffer0_addr, scanf0, endness = io.arch.memory_endness ) self .state.memory.store( buffer1_addr, scanf1, endness = io.arch.memory_endness ) self .state.globals ['so0' ] = scanf0 self .state.globals ['so1' ] = scanf1 def is_succ (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Good Job.' in std_out: return True else : return False def is_fail (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Try again.' in std_out: return True else : return False scanf_sym = "__isoc99_scanf" io.hook_symbol(scanf_sym,Hook()) simgr = io.factory.simgr(init_state) simgr.explore(find = is_succ,avoid=is_fail) if simgr.found: so_state = simgr.found[0 ] so0 = so_state.globals ['so0' ] so1 = so_state.globals ['so1' ] scanf0_so = so_state.solver.eval (so0) scanf1_so = so_state.solver.eval (so1) print (format (scanf0_so)) print (format (scanf1_so))
Veritesting 12_angr_veritesting
程序将进行一个按位的加密,这里将会出现路径爆炸,angr提供了veritesting去避免路径爆炸,只需启用即可
符号执行,一种是动态符号执行(Dynamic Symbolic Execution,简称DSE),另一种是静态符号执行(Static Symbolic Execution,简称SSE)。
动态符号执行会去执行程序然后为每一条路径生成一个表达式。在生成表达式上引入了很多的开销,然而生成的表达式很容易求解。
而静态符号执行将程序转换为表达式,每个表达式都表示任意条路径的属性生成表达式容易,但是表达式难求解。
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 import angrimport claripyimport sysio = angr.Project('./dist/12_angr_veritesting' , auto_load_libs=False ) init_state = io.factory.entry_state() def is_succ (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Good Job.' in std_out: return True else : return False def is_fail (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Try again.' in std_out: return True else : return False simgr = io.factory.simgr(init_state, veritesting=True ) simgr.explore(find=is_succ, avoid=is_fail) if simgr.found: so_state = simgr.found[0 ] so0 = so_state.posix.dumps(0 ) print (format (so0.decode('utf-8' )))
Library 13_angr_static_binary 和00_angr_find唯一的区别是二进制文件被编译为静态二进制文件,我们主动替换库函数避免路径爆炸和加速。
angr提供了写好的SimProcdure,我们直接索引到对应的函数然后hook就行。
就像本来strcmp的实现也是按位比较,但是在前面为什么不会在strcmp上发生路径爆炸,因为angr已经自动给这些函数hook了
但是用静态编译,angr没法自动hook,需要手动去hook在使用的标准库的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 25 26 27 28 29 30 31 32 33 34 import angrimport claripyimport sysio = angr.Project('/home/closure/Desktop/CTF/angr_ctf/dist/13_angr_static_binary' ,auto_load_libs=False ) init_state = io.factory.entry_state() def is_succ (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Good Job.' in std_out: return True else : return False def is_fail (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Try again.' in std_out: return True else : return False io.hook(0x804ed80 , angr.SIM_PROCEDURES['libc' ]['scanf' ]()) io.hook(0x804ed40 , angr.SIM_PROCEDURES['libc' ]['printf' ]()) io.hook(0x804f350 , angr.SIM_PROCEDURES['libc' ]['puts' ]()) io.hook(0x8048280 , angr.SIM_PROCEDURES['libc' ]['strcmp' ]()) io.hook_symbol('__libc_start_main' ,angr.SIM_PROCEDURES['glibc' ]['__libc_start_main' ]()) simgr = io.factory.simgr(init_state) simgr.explore(find = is_succ,avoid=is_fail) if simgr.found: so_state = simgr.found[0 ] so0 = so_state.posix.dumps(0 ) print (format (so0.decode('utf-8' )))
14_angr_shared_library
validate来自动态链接库lib14_angr_shared_library.so
我们指定下共享库的基地址加上偏移就能定位到validate。这里用到一个新的内置state:call_state
.blank_state():空白状态,其大部分数据未初始化。 访问未初始化的数据时,将返回一个不受约束的符号值。
.entry_state() :构造一个准备在主二进制文件入口点执行的状态。
.full_init_state():通过需要在主二进制文件入口点之前运行的任何初始化程序构造一个准备执行的状态
.call_state():构造一个准备好执行给定函数的状态。
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 import angrimport claripyimport sysdef is_succ (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Good Job.' in std_out: return True else : return False def is_fail (state ): std_out = state.posix.dumps(sys.stdout.fileno()) if b'Try again.' in std_out: return True else : return False libc_so = '/home/closure/Desktop/CTF/angr_ctf/dist/lib14_angr_shared_library.so' libc_base = 0x8048000 io = angr.Project(libc_so,load_options={ 'main_opts' :{ 'custom_base_addr' :libc_base } }) validate_addr = libc_base + 0x6D7 buffer_pointer = claripy.BVV(0x3000000 , 32 ) init_state = io.factory.call_state(validate_addr, buffer_pointer, claripy.BVV(8 , 32 )) password = claripy.BVS('password' , 8 *8 ) init_state.memory.store(buffer_pointer, password) simgr = io.factory.simgr(init_state) success_address = libc_base + 0x783 simgr.explore(find=success_address) if simgr.found: so_state = simgr.found[0 ] so_state.add_constraints(so_state.regs.eax == 1 ) so0 = so_state.solver.eval (password,cast_to=bytes ) print (format (so0.decode('utf-8' )))
Overflow 15_angr_arbitrary_read 程序通过scanf获取输入,第一个key经过校验后将使用puts输出,但是都为try_again,但是一个s(try_again)在栈上,而我们的第二个输入也将写入栈上,并且输入长度并没有限制,也就是说可以覆盖s为Good Job.
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 import angrimport claripyimport sysio = angr.Project('./dist/15_angr_arbitrary_read' , auto_load_libs=False ) init_state = io.factory.entry_state() class Hook (angr.SimProcedure): def run (self, format_string, key_addr, password_addr ): key_bvs = claripy.BVS('key_bvs' , 32 ) password_addr_bvs = claripy.BVS('password_addr_bvs' , 20 * 8 ) for char in password_addr_bvs.chop(bits=8 ): self .state.add_constraints(char >= 'A' , char <= 'Z' ) self .state.memory.store( key_addr, key_bvs, endness=io.arch.memory_endness ) self .state.memory.store( password_addr, password_addr_bvs ) self .state.globals ['solutions' ] = (key_bvs, password_addr_bvs) scanf_sym = "__isoc99_scanf" io.hook_symbol(scanf_sym, Hook()) simgr = io.factory.simgr(init_state) def success (state ): jmp_puts_addr = 0x8048370 if state.addr != jmp_puts_addr: return False goodjob_addr = 0x484F4A47 puts_param = state.memory.load(state.regs.esp + 4 , 4 , endness=io.arch.memory_endness) if state.se.symbolic(puts_param): cp_state = state.copy() cp_state.add_constraints(puts_param == goodjob_addr) if cp_state.satisfiable(): state.add_constraints(puts_param == goodjob_addr) return True else : return False else : return False simgr.explore(find=success) if simgr.found: so_state = simgr.found[0 ] (key_so, password_so) = so_state.globals ['solutions' ] so0 = so_state.solver.eval (key_so) so1 = so_state.solver.eval (password_so, cast_to=bytes ) print (so0, so1)
16_angr_arbitrary_write 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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import angrimport claripyimport sysio = angr.Project('/home/closure/Desktop/CTF/angr_ctf/dist/16_angr_arbitrary_write' ,auto_load_libs=False ) init_state = io.factory.entry_state() class Hook (angr.SimProcedure): def run (self,format_string,key_addr,password_addr ): key_bvs = claripy.BVS('key_bvs' ,32 ) password_addr_bvs = claripy.BVS('password_addr_bvs' ,20 *8 ) for char in password_addr_bvs.chop(bits=8 ): self .state.add_constraints(char >= 'A' , char <= 'Z' ) self .state.memory.store( key_addr, key_bvs, endness = io.arch.memory_endness ) self .state.memory.store( password_addr, password_addr_bvs ) self .state.globals ['solutions' ] = (key_bvs, password_addr_bvs) def success (state ): strncpy_addr = 0x8048410 if state.addr == strncpy_addr: return check_strncpy(state) else : return False def check_strncpy (state ): strncpy_dest = state.memory.load(state.regs.esp+4 , 4 , endness = io.arch.memory_endness) strncpy_src_addr = state.memory.load(state.regs.esp+8 , 4 , endness = io.arch.memory_endness) strncpy_len = state.memory.load(state.regs.esp+12 , 4 , endness = io.arch.memory_endness) src_contents = state.memory.load(strncpy_src_addr,strncpy_len) if state.solver.symbolic(src_contents) and state.solver.symbolic(strncpy_dest): password_str = "NDYNWEUJ" buffer_addr = 0x57584344 check_content_password = src_contents[-1 :-64 ] == password_str check_dest_buffer_addr = strncpy_dest == buffer_addr if state.satisfiable(extra_constraints = (check_content_password,check_dest_buffer_addr)): state.add_constraints(check_content_password,check_dest_buffer_addr) return True else : return False else : return False scanf_sym = "__isoc99_scanf" io.hook_symbol(scanf_sym,Hook()) simgr = io.factory.simgr(init_state) simgr.explore(find = success) if simgr.found: so_state = simgr.found[0 ] (key_so,password_so) = so_state.globals ['solutions' ] so0 = so_state.solver.eval (key_so) so1 = so_state.solver.eval (password_so,cast_to=bytes ) print (so0,so1)
17_angr_arbitrary_jump 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 54 55 56 import angrimport claripyimport sysio = angr.Project('/home/closure/Desktop/CTF/angr_ctf/dist/17_angr_arbitrary_jump' ,auto_load_libs=False ) init_state = io.factory.entry_state() class Hook (angr.SimProcedure): def run (self,format_string,scanf_input ): scanf_input_bvs = claripy.BVS('scanf_input' ,200 *8 ) for char in scanf_input_bvs.chop(bits=8 ): self .state.add_constraints(char >= 'A' , char <= 'Z' ) self .state.memory.store( scanf_input, scanf_input_bvs ) self .state.globals ['scanf_input_bvs' ] = scanf_input_bvs scanf_sym = "__isoc99_scanf" io.hook_symbol(scanf_sym,Hook()) simgr = io.factory.simgr(init_state, save_unconstrained=True , stashes={ 'active' : [init_state], 'unconstrained' : [], 'found' : [], }) while (simgr.active or simgr.unconstrained) and (not simgr.found): for unconstrained_state in simgr.unconstrained: def should_move (s ): return s is unconstrained_state simgr.move(from_stash='unconstrained' , to_stash='found' , filter_func=should_move) simgr.step() if simgr.found: so_state = simgr.found[0 ] print_goodjob = 0x42585249 so_state.add_constraints(so_state.regs.eip == print_goodjob) scanf_input_bvs_so = so_state.globals ['scanf_input_bvs' ] so0 = so_state.solver