论文来源:CCS-2020-ELOISE-A Systematic Study of Elastic Objects in Kernel Exploitation
摘要
目标:目前有人尝试过利用 elastic objects
来绕过KASLR,但不确定是否适合大多数内核漏洞。
解决:实现原型ELOISE(ExploitabLe Object dIScovEry)——使用静态/动态分析技术来识别elastic object
,再采用Z3约束求解来配对相应的内核漏洞。
实验:在主流系统(Linux\FreeBSD\XNU)上测试,发现elastic object
很普遍,测试40个内核漏洞,能帮助多数漏洞绕过KASLR和heap cookie protector
,甚至实现任意写。最后作者还提出一种防护机制,使用独立的cache来隔离elastic object
,且带来的额外开销可忽略不计。
1.Introduction
贡献:
设计和开发一种系统性的方法,验证
elastic object
利用的危害性。实现工具ELOISE,帮助发现
elastic object
并匹配到对应的漏洞。提出针对
elastic object
的防御机制。
2.background
elastic object
特征:开头一个length域表示elastic buffer
的大小,当内核访问该buffer时,length能控制内核读写的范围。作用——内核开发者可以通过手动控制内存分配,节省内存,通过提高 cache hit rate [64]来提高内核性能。
elastic object
利用示例:xfrm_replay_state_esn
就是典型的elastic object
,包含两个关键字段,一个是缓冲区字段bmp
,一个是长度字段bmp_len
,对应系统调用recvmsg
可以读取的bmp
。利用vul obj
溢出改大bmp_len
,就能泄露与xfrm_replay_state_esn
相邻的对象,例如包含函数指针的ext4_file_operations
结构。有的可以泄露堆栈cookie,如果能覆盖指针,甚至能任意读。
挑战:1.内核得含有elastic object
;2.能够将elastic object
泄露到用户;3. vul object
和eslatic object
在相同的cache/zone,且漏洞能覆盖elastic object
的length区域。
威胁模型:SMEP/SMAP[17] /KPTI [19] /W⊕R;假设内核堆的freelist有随机化(Linux和XNU有,FreeBSD没有),因为[6, 28, 44]已研究过绕过freelist随机化;POC的漏洞能力。
3.技术方法
3.1 识别elastic object
候选
(1)查找struct候选项
首先,结构平坦化,若结构S的成员fi是结构变量,则用fi的成员替换fi;若fi是多维数组,则用一维替代;若fi是union变量,则用新结构{S1, S2, ..., Su }
替代,u是union中不同定义的数量。
然后,只要结构中含整数变量(可能是长度字段),则视为结构候选项。
(2)查找elastic object
的分配点
步骤:分3步,先定位所有的堆内存分配点,再判断分配的对象是否为候选的struct,最后检查到达该分配点是否需要root权限。
定位分配点:两类——使用通用cache/zone(kmalloc, kalloc, malloc);使用特殊cache/zone(kmem-cache, mcache_alloc, uma_zalloc)。
确定分配对象的type:对分配函数的返回值,进行
use-def
分析,追踪和type casting
、pointer dereferencing
、argument passing
相关的指令,这些指令的操作数显示了object的类型。确定路径权限:检查是否存在一条到达分配点的路径,不调用
capable(CAP_SYS_ADMIN)
— Linux、priv_check, priv_check_cred
—FreeBSD、priv_check_cred()
—XNU 这些函数。若需要root权限,则派出相应分配点和候选struct。
(3)记录struct候选项的cache类型
方法:对于特殊cache/zone的函数(kmem-cache, mcache_alloc, uma_zalloc),第1个参数就显示了struct类型,例如kmem_cache_zalloc(seq_file_cache)
分配struct seq_file
类型的对象;对于通用cache/zone的函数(kmalloc, kalloc, malloc),第1个参数是常量或变量加常量,表示chunk大小,即可得知相应的cache/zone大小。
3.2 筛选elastic object
候选项
筛选规则:整数变量表示kernel buffer
的大小;存在一条路径能泄露elastic object
的数据。
方法:总结泄露函数,见 Table 3。泄露函数一般含两个参数,一个长度 n 一个地址 addr。以这两个参数为污点源,进行过程间的前向污点分析,看看长度参数 n 源自哪里。
见Figure 2,从污点源(参数n)开始,反向跟踪数据流,并检查这些参数源自的内存区域,若 n 源自栈变量或全局内存区域,则丢弃该调用点;如果 n 来自内核堆(如 Figure 2 中,n=objB->N
),则保留该调用点,继续反向分析找 addr 参数的污点源(Figure 2 中的 src
),反向跟踪数据流,跟踪所有初始化了相应addr参数的赋值点(例如 src =&buffer
,src =&objA-> buffer
和 src = objA-> p
),根据这些赋值点来分析src的来源——能泄露的目标。addr
的来源有3种:
❶ 若src指向栈内存(
src =&buffer
),如果可以控制堆上的长度变量(n=objB->N
),则可能可以泄露栈canary和返回地址。❷ 若src指向堆对象内部(
src =&objA-> buffer
),则可能可以通过溢出来覆盖长度变量(n=objB->N
),泄露heap cookie
或绕过KASLR。❸ 若src等于堆对象内部的某个指针(
src = objA-> p
),则可能可以任意读写。则需要继续反向污点分析,确认 p 和 N 是否位于同一个object,如果位于同一object,有可能通过堆溢出同时控制 p 和 N,实现任意读。可读中断描述符表(IDT)绕过KASLR,或者dump内存来查找"root:!:"
字符串(该字符串是/etc/shadow
文件的一部分),泄露用户密码hash,再利用 Ripper密码爆破器破解 [43, 56]。
3.3 匹配漏洞和elastic object
(1)准备工作
elastic object
信息:(1)所用的cache/zone类型;(2)object中length所在偏移;(3)若 p 和 N 在同一object,p的偏移;(4)内核分配该object的分配点;(5)泄漏点;(6)泄露能力。
识别漏洞能力:a. GDB 跟踪PoC 触发流程;b.识别vul object
所属的cache类别,总结漏洞覆写能力。例如,见Figure 3,vul obj
属于 kmalloc-128
,能溢出相邻区域的第1个8字节和第3个8字节,第1个8字节要>=128
,第3个8字节要==1024
(可参考[11,12]使vul obj
和elastic object
相邻)。
漏洞能力表示:用2元组表示,[(VCache1, Cap1), ... , (VCachen, Capm)]
,VCachei
—能溢出的cache种类,Capj
—能溢出的范围。若某漏洞能溢出多个区域,则 Capj
用 list — [(Rj1|Opj1|Vj1), ... , (Rjx |Opjx |Vjx )]
表示,Rjk
—被溢出的区域在哪里,Opjk
和 Vjk
— 溢出可控的能力。
示例:见Figure 3,漏洞能力表示为(kmalloc-128, [([0, 8)⩾128), ([8, 16)=1024)])
。
(2)匹配过程
给定漏洞,首先选取cache/zone匹配的 elastic object
;再检查指向elastic buffer
的指针是否和 length 处于同一object(一起覆写,可能可以任意读);最后检查length能否被覆写。例如,length位于第3个8字节,elastic object
位于 kmalloc-192
,漏洞能力 ([16, 24)=1024)
,所以可以将length覆写为1024并访问相邻快,泄露heap cookie
或绕过KASLR。
(3)检测可利用性的策略
a. 对于堆上的elastic buffer
,检查可控制的buffer size是否为所在cache的两倍(确保能读下一整个cache的内容);
b.对于栈上的elastic buffer
,检查可控制的buffer size是否大于elastic buffer
所在的栈帧的大小(确保能泄露canary和返回地址)。
(4)后续利用问题
问题:需避免走不到泄露函数或者内核触发页访问错误。
解决:给定漏洞和对应的elastic object
,获取所有到达泄露函数的路径,见Figure 4。首先,收集路径上的指针引用,然后收集到达泄露函数时必经的分支约束。对于指针引用要确保指向合法地址,对于分支约束要注意在elastic object
中布置的数据和分支变量的数据依赖。
示例:见Figure 4,溢出覆盖了elastic object
中的length-N和相邻区域f,预先识别出两条到达泄露点的路径,筛选出和N、f相关的约束(e.g., C1=obj.f>0
; C2=obj.N<8
),这样该约束(C1&C2)就作为配对过程的额外限制条件。
4.实现
bitcode生成:编译配置是使用defconfig
。由于编译优化和大量使用load/store指令会给别名分析和控制流图构建带来负担,ELOISE使用LLVM自带的WriteBitcodeToFile()
函数,在 mem2reg
pass和 一些负责优化的pass之间调用该函数,以减少load/store指令的使用并构建变量的SSA形式。
控制流图构建与别名分析:借鉴 [47,48] 的两层类型分析pass来构建调用图,并砍掉了和.init.text
节中函数相关的边,接着构建上下文敏感的控制流图(添加3种边——BranchInst
、Callsite
、ReturnInst
);采用LLVM 自带的 AliasAnalysis
pass(别名分析用于确定分配内核object的type并进行前向污点分析)。注意,识别elastic object
时,只有两变量为 must-alias
关系时才将污点变量传播给别名变量(存在欠污染under-tainting问题,但降低了误报)。
识别elastic object
:跟踪内存分配函数,分析返回值,确定对象的type。ELOISE 根据3种治疗来推断类型信息,Getelementpr
—— 其参数包含指向object的指针和该object的类型信息;BitCast
—— 其参数包含指向object的指针,另外两个参数确定了cast之前和之后的类型信息;CallInst
—— 其调用参数可能含有 指向object的指针。
筛选elastic object
:从泄露函数的长度参数开始反向污点分析。问题一,若发生多层引用A->B->len
,B才是真正的elastic object
,所以发生多层引用时只向前污染1个LoadInst
。问题二,区分内存区域(stack / heap / global),内存区域来自AllocaInst
则为栈,内存区域表示为global/static
变量则为global,其他情况则视为堆区域。
收集关键约束:收集到达泄露点的路径中,和elastic object
相关的约束(用户可控制走向)。例如,若elastic object
区域用于 CmpInst
指令,具体是ICMP_ULT
(>
),TRUE
分支能走到泄漏点则保留>
谓词,反之保留=<
。
object和漏洞配对:ELOISE 使用Z3 solver[60] 来配对。具体来说,用 BitVec
表示对象,用算术表示内存值的范围。例如,对于kmalloc-128
中的约束[8, 16)<8
,创建x = BitVec('x', 128*8)
来表示该对象,x << (8*8)>> (112*8)
表示范围 [8, 16)
,(x << (8*8)>> (112*8)< 8)
即表示该约束,最后把约束集交给Z3求解,如果有解则说明漏洞能力和elastic object
相匹配,可以利用。如果某个约束中包含变量(e.g., [0, 8
5.实验评估
5.1 实验设计
(1)评估ELOISE 对elastic object
的识别(误报FP与漏报FN)
检查漏报:人工识别vs ELOISE
,由于内核代码量过大,人工识别困难,故采用随机采样方法。Linux中取800/6383个elastic object
,手动识别分配点、到达泄露函数的路径,再将结果和 ELOISE
对比。
随机取样的科学性:a.分配点取样是随机的,涵盖内核12.5%的分配点;b.Linux/FreeBSD/XNU内核有共性,Linux可以代表另2个内核;c.手动分析时由2个经验丰富的Linux内核研究者完成。
检查误报:syzkaller+手动 vs ELOISE
。对Linux和FreeBSD,首先在 ELOISE
识别出的分配点和释放点插桩 panic 函数,采用syzkaller来fuzz,看是否能生成输入,能到达分配点并释放elastic buffer
中的数据。对于fuzz不可达的点,再进行人工分析确认,仍然不可达的点就是 ELOISE
的误报。对于XNU内核,没有可用的fuzzing工具,只能纯人工分析。
(2)评估绕过防护的能力
防护:KASLR, heap protector, stack canary。
实验:选40个内核漏洞(很有代表性,包含所有类型的堆漏洞,如OOB、UAF、Double-Free;包含所有漏洞利用研究中涉及的堆漏洞类型;包含随机10个szbot [26] 上的漏洞,被syzkaller挖到但还没有CVE编号;包含近三年公布的XNU和FreeBSD漏洞),见Table 7,包含人工总结的漏洞能力。
漏洞选取依据:对FreeBSD和XNU,选取标准是——a.含触发漏洞的PoC;b.不需要特定的硬件驱动和root权限;c.PoC的崩溃可复现。
实验环境:漏洞都移植到Linux-v5.5.3、FreeBSD-v12.1、XNU-4903.221.2。
(3)评估 ELOISE 辅助利用的作用
实验:找了6个内核研究者,分为两组,给定5个漏洞和相应PoC,要求写出绕过KASLR的exp。对于A组,提供ELOISE
工具来辅助找elastic object
和配对漏洞。每隔半小时记录两组的进展(进展分为3个阶段,漏洞能力探索、elastic object
收集、内存结构布置),根据每个阶段的耗时来量化ELOISE
辅助利用的作用。
5.2 实验一结果
漏报:ELOISE 识别出 97 / 800个 elastic object
, 对应98个泄露路径,人工找到的是ELOISE 的子集,说明漏洞率很低。
误报:人工验证74 / 97个为正确的elastic object
,误报率可容忍。误报主要源于不准确的调用图构建,例如,只有当硬件设备插入时,ieee80211_set_probe_resp()
和register_leaf_sysctl_tables()
函数才会分配probe_resp
和 ctl_table
对象,但 ELOISE把 这些对象和未授权系统调用联系在了一起。
elastic object
可利用性和普遍性:见Table 4~6,70/74个elastic object
可以帮助泄露堆数据,绕过KASLR 和 heap cookie protector;28/74个可以任意读;5/74个可以泄露栈canary。Linux / FreeBSD / XNU 分别有39,483 / 44,956 / 22,307 次分配和使用这74个对象。
elastic object
所需权限:60/74不需要任何权限;14/74需要 CAP_NET_ADMIN
或 CAP_AUDIT_READ
权限。
elastic object
cache/zone覆盖:见Table 4~6,74个 elastic object
涉及大多数通用和特殊的cache / zone(eg,kmalloc-16384
— Linux, mbuf
— FreeBSD, pipe_zone
— XNU)。见Table 1,4,5,6带星标的18/74个对象的大小是可变的,提高了可利用性。
elastic object
泄露路径及约束:见Table 1,4,5,6,ip_options
有多个泄露路径(能从堆和栈泄露),其余对象都只有1条;10/74个对象的泄露路径的约束中含变量,不适合利用。
5.3 实验二结果
绕过防护:见Table 7 和 Table 2,27/40漏洞可以绕过KASLR和 heap cookir protector,12/27可以泄露栈canary,8/27可以任意读。
利用的丰富性:26 / 27(除了CVE-2017-2370)可以利用多个elastic object
,CVE-2017-7184可利用最多。丰富性取决于两点,一是漏洞可溢出多种cache,二是覆盖elastic object
的限制较少。
失败案例:13个利用失败。CVE-2018-5703, CVE-2018-12233, CVE-2018-1000112, 3d67[68] 只能覆写 vul obj
内部数据;CVE-2018-18559, CVE-2017-15649, CVE-2017-10661, CVE-2019-6225, 422a[69] ,ELOISE 无法找不到任何 elastic object
的length 和被溢出的区域重叠;CVE-2018-4243 只能把elastic object
对象的length 覆写为0;CVE-2019-5603, CVE-2019-5596, bf96[74] 只能溢出 特殊的cache / zone,没有合适的elastic object
可用。
5.4 实验三结果
结果:A组在ELOISE的帮助下成功完成5个漏洞的内核基址泄露,B组全部失败。细节可见 Appendix A.5
6.防护机制
6.1 现有的防护机制
heap freelist randomization [16, 23],[16] 指出freelist随机化对于UAF和Double-Free漏洞无效,[6, 28, 44] 都研究了如何绕过该机制(都是iOS)。
structure layout randomization [15],防止修改
elastic object
中的length,但该机制依赖一个随机种子来进行随机化,可以泄露该种子。"USERCOPY" checking [22],Linux / XNU 支持,保证length参数不超过 cache /zone slot 或 stack frame的大小。但它只对
copy_{from/to}_user()
和copyout()
进行length检查,不包含其他传递函数,且对合法的length范围限制不严。
6.2 我们的防护方法
方法:使用shadow caches/zones
隔离 elastic objects
,例如,用 kmalloc-isolated-16
替代 kmalloc-16
。不同于 xMP [58] 页粒度的隔离机制,这个粒度的隔离仍然能覆写。
实现:在Linux kernel上实现,在boot阶段创建 shadow cache
,修改内核加上 flag __GFP_ISOLATE
作为编译选项,在kmalloc附近,可以选择是否将elastic object
分配到 shadow cache
。
性能评估:环境是 1.6 GHz CPU, 16GB RAM, and 500 GB HDD
,内核版本 v5.5.3
,benchmark选择 3种,LMbench v3.0 [51] ——测系统调用和I/O操作的延迟和带宽、Phoronix Test Suite 9.8 [1]——5个真实应用、定制的系统调用序列(能到达elastic object
分配点和相应泄漏点)——对防护方法进行压力测试。结果见Table 8,性能平均开销是0.19%,可忽略不计。
安全评估:采用作者提出的防护方案,对Table 7中所有漏洞(除了 CVE-2017-7184 和 CVE-2017-17053),ELOISE 找不到可用的 elastic object
。对于 CVE-2017-7184 和 CVE-2017-17053,他们的vul object
位于 shadow caches
,虽然还能利用elastic object
泄露数据,但是泄露不出重要数据(shadow caches
中找不到包含函数指针的object),无法绕过KASLR。
7.相关工作
提高可利用性:侧信道攻击 [24, 34]——利用硬件特性 Intel TSX timing attack [37]、pre-fetch [27]、out-of-order execution [46];新的利用方法——ret2dir [39]、 KEPLER [79]、Data-only attack [9]。
辅助利用:自动生成exp——[4, 7, 8, 33, 62, 63, 66];辅助生成exp——[5, 29, 30, 36, 59, 78, 82];two memory collision attack [81] 辅助UAF的堆喷射;确定性的栈喷射技术和消耗内存的喷射技术,来利用栈 Use-Before-Initialization漏洞 [49]、扩展栈喷射技术 [14];FUZE针对内核UAF漏洞 [80];能力导向的fuzzing技术 搜集OOB漏洞的能力 [10];SLAKE 自动化内核堆布局 [11]。
8.Conclusion
本文对elastic object
进行系统性的研究,显示elastic object
几乎总能帮助内核漏洞绕过防护机制,如KASLR、heap cookie protector、stack canary,并提出轻型的防护机制,将elastic object
放在不同的 cache/zone中。