本文来自CCS 2019,SLAKE- Facilitating Slab Manipulation for Exploiting Vulnerabilities in the Linux Kernel。源码见https://github.com/chenyueqi/SLAKE
摘要:
目标:通过操控slab布局进行提权。
难点:一是不知道哪个对象和系统调用有利于利用;二是不知道如何操控slab以获得预期布局。
方法:先用静态/动态分析技术探索有助于利用的内核对象和系统调用,对常用的利用方法建模,研究出一种方法来调整slab布局。
实现:工具SLAKE,扩展LLVM和Syzkaller。
实验:用27个真实内核漏洞,不仅能丰富利用方法而且能增大内核漏洞可利用性,不仅找到了所有常用的布置slab的系统调用,还找到了不常用的。
1.Introduction
研究原因:内核漏洞危害很大,但人力有限,来不及及时修补漏洞,一般根据可利用性来确定优先级,进行修补,所以要研究。
废话:利用方法是操纵slab布局,以劫持控制流并提权,难点已说明。已有的文章如[15]是关于自动布置堆结构,难点是系统太过复杂。
本文方法:SLAKE(SLAB manipulation for Kernel Exploitation)。步骤方法和实验结果已说明。
贡献:
a.设计一种新方法,用静态/动态分析去识别有助于利用的内核对象和系统调用;
b.建模已有的利用方法,并设计方法以获得预期slab布局;
c.扩展LLVM和Syzkaller实现SLAKE,并在27个内核漏洞上验证有效性。
2.背景和挑战
2.1 问题、假设、目标
问题:许多内核漏洞最后都要利用SLAB/SLUB分配器,但目前没有一个通用、系统的方法来促进内核漏洞布局。
假设:a. 只能利用PoC的能力,eg,PoC只能触发覆写1字节,而漏洞本身可以任意写,则分析者只能写1字节;b. 可利用性 == 程序计数器是否可控(只要能劫持控制流,就很容易绕过保护并提权)。
目标:a. 识别有助于利用的对象和相关系统调用;b. 构造预期的内存布局。
2.2 技术背景
说明:
- victim object —— syscall产生的含函数指针的对象 VO
- spray object —— syscall产生的可布置数据的喷射对象 SO
- vulnerable object —— 漏洞代码中的对象 VULO
SLAB/SLUB分配器:同一cache中的对象大小相同,后进先出LIFO。SLUB中,每个空闲槽(slab)都有个meta-data header指向下一个空闲槽,组成一条单链和假的head freelist(存单链表的表头,非数组);而SLAB则用freelist索引数组来存对象(非链表)。参见SLUB和SLAB的区别。
内核利用方法:
总体步骤是,先确定漏洞类型和可控内存,再劫持控制流,最后关闭内核保护并利用。见Figure 1。
- OOB(out-of-bounds write):a. 覆盖相邻对象中的函数指针;b. 覆盖相邻对象中的函数表指针(指向physmap[20,37]或用户空间,伪造函数表)。
- UAF:使可控的spray object和vulnerable object重叠,通过spray object修改vulnerable object上的关键函数指针或函数表指针。
- Double-Free:两次释放同一个块,VULO的metadata头将指向自身。先选取和VULO等大的VO;再申请VO;最后申请SO来修改VO中的关键函数指针或函数表指针。
- 伪造metadata头:以上3类漏洞都能够达到篡改metadata头的目的,使SLUB将VO分配到可控的内存区域,很容易篡改函数指针或函数表指针。
2.3 挑战
a. 选取被覆盖对象VO(必须含函数指针,且大小合适)
b. 如何利用syscall分配到目标对象,并调用目标对象上的函数指针。
c. 如何调整syscall,以获得预期的slab布局,避免分配无关的内核对象。
3. 对象&syscall识别——挑战a/b
主要针对挑战a/b,直观思路是,fuzz测试syscall,观察记录slab和函数指针引用。难点,一是内核代码太多,很难用常规testcase找到所有对象的申请/释放和函数指针调用,二是syscall太多,各个syscall参数不同,导致fuzz测试效率低。解决办法,先静态分析找到感兴趣的程序点(对象申请/释放和函数指针调用),记录对象类型、size、cache、关键函数指针在对象(VO)中的偏移;再分析内核调用图的可达性,以此引导内核fuzz只fuzz能够到达兴趣程序点的syscall,记录能够到达兴趣点的syscall和相应参数。
3.1 静态分析——识别对象和兴趣点
如何识别关键对象,如何记录关键对象,如何识别通过VO引用函数指针的程序点。
(1)关键内核对象
a. victim object——针对OOB、Double-Free
特征:包含函数指针或函数表指针,通过和DF漏洞的VULO重叠或分配到OOB漏洞的VULO相邻,覆盖VO的函数指针来劫持控制流。
检测方法:检查对象是否含函数指针,如果是结构指针,则递归检测。
注意:不要分析链表,以免陷入死循环。
b. spray object——针对UAF、Double-Free
特征:能申请内核到空闲对象且被悬垂指针所引用,并布置数据。
检测方法:判断copy_from_user()
的dst参数是否为堆分配函数(如kmalloc()、kmem_cache_alloc())的返回值。
(2)关键对象的分配/释放点识别
目标:目标是识别能够分配VO或SO的分配/释放点。
分配点:对分配函数(如kmalloc)的返回值,根据其def-use链,检查返回值是否属于某类关键对象的指针,属于则为感兴趣的分配点。
释放点:对释放函数(如kfree),检查传入的指针是否属于某类关键对象的指针。
(3)函数指针引用点(VO)——Dummy dereference sites
难点:a. 过程间数据流分析不准确,很难构造准确的CFG;b. 内核的软中断机制(如Read-Copy Update RCU)会释放对象,因而异步调用函数指针(比较隐蔽)。
解决:先用静态分析识别引用点,再用fuzz和动态数据流分析追踪真正的引用点和相应的syscall。对于RCU引用,引用点就是释放函数(kfree_call_rcu()和call_rcu_sched()),它们会释放VO并异步调用VO中的函数指针;对于非RCU引用,则比较明显,若调用VO中的函数指针则为引用点。
3.2 Fuzz——搜索syscall
目标:通过内核fuzz,找到能分配目标对象和引用函数指针的syscall和相应参数。也即如何到达该路径
问题:传统kernel fuzz只能找到申请/释放和引用函数指针的这些点,但是并不能确定释放的是否是指定的VO、引用的指针是否来自于指定的VO。
解决:基于上下文的fuzzing和动态数据流分析。
(1)Panic Anchors——插桩
目的:在目标点(申请、释放、引用点)插桩,如果fuzz到目标点,则终止并记录syscall和参数。
问题:误识,其他进程的异常信号、设备的中断信号、内核线程、用户进程也可能执行到目标点。
解决:Panic Anchors 负责 a. 检查内核变量system_state == SYSTEM_RUNNING
,表示正执行用户调用的syscall;b. 检查task_struct
结构中的comm值,显示是否为用户调用的syscall。 并记录分配对象的地址。
// comm值可由用户进程指定
// kernel代码的打印信息中常可看到使用该成员表示相关进程信息,例如
printk(KERN_INFO "note: %s[%d] exited\n",
current->comm, task_pid_nr(current));
// 使用函数set_task_comm设置该成员的值,使用函数get_task_comm获取该成员的值。
char *get_task_comm(char *buf, struct task_struct *tsk)
void set_task_comm(struct task_struct *tsk, char *buf)
(2)syscall识别
目的:减少fuzz的syscall目标数,提高效率。
方法:构造内核call graph,对目标点进行可达性分析(可达且不需要特权执行CAP_SYS_ADMIN),只fuzz可达的syscall。
记录:
- a. 分配点:执行到syscall的分配点,则记录相应参数和分配的slab,有利于布局slab和基于上下文的fuzzing。存储database细节见Appendix。
- b. 释放点:先执行syscall分配对象并记录,再fuzz到释放点,检查释放对象是否为之前的分配对象,记录syscall和相应参数。
- c. 引用点:先执行syscall分配对象并记录,再fuzz到引用点,检查引用的函数指针是否位于之前的分配对象VO中(方法是,当执行到引用点,则继续执行直到退出当前函数,记录整个的执行路径,以此构造use-define chain,根据use-define chain来判断函数指针是否来自VO),记录syscall和相应参数。
4. slab布局——挑战c
方法:搜索以上database,找到能匹配漏洞能力的可用对象,使用slab布局方法获得有助于利用的slab布局。
4.1 匹配漏洞能力和可用的对象
(1)漏洞和对象建模
Ar[m]——表示漏洞能力,即m个用户可控的不重叠的内存对象(VO/SO),元素为(l, h)—起始与终止偏移。
Ap[n]——n个关键数据(函数指针/metadata)的偏移。
w——指针的size,4或8。
(2)配对漏洞和可用对象
具体方法的描述见Appendix。
- a. OOB:一是覆盖相邻空闲对象的metadata,并将VO分配到可控区域,要求是VO与VULO属于同一cache;二是申请VO与VULO相邻,再利用漏洞覆盖VO,要求是VO与VULO属于同一cache,且能覆盖到关键函数指针(通过检查Ar[m]和Ap[n]是否重叠)。
- b. UAF:一是修改metadata,并将VO分配到可控区域,要求是VO与VULO属于同一cache;二是使SO与VULO重叠,通过SO修改VULO上的关键函数指针来劫持控制流,要求是SO与VULO属于同一cache,且能覆盖到关键函数指针(通过检查Ar[m]和Ap[n]是否重叠)。
- c. Double-Free:一是使VO和SO重叠,修改VO中的关键指针,要求是VO/SO/VULO在同一cache,且SO与Ap[n]中函数指针重叠;二是使VULO和SO重叠,修改VULO中的metadata,要求是VULO和SO在同一cache,且SO和VULO中的metadata重叠。
4.2 slab布局
问题:很容易根据database选取合适的object,但是可能还会分配/释放其他的数据对象,不能获得预期的slab布局。
(1)调整空闲槽——目标槽空闲
目标:将对象分配到目标空闲槽,eg,构造重叠的SO和VULO。
方法:a. 列出 free list chain上所有连续的空闲槽并编号(在LIFO链表中,从左到右)。b. 假定实际PoC中的syscall将关键对象分配到第i个,但是目标位置是j,若i
注意:a. 所选的syscall在申请/释放对象要没有副作用;b. 触发漏洞和slab布局之前要进行defragmentation[31,48] 反碎片化,避免网络连接、文件打开、页映射等无关操作对slab布局的影响。
(2)重组空闲槽——目标槽被占
目标:对于OOB漏洞,要使VULOi-1和VOi相邻,需进行slab重排。见Figure4。
步骤:a. 扩展PoC,在Syscallvulo后插入Syscallvo;b. 用ftrace插桩PoC,记录分配/释放的对象,假定总共申请了K个内核对象,VO实际在jth槽,预期在ith槽;c. 重排序步骤,先去碎片化,然后分配对象占据K个空闲槽,再逆序释放(交换第i和j个对象的释放顺序),最后按原先顺序执行,即可使VULO与VO相邻。
5. 实现
(1)静态分析
准备:glove[18]将整个系统编译为LLVM IR。
实现:两个LLVM pass,基于LLVM6.0,总共2000行C++代码。
a. 识别对象和目标点:利用LLVM IR中的类型信息来追踪VO;利用调用内核I/O函数的CallInst(如copy_from_user())来识别SO。对于识别到的对象,记录其申请/释放点和所在的cache name,若对象包含函数指针,则识别其引用。
b. 构建调用图:改进KINT[43](利用静态域敏感的过程间污点分析,构建调用图)。采用两种剪枝方法,一是去掉和.init.text
节中的函数有关的节点和边(系统启动后不会再调用);二是去掉不相关模块之间的bridging edges(通过KConfig文件中的标签"depends -> on"、"select",表明模块是相关的)。
(2)动态分析
实现:(a+b)—700行C++;c—200行python;d—400行C。
分析:由于对对象的释放和引用要和之前申请的相关联,所以KCOV[49]不适合用于本文的插桩。
a. GCC插件-插桩:用fuzz找到执行到目标点的路径,以识别能走到分配/释放点和函数指针引用点的syscall。
b. 扩展Syzkaller和Moonshine:另外增加了一些fuzzing模板。
c. GDB python脚本:目的是确定函数指针引用是通过VO引用的。在QEMU上运行内核,在函数指针引用点下断点,只要断下来,就单步执行,记录从断点到当前函数执行结束的路径,构造use-def chain,进行前向数据流分析,检查chain上每个函数指针引用,确定是从相应的VO引用的。
d. ftrace工具:对syzkaller产生的程序进行插桩,记录每个syscall的slab活动(记录syscall副作用)。
6. 实验
6.1 环境配置
内核版本:v4.15,对core kernel(defnoconfig定义的)和32和通用模块进行静态分析,建立database。fuzz时的编译选项加上KCOV。
fuzz时间:每个目标点fuzz 2h。
环境:3个VM,配置Intel(R) Xeon(R) CPU E5-2650 v3 @ 2.30GHZ CPU and 64GB memory
。
CVE选择标准:有公开PoC / 移植到v4.15很方便 / 触发条件不涉及硬件。共27个。
6.2 syscall识别评估
(1)比较调用图构造
对比:改进的KINT[43] vs 原型匹配方法[50]。结果见Table 1。
结果:a.排除通过syscall不可达的VO/SO对象数量,"# of v/s"—124/4 --> 104/4;b. 排除目标点不可达的syscall,"avg. syscall #"— 257 --> 68,减少动态分析的时间("avg. time" 34min --> 2min)。
注意:fuzz时间是2h,SLAKE发现目标点不可达并不意味着真的不可达,而是在2h内fuzz不到目标点。
(2)比较不同模块
a. 有些模块找不到有用的object;
b. SO数量少于VO,只有3个模块含有效SO,原因是内核一般将用户数据存于栈而非堆中。
(3)比较SLAKE和手动分析
手动分析syscall很难,换个角度,对比已有的exp中用到的syscall(10个),SLAKE全发现了,还发现额外22个含可用object的syscall。找到含VO/SO的syscall见Table 2。
6.3 exp生成
(1)对比已公布的和SLAKE生成的exp
能够极大丰富内核漏洞的利用方式,对未公布exp的漏洞,SLAKE能找到合适的object,提升可利用性。结果见Table3。
(2)失败案例分析
a. CVE-2017-1000112 / CVE-2018-5703:其PoC显示只能覆盖VULO内的数据。
b. CVE-2017-2636 / CVE- 2014-2851 / CVE-2018-17182:没有合适的object可用。
c. CVE-2018-12714 / CVE-2018-16880 / CVE-2017-17052 / CVE-2018-10840:其PoC显示并不能自由写目标内存。
7. 讨论&未来工作
平台扩展——其他OS,如Windows、FreeBSD、Android。
其他分配器——如SLOB、buddy、ptmalloc。
其他对象识别方法——不只是利用copy_from_user
识别SO,还有memcpy
。
其他利用方法——[16,21]。
自动化分析漏洞的能力。