[TOC]
本文将以多个真实的堆相关的漏洞以及一些高质量的研究文章为例,介绍在真实漏洞场景下的堆利用技巧,主要是介绍各种现实漏洞场景的堆布局技巧。
vuln obj : 存在漏洞的对象,比如存在堆溢出的对象。
target obj: 漏洞利用时需要篡改的对象,比如利用堆溢出时,我们想覆盖的对象。
https://vipread.com/library/topic/2315
tips:
寻找target obj 时,优先在出现漏洞模块中寻找,一般可以考虑把存在漏洞对象作为 target obj,其次就是一些带指针、size、offset的对象。
当来利用堆溢出的时候,我们需要找一个 gadget object 来进行利用
常用的 gadget object 类型:
- 有函数指针,虚表指针,可用于劫持控制流。
- 对象中有 size/length 字段,修改这个字段可以导致后续使用对象时发送越界。
- 对象中的指针在后面的逻辑中会被用于读、写内存等操作,利用程序的逻辑可以完成一些漏洞利用的原语。
此外利用堆漏洞非常重要的是进行堆布局,为了进行堆布局我们需要找到外部输入可控的内存操作原语,搜索内存操作原语时需要关注三个要素:
- 内存分配的大小是否可控
- 内存中的内容是否可控
- 分配到的内存的生命周期是否可控,即我们能否控制其申请和释放的时机
然后根据不同的内存控制原语采用合适方式进行堆布局,常见搜索内存操作原语的思路是在代码中找含有malloc,new,realloc,std::vector,std::string的调用点,然后判断是否可以由外部输入触发。
此外为了提升溢出到 gadget 的概率我们可以先把内存碎片清理了,然后再进行占位式堆喷,这样 vuln obj 和 gadget obj 大概率会相邻分配。
如果分配 gadget object 的时候会有一些小对象的分配从而导致影响内存布局,我们可以先在堆上占位一些小的内存块,然后在分配 gadget 前释放一些小的内存块,这样小对象就会落在之前占位的小内存块中,避免影响 vuln obj 和 gadget object 之间的布局。
Assuming the gadget object is the one which allocates the unwanted allocations, one way to deal with this issue is to do the following:
- First, at the beginning of the exploit, we allocate a bunch of smaller blocks, let’s say 100 blocks of 0x100 bytes each
- We then allocate all the placeholder sets (表示一组相邻的 overflowable 和 gadget 占位对象)
- Then, for each placeholder set, we first free the gadget placeholder.
- Then we free a few of the small filter allocations. These will be added to the free bins
- Then we allocate the gadget itself, the smaller unwanted allocations will use the filter allocations we just freed, and the gadget itself will fall on the placeholder.
漏洞是一个整数溢出导致的堆溢出
case FOURCC('t', 'x', '3', 'g'):
{
uint32_t type;
const void *data;
size_t size = 0;
if (!mLastTrack->meta->findData(
kKeyTextFormatData, &type, &data, &size)) {
size = 0;
}
if (SIZE_MAX - chunk_size <= size) { // <---- attempt to prevent overflow
return ERROR_MALFORMED;
}
uint8_t *buffer = new uint8_t[size + chunk_size];
if (size > 0) {
memcpy(buffer, data, size);
}
if ((size_t)(mDataSource->readAt(*offset, buffer + size, chunk_size))
< chunk_size) {
delete[] buffer;
buffer = NULL;
return ERROR_IO;
}
在分配内存前有一个检查,但由于 chunk_size
在 32 位系统下下也是 uint64_t
的,所以当 chunk_size > SIZE_MAX
时(比如 0xFFFFFFFFFFFFFFFF
),会通过检查,然后下面分配的时候就会整数溢出。
从文件中解析 chunk_size
的代码如下
status_t MPEG4Extractor::parseChunk(off64_t *offset, int depth) {
uint32_t hdr[2];
if (mDataSource->readAt(*offset, hdr, 8) < 8) {
return ERROR_IO;
}
uint64_t chunk_size = ntohl(hdr[0]);
uint32_t chunk_type = ntohl(hdr[1]);
off64_t data_offset = *offset + 8;
if (chunk_size == 1) {
if (mDataSource->readAt(*offset + 8, &chunk_size, 8) < 8) {
return ERROR_IO;
}
chunk_size = ntoh64(chunk_size); // 从文件中获取 8 字节的 chunk_size
因此通过这个漏洞我们可以实现一个堆溢出,vuln obj 的大小可控,溢出的内容可控,由于是整数溢出后面可能会拷贝大量的数据(比如 0xFFFFFFFFFFFFFFFF
),最终可能会由于访问到没有映射的内存导致进程崩溃。
因此,利用这种类型的整数溢出漏洞,我们需要找到一些路径,让溢出发生后,不会拷贝过量的数据,对于这个漏洞来说我们可能有以下两种方式:
- 内存分配后,程序会调用 readAt 从文件中读取 chunk_size 的数据,如果能让 readAt 返回值小于 chunk_size (比如控制文件大小),就能避免溢出过量的数据。
- 通过覆盖在具体数据拷贝之前调用的函数指针完成利用,从而避免了溢出过量数据。
该漏洞的利用采取的是第二种方式,即溢出到 mDataSource
对象 (其类型 MPEG4DataSource
),然后把该对象的虚表劫持,从而劫持 readAt
这个虚函数调用。
那么利用该漏洞的 target obj
就是 mDataSource
对象,如果要利用该漏洞,buffer 和 mDataSource 的内存布局如下:
下面就介绍如何利用程序中的逻辑来整理堆的布局,实现上述的布局,首先需要分析源码定位 vuln obj 和 target obj 的分配方式,当解析到 stbl
标签时,会分配 mDataSource
对象(target obj),当解析 tx3g
标签时就会分配 vuln obj 并触发溢出。
然后再看看程序用的内存分配器的一些特点,其使用的是 jemalloc ,jemalloc的大概思路是把内存块分成大小相同的 object,然后返回给申请者,类似于内核的 slub 分配器。
由于堆分配器的实现,堆在初始情况下先分配的对象的地址会比后分配的对象的地址小,这时如果直接先分配 target obj 的话,vuln obj 就会在 target obj 的后面,因此无法溢出到 target obj.
假如先分配 vuln obj 的话,由于漏洞触发点的内存分配和数据溢出是一起发生的,也是无法覆盖到 mDataSource
。
或者我们可以利用jemalloc的LIFO特性,先把后面的块释放了,然后把前面的块释放了,然后再先分配 target obj 后分配 vuln obj,也可以实现上述的布局。
exploit
里面的思路是采用占位对象,然后通过控制占位对象的分配与释放,来完成布局,示意图如下:
- 首先分配两个占位对象 A 和 B,且 A 和 B 的前后都是被分配出去的内存,然后释放B,再利用
stbl
标签分配 target 对象,这样mDataSource
就会落在 B 的位置。 - 然后释放 A ,并利用
tx3g
标签触发漏洞,分配buffer到 A 的位置,就能完成我们需要的布局,这样一溢出就可以覆盖到mDataSource
对象。
要完成上述布局,关键点在于需要找到可以通过用户输入(mp4文件)控制 分配/释放 的代码逻辑,在 libstagefright
中我们可以利用 avcC
和 hvcC
来进行占位,通过这两个标签的特点为:
- 分配任意大小的内存,内存中的内容可控(从文件读取)
- 内存释放的时机可以通过输入文件中的特定标签控制
利用 avcC
和 hvcC
来占位的示例图如下:
为了稳定的完成堆布局,还需要解决内存碎片的问题,上述布局成立的前提是 avcC 和 hvcC 对象是相邻的,如果内存中存在内存碎片的话,这两个对象是有可能不相邻的。
为了解决这个问题常用的方式是首先把之前的内存碎片清掉(即大量分配内存,把之前堆中碎片的内存块申请完),之后再申请内存的时候,我们分配到的内存块的顺序就会符合我们的预期了。
exploit
中使用的是 pssh
标签来清理内存碎片,原因是程序在解析 pssh
标签时会根据标签中的数据分配内存,内存的大小和内容可控,且分配出来的内存在整个文件解析完成后才会被释放,因此直接在输入文件中放置多个 pssh
标签就可以完成内存碎片的清理工作。
完成堆布局后现在就可以溢出到 mDataSource
对象 (其类型 MPEG4DataSource
),接下来的利用思路就是覆盖对象的虚表,把虚表劫持到我们伪造的需要,然后对象进行虚函数调用的时候就可以劫持pc。
目前的问题是如何知道 伪造的虚表 的地址,获取地址一般有两种方式:
- 找一个信息泄漏漏洞,或者利用漏洞构造信息泄漏,从而可以拿到堆的地址,然后把虚表指针指向堆上的可控数据位置即可。
- 利用堆喷射技术,在某个固定的地址布置伪造的虚表数据,然后把虚表指针指向该位置即可。
这里使用的是堆喷射技术,以32位系统为例,堆喷射的思路大概是由于进程虚拟地址空间的大小限制,当程序分配大量内存时(比如 0xff000 字节时), 尽管存在随机化,我们依然可以以极大的概率在某个具体的地址(0xf7500000)上布置我们的数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#define ALLOC_SIZE 0xff000
#define ALLOC_COUNT 0x1
int main(int argc, char** argv) {
int i = 0;
char* min_ptr = (char*)0xffffffff;
char* max_ptr = (char*)0;
for (i = 0; i < ALLOC_COUNT; ++i) {
char* ptr = mmap(NULL, ALLOC_SIZE,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (ptr < min_ptr) {
fprintf(stderr, "new min: %p\n", ptr);
min_ptr = ptr;
}
if (ptr + ALLOC_SIZE > max_ptr) {
fprintf(stderr, "new max: %p\n", ptr + ALLOC_SIZE);
max_ptr = ptr + ALLOC_SIZE;
}
memset(ptr, '\xcc', ALLOC_SIZE);
}
fprintf(stderr, "finished min: %p max %p\n", min_ptr, max_ptr);
((void(*)())0xf7500000)();
}
利用大量的内存分配可以让进程的某个地址存放我们的数据,数据的内容如何控制呢,按照我的理解,我们在进行堆喷的时候尽量按页对齐去分配,然后以页为单位进行数据布局,就可以实现在可预测的地址,布置可控的数据,具体的偏移和布局还需要调试确认。
exploit 中的堆喷射思路如下
小结
该漏洞的利用非常经典,主要的特点如下:
- 触发整数溢出后,通过劫持后续可能会使用的虚函数调用来避免整数溢出后的内存拷贝访问到不可访问的内存.
- 通过分析程序对文件的解析流程,找到了内存申请和释放的原语(avcC 和 hvcC),随后使用这两个标签进行占位并完成了 vuln obj 和 target obj 的布局。
- 为了增大占位成功的概率,使用了 pssh 标签分配大量对象,清理内存碎片。
- 为了在没有信息泄露的情况下劫持虚表,利用
pssh
进行堆喷射,在可预测的地址处放置 rop gadget.
主要可以学习的思路有:
- 触发漏洞后,要思考后续的步骤,比如后续要覆盖什么,为了完成覆盖,需要什么样的内存布局。
- 然后去程序中查找是否存在内存控制的原语,比如内存申请/释放原语,申请的大小、内存的内容、释放的时机是否可控等。
- 然后就根据堆分配器的特性来尝试堆布局。
- 堆喷射在32位下还是很有用的,64位下在一些情况下面也是可以用的。
- 占位型堆布局和内存碎片的清理也非常的经典。
漏洞是8字节的溢出, vuln obj 的大小可控。
作者通过不断调整 vuln obj 的大小来触发漏洞,然后分析 crash 的上下文,最后在32字节的run中找到了合适的 target obj (fixed_queue_t),该对象中有个函数指针,可以用于劫持控制流。
为了实现控制 pc,还利用堆喷在可预测的地址上布置了数据,布置的过程主要靠尝试和调试。
小结
该漏洞利用最直接借鉴的思想是通过不断尝试 vuln obj 的大小,来找到潜在的 target obj,这种思路比较有通用性,也可以减少人工搜索 target obj 的工作量,甚至会发现一些比较神奇的对象。
还有就是 jemalloc 的堆喷可以通过观察 regions 的内存布局来提升成功率。
这组漏洞利用包含两个漏洞,一个信息泄露漏洞(CVE-2020-3847)和一个堆溢出漏洞(CVE-2020-3848)。
CVE-2020-3847 的成因是内存申请的大小和访问内存时的偏移检查有问题,漏洞触发流程:
- 首先发一个请求,让 ServiceAttributeResults 分配 16 字节的内存
- 然后再发一个请求,设置一个比较大的偏移,就可以 泄露出 ServiceAttributeResults 后面的内存数据
CVE-2020-3848 的漏洞成因是分配内存时分配的是 0x20 字节,但是拷贝数据时最多可以拷贝 0xff 字节。
为了实现 Zero Click 的漏洞利用,作者经过分析发现只能通过创建 SDP 连接来让目标分配内存,且一个设备最多只能创建 30 个 SDP 连接,创建 30 个 SDP 连接后实际会分配的对象和关系如下图
随后作者通过释放其中的几个 SDP 连接对象,并结合信息泄露漏洞来检查当前堆布局是否可以用于利用,即vuln obj 后面是否存在可以用于利用的对象。
大概思路应该是通过一些内存的申请释放看能否让 vuln obj 后面放置可利用的对象。
小结
主要启示在于:
- 在审计代码的时候,要注意审计内存申请和使用不是在同一次请求、解析过程中处理的情况,两者的不一致就有可能导致漏洞。
- 该漏洞利用的堆布局有点撞运气的感觉,不过由于有信息泄露,我们可以多次尝试,直到符合预期的堆布局出现再触发堆溢出漏洞。
- 有限的堆操作原语(只能控制某些特定对象的申请于释放)也是非常有用的。
这篇文章涉及3个漏洞,本节主要涉及其中2个被利用的漏洞,即 CVE-2020-12352 和 CVE-2020-12351,比较有意思的是作者在编写 CVE-2020-12352 的 POC 时,触发了 CVE-2020-12351.
CVE-2020-12351 是一个栈变量未初始化漏洞导致的信息泄露,作者通过随机的发送一些报文进行尝试,最终可以通过该漏洞泄露出 内核和堆的地址。
CVE-2020-12352 是一个类型混淆漏洞,漏洞的成因是把 struct amp_mgr
对象当作了 struct sock
对象传入了 sk_filter
函数进行处理。
经过分析,通过控制 sock
结构体 (即被类型混淆的 amp_mgr
对象)的 sk_filter
指针可以完成漏洞利用, struct sock
的结构体布局如下:
// pahole -E -C sock --hex bluetooth.ko
struct sock {
struct sock_common {
...
short unsigned int skc_family; /* 0x10 0x2 */
...
} __sk_common; /* 0 0x88 */
...
struct sk_filter * sk_filter; /* 0x110 0x8 */
可以看到 sk_filter
字段位于 sock
结构体偏移 0x110
处,但是 struct amp_mgr
的大小为 0x70
,由于 slub 分配器的特性, amp_mgr
结构体实际会在 kmalloc-128
处分配,所以 sk_filter
字段实际位于 amp_mgr
结构体后面第二个内存块中,如下图所示:
amp_mgr
分配在 kmalloc-128
中,当 amp_mgr
被当作 sock
结构传入 sk_filter
函数处理时,其访问 sk->sk_filter
时,实际访问的是图中标红区域。
因此目前的问题是如何控制 fake sk_filter
字段,作者的思路是首先分配 amp_mgr
,然后再 kmalloc-128
中分配两块内存,从而控制 fake sk_filter
字段.
上述思路的关键是需要找到可以远程从 kmalloc-128 中分配内存的原语,常见思路是在协议栈代码中搜索内存申请函数(比如 kmalloc, kzalloc等),不过作者没有找到上述的原语,最终是利用 kmemdup
操作实现的内存申请,代码如下
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/a2mp.c
static int a2mp_getampassoc_rsp(struct amp_mgr *mgr, struct sk_buff *skb,
struct a2mp_cmd *hdr)
{
...
u16 len = le16_to_cpu(hdr->len);
...
assoc_len = len - sizeof(*rsp);
...
ctrl = amp_ctrl_lookup(mgr, rsp->id);
if (ctrl) {
u8 *assoc;
assoc = kmemdup(rsp->amp_assoc, assoc_len, GFP_KERNEL);
if (!assoc) {
amp_ctrl_put(ctrl);
return -ENOMEM;
}
ctrl->assoc = assoc;
ctrl->assoc_len = assoc_len;
ctrl->assoc_rem_len = assoc_len;
ctrl->assoc_len_so_far = 0;
amp_ctrl_put(ctrl);
}
...
}
大概堆布局思路如下:
- 首先利用
kmemdup
的原语大量分配内存,清理之前slab
中的碎片 - 然后分配
amp_mgr
,之后再分配一些ctrl->assoc
,最后触发漏洞就可以控制fake sk_filter
的值。
不过看 exploit
里面的堆布局思路,貌似是直接先分配多个 ctrl->assoc
,然后释放掉最后一个 assoc
和 amp_mgr
,最后重连,就有一定概率会达到上面的布局,从而完成利用。
堆布局相关代码片段:
小结
- 搜索内存操作原语时,间接调用内存申请的函数也是需要关注的。
- 做漏洞利用时有一定概率也是可以接受的,比如堆布局,即使无法达到100%,能提升一点概率是一点,或者如果概率可以接受的话也可以直接随机着来。
文章中利用的漏洞是固件在解除 TDLS 连接时 由于解析 FT IE 时没有检查长度导致的堆溢出漏洞
因此我们就有了一个堆溢出漏洞,vuln obj 的大小是 256 字节,下面就需要找被溢出的对象,以及寻找堆布局的方式。
作者首先逆向分析了固件中的堆分配器,然后通过 固件代码 patch、内存dump等手段实现了一个堆的可视化工具,可以跟踪固件的内存申请和释放、可视化某个时刻的堆内存布局:
图中红色表示正在使用的内存块,灰色表示处于 free 状态的内存块。
- 图中第一行表示 初始的堆状态,可以看到这里有一块很大的空闲内存.
- 第二行表示 创建 TDLS 连接后的堆状态,有很多内存被使用了,值得注意的是此时堆中还有两块特别标注的空闲内存块,即
0x1f03fc
(大小为 0x11c )和0x1f2108
(大小为 0x124).
然后我们如果尝试解除 TDLS 连接并发送恶意的FT IE触发漏洞,由于堆分配器的实现,此时 malloc(256)
会分配到 0x1f03fc
这块内存,我们也就可以溢出到 0x1f0520
后面的块。
此外经过作者的不断尝试发现,堆状态非常稳定,每次 创建/解除 TDLS 连接 时堆的状态基本是一样的,这样如果能在这个堆状态下完成利用,exploit的稳定性应该也是比较高的,不过如果程序中有一些可用于堆布局的原语(内存申请、释放),可能可以改变这个内存布局,不过作者没有尝试,而是在当前内存状态下完成了利用。
因此 vuln obj
是 0x1f03fc
这块内存,target obj
是 0x1f0520
后面的内存块。
对于堆溢出的利用常见的思路有两种:
- 修改堆块头的元数据,比如 size,或者处于释放状态的块中的 next 指针
- 修改堆中对象的数据,比如对象的长度字段,堆中的指针等,然后利用程序对对象的操作来进行后续的利用
vuln obj
和 target obj
的内存 dump 如下:
可以看到 target obj
的头不由两个看着像指针的值,不过经过修改尝试发现这两个指针好像没有人用,因此目前只能尝试修改内存块的 头部字段,由于 target obj 所在内存块是 inuse 状态,所以头部字段中只有 size 字段是有效的。
在 glibc 的堆溢出 中常用的方式是修改 size 字段来构造重叠的堆块,这里也是这样的思路,由于目标系统中堆的使用情况比较复杂, 作者采取的方式是,写脚本自动化地测试,即尝试把 target obj 的 size 字段修改成不同的值,然后检查操作完成后的堆状态。
经过测试,作者发现把 target obj
的 size
字段覆盖为 72
并断开 TDLS
连接,堆中出现了重叠的堆块:
可以看到在断开 TDLS
连接后,0x1f072c
这个 0
字节的 chunk
位于一个大的空闲块中间。
创建重叠堆块后,还需要一个控制内存分配的原语,用于分配到重叠的块,然后修改它的 size 和 next 指针,从而实现任意地址写,固件处理 action 帧的代码中就存在这样的逻辑:
A 是一个全局变量,利用 A 的分配和申请逻辑,可以获取一个生命周期、大小、内容可控的内存控制原语。
最后利用堆分配的 best-fit
特性,修改 chunk
的 next
指针,实现把一个已分配的块链接到空闲块的链表里面,然后实现任意地址写,最后的利用手法如下:
首先利用任意地址写,往一个初始化阶段分配的堆块中布置 shellcode(由于没有地址随机化,系统启动过程中分配的一些堆内存的位置会固定),然后修改 堆中定时器的函数指针,最后等待定时器到期,执行 shellcode.
选择把处于使用中的堆块链入空闲块链表的原因是:处于使用的块的 next 指针为0,这样就不会导致内存分配器在分配内存时由于遍历链表导致崩溃。
小结
- 堆利用的时候,通过调试、打印、dump等手段查看 vuln obj 附件的内存状态,以及触发漏洞前后的内存使用情况,可以很好辅助漏洞利用、定位漏洞利用的方案。
- 对于比较复杂、不熟悉的场景可以尝试暴力枚举,来探测可利用的方案。
漏洞成因是 sudo 在解析命令行参数时存在堆溢出,vuln obj 的大小和内容可控。
目前问题就是寻找 target obj,作者的思路是fuzz,通过随机选择 vuln obj 的大小、溢出的大小、setlocale 涉及的相关环境变量(用于控制溢出前的内存块布局),最后通过对 crash 的上下文去重,发现了三种可用于利用的场景,本节将介绍通过覆盖 service_user
完成利用的过程。
首先通过调整 poc 和调试,能够发现在堆上确实是存在 service_user
结构,不过调试发现其和 vuln obj 的偏移过大,直接去尝试覆盖 service_user
程序会由于中间的一些数据被覆盖而崩溃,且两者的偏移也不固定。
为了解决这个问题,可以利用 setlocale
函数中的 内存分配/释放 操作进行堆布局,让 user_args
落在 service_user
的前面。
setlocale
函数在解析环境变量(比如LC_CTYPE
, LC_MESSAGES
, LC_TIME
等)时,会申请内存并把环境变量的值拷贝到申请的内存中,申请的内存在函数退出前会被释放。
通过利用 setlocale
函数的逻辑和堆分配器的机制,我们提前在堆中创建一些 free 的堆块,让 service_user 分配到其中的一个空闲块,这样在漏洞触发时就能在 service_user
前面留一个 free
堆块,这时我们将 user_args
分配到 service_user
前面就可以稳定溢出service_user
了,大概流程图如下所示:
小结
- 通过 fuzz 来探测可能用于利用的内存状态值得学习。
- 通过提前在堆里面凿一些洞(空闲的堆块)进行堆布局的思路非常新奇。
漏洞是 Zoom
在进行密钥协商时存在一个堆溢出,堆溢出的情况如下:
- vuln obj 的大小固定 1040 字节
- 溢出的大小和内容可控
漏洞利用的环境是 win10 + 32位的 Zoom 软件。
首先利用堆溢出实现信息泄露,常见的思路是分配一些带 length 字段的对象,然后覆盖length字段从而 leak 出一些数据,经过一番分析没有发现能够回显的这种对象。
最后发现,可以给目标 Zoom
客户端发送特定的请求,让对端 Zoom
向我们的服务器发起 https
请求,请求的 url 部分可控,有种 SSRF 的感觉。
接下来就是利用这个 bug 进行信息泄露,具体来说就是利用溢出把 URL
末尾的 \x00
覆盖掉,然后让目标客户端向我们的服务的发起请求,就可以泄露 URL 后面的内存数据了。
这里选择泄露的对象是 TLS1_PRF_PKEY_CTX
,这个对象是 openssl
进行 TLS 协商时创建的,对象中存在指向 DLL
的指针,通过泄露这个指针可以可以拿到 DLL
的基地址。
如果堆布局按照预期就能在我们的服务器收到泄露的数据
不过由于 LFH 分配器的随机化和内存状态的不确定性,成功率还是比较低,作者采取了一些措施来提升成功率:
- 创建多个服务器的连接,用于不断的进行TLS协商,目的是在 LFH 的桶中分配大量
TLS1_PRF_PKEY_CTX
,这个过程中即使对象被释放也没关系,因为释放之后内存中的指针不会被清理,也能进行leak,这样就可以增大布局的概率。 - 在覆盖的过程中,可能由于内存布局的不一致导致 vuln obj 和 url 之间存在一些带指针的对象(比如某些类的实例),作者的处理方式是首先通过堆喷在某个地址(比如 0x0d0d0d0d)放置数据,然后用 0x0d0d0d0d 覆盖对象,这样即使覆盖到了不正确的对象也不会导致进程崩溃,从而可以多次尝试。
完成信息泄露后,下一步就是找一个对象控制 PC, 这一步使用的是 FileWrapperImpl 该对象会在客户端被呼叫响铃的时候被不断的创建和删除,因此使用堆溢出覆盖其虚表即可劫持控制流。
为了劫持虚表还需要进行堆喷,攻击者可以发起请求让目标客户端加载 gif
图片,而且没有大小限制,利用这个机制就可以进行堆喷。
第二部的过程中也会存在很多不确定性,比如溢出 vuln obj 的过程中可能会覆盖到堆中的其他类的虚表,作者的解决方案如下:
- 不断尝试溢出、调试搜集在利用过程中可能会触发的场景,在虚表、rop 中进行适配,比如 A 类调用的时候可能用的是虚表偏移 0x20 的函数指针,我们就在 0x20 的函数指针布置适合 A 的利用 rop.
- 不断溢出,每次溢出的大小递增,避免出现一次溢出太多导致不稳定。
小结
- 利用客户端的 url 请求机制进行 leak 也是一个思路。
- 处理堆布局随机的情况可以多次尝试,为了能够多次尝试还需要合理控制溢出数据,避免溢出失败导致进程 Crash.
- 针对不同的溢出场景,在虚表中进行适配思路非常不错!
漏洞是在解析图片时存在整数溢出导致的堆溢出
首先根据 width
, height
, output_components
计算分配内存的大小,然后从文件中一段一段读入内存,如果 width * height * output_components
发生整数溢出,后面从文件中读取内容时就会溢出。
之前提到过利用整数溢出漏洞需要找到终止拷贝的条件,或者在访问到非法地址前劫持控制流,这里采用的是覆盖拷贝过程中会用到的函数指针,在循环访问到非法地址前劫持控制流。
作者通过分析循环中的一些函数调用,发现在 jpeg_read_scanlines
函数和子函数里面会使用 cinfo
结构体中的一些的函数指针
struct jpeg_decompress_struct {
......
struct jpeg_d_main_controller *main; <<-- there’s a function pointer here
struct jpeg_d_coef_controller *coef; <<-- there’s a function pointer here
struct jpeg_d_post_controller *post; <<-- there’s a function pointer here
......
};
target obj 找到了不过由于没有找到合适的内存控制原语,没法进行完美的堆布局,作者最终采取的策略是通过调试和观察漏洞触发前后的堆状态,来搜索可能用于利用的 vuln obj 大小。
首先通过分析知道 cinfo
分配在 0x5000
的 run
中,然后查看 cinfo 前面的一些run中的使用情况,发现可以通过分配 0xe0
的内存,然后往后溢出即可覆盖 cinfo
.
原文作者到这里就没继续了,这种方式有点撞运气的成分,如果进程在之前有些其他分配就可能会导致偏移不一致,不过原文中也提到了,我们可以溢出多一点总是能够溢出到函数指针的。
小结
- 通过修改循环中的函数指针来防止整数溢出后的Crash也是比较经典。
- 有时候实在没有堆控制原语时,也需要多调试、分析,查看 target obj 和 vuln obj 的附件是否有可以用于利用的内存块。
漏洞利用链包含两个漏洞:
- malloc 未初始化导致的信息泄露。
- vrend_resource 堆溢出,vuln obj 的大小可控,溢出的大小内容可控。
利用未初始化漏洞的思路是堆喷带有函数指针的结构体,然后释放这些结构体,最后触发未初始化漏洞并获取未初始化的内存内容,从而获取 virglrenderer 库的地址。
获取 libc 的地址的思路是申请大块内存,内存中就会包含libc 的地址
堆溢出的利用思路是堆喷多个 vrend_resource 对象,然后利用溢出修改下一个 vrend_resource 对象的指针,从而实现任意地址写
溢出前,两个 vrend_resource 对象的布局
溢出后,第二个 vrend_resource 对象的指针修改为任意地址,然后利用程序的逻辑就可以实现任意地址写
小结
- 利用未初始化漏洞进行内存泄露的思路值得学习,比如堆喷带指针的结构,然后利用未初始化漏洞获取地址。
- 通过堆喷 vuln obj ,然后溢出下一个 vuln obj 的指针来进行任意地址写,得到启示是做利用的时候可以先看 vuln obj 是否就可以作为 target obj.
主要就是介绍如何在 ios 的内核里面进行占位式堆布局。
具体思路是先清理堆中的内存碎片,然后用多个 victim object 包着一个 vuln object ,这样就能提升溢出到 victim object 的成功率,如下图所示
其中: V 表示 victim object, P 表示用于占位的对象,后面溢出的时候就会把 P 释放,然后分配 vuln object.
为了进一步增加漏洞利用的成功率,可以多创建几个用于漏洞利用的区域,多次触发漏洞来提升成功率,如下图所示:
小结
通过在 vuln obj 前后多包裹几个 victim object 和 创建多个漏洞利用区域来提升成功率的思路值得借鉴。
漏洞位于 __config_session_info
中,通过 ioctl
触发。
int __config_session_info(struct npu_session *session)
{
...
ret = __pilot_parsing_ncp(session, &temp_IFM_cnt, &temp_OFM_cnt, &temp_IMB_cnt, &WGT_cnt);
temp_IFM_av = kcalloc(temp_IFM_cnt, sizeof(struct temp_av), GFP_KERNEL);
temp_OFM_av = kcalloc(temp_OFM_cnt, sizeof(struct temp_av), GFP_KERNEL);
temp_IMB_av = kcalloc(temp_IMB_cnt, sizeof(struct temp_av), GFP_KERNEL);
...
ret = __second_parsing_ncp(session, &temp_IFM_av, &temp_OFM_av, &temp_IMB_av, &WGT_av);
...
}
session 在 open 驱动时创建,驱动和用户态进程会使用共享内存进行数据交互,__pilot_parsing_ncp
首先从共享内存中提取出 temp_xxx_cnt
,然后根据提取出的数量字段调用 kcalloc 分配内存,最后调用 __second_parsing_ncp
从共享内存中拷贝数据。
问题在于 __pilot_parsing_ncp
和 __second_parsing_ncp
会从共享内存中提取两次 temp_xxx_cnt
。
如果通过条件竞争(起一个线程不断修改 共享内存中的值),让第二次取出的 temp_xxx_cnt
的值比一次的大就会导致后面的溢出。
下面介绍三种利用方式。
这篇文章作者使用的是一个 ION buffer
的内存越界加漏洞
address_vector_index = (mv + i)->address_vector_index;
// address_vector_index 可控, av 指向 ION 内存
weight_offset = (av + address_vector_index)->m_addr;
(av + address_vector_index)->m_addr = weight_offset + ncp_daddr;
这里的 ION buffer 分配在 VMALLOC
区域,那么首先就需要找到要越界写的 target obj,作者在文中提到内核开启 CONFIG_VMAP_STACK
选项后,进程(task)的内核栈也会从 VMALLOC 区分配,因此作者采用使用用户态进程的内核栈作为 target obj
,因此大概的布局思路如下:
在 VMALLOC
区域分配内存时,如果没有指定 NO_GUARD_PAGE
参数就会在分配的内存后面加上一个不可访问的页作为 grard page
.
预期布局如上图,在存在溢出的 ION buffer
后面是 进程的内核栈,然后触发漏洞修改内核栈的数据完成漏洞利用(比如 ROP)。
下面介绍 VMALLOC
区域的内存分配/释放机制,以及内存布局的方式。在 Linux 内核的虚拟地址空间中由 [VMALLOC_START, VMALLOC_END]
组成的区域称为 VMALLOC
区域,用于分配虚拟地址连续但是物理地址不一定连续的内存, VMALLOC
区域中已经分配出去的虚拟地址空间通过 vm_area
结构体表示,如下图所示:
所有的 vm_area
通过链表和红黑树组织,在进行内存分配时,内核会从低地址往高地址遍历 vm_area
搜索其中处于空闲状态的地址区间,当 空闲空间大小 大于 申请大小 时就会把这个虚拟地址区间分配出去,类似于 ptmalloc
中 unsorted bin 的分配机制。
申请到虚拟地址空间后会调用 alloc_page
一次一页地向伙伴系统申请页面,比如请求分配 10
页,就会调用10
次 alloc_page
。
在释放 VMALLOC
中的内存时,首先会把虚拟地址空间中的物理地址释放回伙伴系统,但是对应的虚拟地址空间不会立马被释放,而是会被标记为 unpurged vm_area
,当所有 unpurged vm_area
的页面数大于一个阈值(ubuntu 20.10 上是 40000 页左右)时才会对这些 unpurged vm_area
进行回收,在 unpurged vm_area
被回收前其对应的虚拟地址是不可被申请的,这个特点在后面进行内存布局的时候需要考虑。
还有一个比较重要的特性是在 VMALLOC
区域分配内存默认会在申请的页面后面加一页 guard page
,用于防止溢出,除非指定 NO_GUARD_PAGE
标记,对于这个漏洞而言 vuln obj
(ION buffer
) 和 target obj
(task kernel stack
) 的后面都会带一个 guard page
,由于该漏洞是 OOB ,所以只需要在计算偏移时注意即可。
为了进行内存布局,首先需要找到能够在 VMALLOC 区进行内存分配/释放的原语,通过一番搜索和咨询,作者发现可以利用 binder 驱动的 mmap 接口来在 VMALLOC 区域中申请和释放内存,不过这里有一个限制,即一次最多只能申请 4 MiB 的内存。
一开始作者以IOS内核漏洞利用的思路尝试进行布局,大概思路如下:
- 首先分配一大块内存,然后利用 fork 喷射大量的进程的内核栈,这样 large alloc 前后就会被填满进程的内核栈。
- 然后释放掉刚刚申请的大内存
- 再次分配一些内核栈,然后分配 ION buffer,然后再分配一些内核栈,这样在大内存中就会形成上图所示的布局,即在 ION buffer 的前后包裹了进程的内核栈
- 然后触发漏洞就能修改进程的内核栈了。
但是这里有一个小问题就是当释放 VMALLOC 区间中的内存时,如果 unpurged vm_area
的页面总数小于阈值,则这段虚拟地址空间不会被回收,所以也就不能完成上述的第3步操作,然后作者就放弃这种布局思路。
不过我感觉,只要用 mmap 的接口分多次分配大量的地址空间,然后触发释放,就能达到阈值,这时应该也能完成上述的布局。
最后的布局思路:
- 首先利用 binder 驱动的 mmap 接口,在 vmalloc 区域中分配内存,目的是把内存区间中的空隙给补上,之后的内存分配就会在一个空闲的大内存区域中分配。
- 通过 fork 在 vmalloc 区分配大量内核栈
- 分配 ION buffer
- 再次通过 fork 分配大量内核栈
然后访问 /proc/vmallocinfo
就可以看到 ION buffer
的后面跟着一些 内核栈。
然后就通过修改内核栈里面的数据,实现了信息泄露和ROP.
小结
作者的思路非常清晰,拿到一个 vmalloc 堆的越界写漏洞,首先就是寻找 target obj,最终选择修改同处于 vmalloc 栈中的 进程内核栈 来进行提权。
在进行堆布局时作者一开始计划采用他之前在 IOS 上经常使用的布局方式(占位式布局),不过由于 vmalloc
区域在释放的时候采取的延时释放机制,作者放弃了这种方式,而是选用了相对比较直接的布局方式,即先填充内存碎片,然后分配 vuln obj, 然后分配 target obj,最后触发漏洞即可。
为了进行堆布局,作者首先是分析了 npu 驱动本身是否存在 vmalloc 区域的内存控制原语,最后是用的 binder 驱动中的逻辑完成的布局,不过这种首先从相对熟悉的代码中搜索需要的东西的思路也值得借鉴。
这篇文章的的利用思路和p0的思路差不多,不过这里没有采取复杂的内存布局策略,而是先用 fork 喷射大量的内核栈,然后分配 ION buffer ,然后再次分配大量内核栈,最后就可以实现布局。
...
atomic_int *wait_count;
int parent_pipe[2];
int child_pipe[2];
int trig_pipe[2];
void *read_sleep_func(void *arg){
atomic_fetch_add(wait_count, 1);
syscall(__NR_read, trig_pipe[0], 0x41414141, 0x13371337, 0x42424242, 0x43434343);
return NULL;
}
...
int main(int argc, char *argv[]) {
...
pipe(parent_pipe);
pipe(child_pipe);
pipe(trig_pipe);
...
*wait_count = 0;
int par_pid = 0;
if (!(par_pid = fork())) {
for (int i = 0; i < 0x2000; i++) {
int pid = 0;
if (!(pid = fork())){
read_sleep_func(NULL);
return 0;
}
}
return 0;
}
...
if(leak(0xeec8) != 0x41414141){
write(trig_pipe[1], "A", 1); // child process kill
for (int i = ion_fd; i < 0x3ff; i++) {
close(i);
}
munmap(ncp_page, 0x7000);
goto retry;
}
...
前面两篇文章都是利用 vmalloc 区域的越界写漏洞来完成的漏洞利用,本文使用的漏洞是由于竞争导致的 kmalloc 内存的溢出,vuln obj 的大小、溢出的大小可控,溢出内容部分可控。
漏洞代码的大概逻辑如下:
- 首先获取
temp_IFM_cnt
,然后分配temp_av
数组,数组中成员个数为temp_IFM_cnt
。 - 获取
WGT_cnt
, 然后分配addr_info
数组,数组中成员个数为WGT_cnt
。 - 然后会再次取
temp_IFM_cnt
和WGT_cnt
往addr_info
和temp_av
中数组中的每个成员赋值。
如果第 3 步取的 xxx_cnt
大于前面用于数组内存分配的值时就会导致溢出。
不过由于结构体中的一些成员不可控,所以需要仔细梳理结构体中可以控制的成员以及结构体的大小,为后续的利用做准备
struct temp_av
结构体大小为 64 字节
offset - size - name
0 - 4 - index : Semi controlled, values are restricted
4 - 4 - hole : Untouched (compiler gap)
8 - 8 - size : Least significant 4 bytes are controlled, rest is zeroed
16 - 4 - memory_type: Untouched
20 - 4 - hole : Untouched (compiler gap)
24 - 8 - vaddr: Untouched
32 - 8 - daddr: Untouched
40 - 4 - pixelf_format: Controlled
44 - 4 - width: Controlled
48 - 4 - hieght: Controlled
52 - 4 - channels: Controlled
56 - 4 - strize: Zeroed
60 - 4 - cstride: Untouched
struct temp_av
结构体大小为 56 字节
offset - size - name
0 - 4 - memory_type : Zeroed
4 - 4 - av_index : Semi controlled, values are restricted
8 - 8 - vaddr : Kernel pointer into the ION buffer at controlled offset
16 - 8 - daddr : Bus address of the ION buffer at controlled offset
24 - 8 - size : Least significant 4 bytes are controlled, rest is zeroed
32 - 4 - pixelf_format: Untouched
36 - 4 - width: Untouched
40 - 4 - hieght: Untouched
44 - 4 - channels: Untouched
48 - 4 - strize: Untouched
52 - 4 - cstride: Untouched
因此我们漏洞的能力如下:
- 通过控制数组元素个数,可以控制vuln obj 的大小。
- 溢出的大小可控。
- 由于结构体的部分成员不可控,会导致溢出的内容部分不可控,这个在选择 target obj 时需要非常注意。
之后作者通过在 kmalloc 和 kfree 增加日志来追踪内存的使用情况,最后发现 kmalloc-128 会被频繁使用,于是觉得让 vuln obj 和 target obj 都落在 kmalloc-256 中,即在 kmalloc-256 中进行溢出。
为了实现稳定的漏洞利用,作者首先决定寻找用于占位的对象来实现稳定的堆布局,布局思路如下:
这里使用的布局对象是 timerfd_ctx
。
完成布局后,作者开始寻找 target obj,他首先在 npu 的代码搜索,后面使用 codeql 来搜索,不过最后找到的对象都可用,原因是由于溢出数据中不可控的部分会把 目标对象 的一些关键成员破坏导致内核 panic.
搜索 target obj 的 codeql 脚本如下
import cpp
// Check if the function is set as a fielf of
// struct file_operations
predicate isFopsHandler(Function f) {
exists(Initializer i |
i.getDeclaration().(Variable).getUnspecifiedType().hasName("file_operations") and
f = i.getExpr().getAChild().(Access).getTarget())
}
// Can the function be called from system call handler
// or from a file_operations callback
predicate isSysHandler(Function f) {
f.getName().matches("sys_%")
or
isFopsHandler(f)
or
// Apply this recursively to previous functions in the control flow
isSysHandler(f.getACallToThisFunction().getControlFlowScope())
}
// Return the name of the calling function
string getSysHandler(Function f) {
(f.getName().matches("sys_%") and result = f.getName())
or
(isFopsHandler(f) and result = f.getName())
or
result = getSysHandler(f.getACallToThisFunction().getControlFlowScope())
}
from FunctionCall fc, Function f, string sys
where
// Match kmalloc family allocations
fc.getTarget().getName().regexpMatch("k[a-z]*alloc") and
// With a given size
(fc.getArgument(0).getValue().toInt() > 128 and fc.getArgument(0).getValue().toInt() <= 256) and
// Reachable from system call or file operation
isSysHandler(fc.getControlFlowScope()) and sys = getSysHandler(fc.getControlFlowScope())
select fc, "Callsite of fitting K*alloc: " + fc.getTarget().getName() + "(" + fc.getArgument(0).getValue().toString() + ") by " + fc.getControlFlowScope().getName() + " from " + sys
由于 三星 的内核中没有开启 CONFIG_SLAB_FREELIST_HARDENED ,最终的利用思路是通过覆盖 slab 中空闲 object 的 next 指针来让 ION buffer 链接到 slab 的 freelist 中,然后让 pipe_buffer 分配到 ION buffer ,此时就可以通过修改 ION buffer 来修改 pipe_buffer ,最后通过修改 pipe_buffer 的 page 指针实现物理内存的任意读写。
小结
首先作者在拿到这样一个条件竞争导致的堆溢出时,对竞争可能出现的情况进行了分析,发现即使竞争失败也不会导致严重的后果(比如内核panic),这样就表示我们可以不断触发竞争直至竞争成功,保证了漏洞利用的成功率。
接下来作者对漏洞的原语进行了分析,搞清楚我们通过漏洞能实现的能力,比如溢出的大小,溢出的内容之后部分可控,以及可控的区域,这样对后续的漏洞利用打下了很好的基础。
后面寻找占位对象,进行占位式布局,寻找受害者对象等一系列思路都非常的流畅、清晰,这就提示我们在进行漏洞利用的时候需要搞清楚当下我们拥有的能力,接下来的目标,以及为了实现目标我们需要的东西和实施的方案,漏洞利用就是一环扣一环,需要时刻保持清晰的思路,一步一步的进行。
最后漏洞利用的思路也是非常有意思,利用溢出数据中 ION buffer
的地址覆盖 释放状态的内存块 的 next
指针,实现把 ION buffer
链入 slab
中,然后分配 pipe_buffer
到 ION buffer
中,然后用户态通过控制 ION buffer 的值控制了 pipe_buffer
结构体。
由于 page
指针指向的其实时 vmemmap
数组中的成员,所以通过修改 page
指针到 vmemmap
数组的其他位置就可以利用 pipe_buffer
的逻辑实现任意物理内存的读写。
漏洞成因是由于内核将 size_t
强转为 int
类型使用,导致检查绕过,最终实现向前越界写,vuln obj
是通过 vmalloc
分配的,触发漏洞时需要让 vuln obj
足够大 (比如 2GB, 即 0x80000000
如果当作 int 型使用是最小负数),然后就能往 -2GB-10B
写入 //deleted
字符串。
示意代码如下:
#include <stdio.h>
int sequoia_test(char* buffer, int len, char* value, int value_len)
{
printf("buffer:%p, len:%d\n", buffer, len);
char* p = buffer + len;
len -= value_len;
if(len < 0)
return -1;
p -= value_len;
printf("new buffer:%p, len:%d, delta:%p\n", p,len, buffer - p);
memcpy(p, value, value_len);
}
int main()
{
size_t sz = 0x80000000;
char* buf = malloc(sz);
sequoia_test(buf, sz, "//deleted", 10);
}
运行结果
$ ./test
buffer:0x7fba536a7010, len:-2147483648
new buffer:0x7fb9d36a7006, len:2147483638, delta:0x8000000a
segmentation fault ./test
分析清楚漏洞的能力后,作者下一步就是寻找 target obj,通过分析之前的研究文章,发现 ebpf 的 jit 代码也是在 vmalloc 区域进行分配,所以作者的思路就是在 ebpf 生成代码后,修改代码页权限为不可写前把ebpf的代码给修改了,然后后利用 ebpf 完成内核的任意地址读写。
为了实现漏洞利用,作者进行堆布局的思路如下:
- 首先通过
bind
不同长度的目录,调用vmalloc
分配各种大小的内存,目的是把内存中的碎片都分配出来,这样后面分配时就会在一块新的空间中分配(vmalloc
区域的尾部) - 然后分配依次分配两个 1G 的内存 和 一个 2 G 的内存,这3个内存会顺序布局
- 然后释放第一个 1 G 的内存,由于内存大小比较大,会超过vmalloc回收地址空间的阈值,所以这个地址空间会被释放,接着大量分配 ebpf ,并利用 USERFAULTFD 让 ebpf 阻塞在修改代码页权限前。
- 触发漏洞,修改 ebpf 的代码
又是经典的占位式布局!
漏洞成因是由于 shiftfs 驱动对 copy_to_user 的返回值使用有误导致的 double free。
- 首先
shiftfs_btrfs_ioctl_fd_replace
通过memdup_user
拷贝用户态数据到 buf,然后进行部分处理后会通过copy_to_user
把修改后的数据返回到用户态。 - 如果
copy_to_user
在拷贝过程中失败(访问到用户态的只读页),其会返回已经拷贝的字节数。 - 然后
shiftfs_btrfs_ioctl_fd_replace
会调用shiftfs_btrfs_ioctl_fd_restore
把buf拷贝到用户态,然后释放 buf 。 - 然后在
shiftfs_real_ioctl
中会再次进入shiftfs_btrfs_ioctl_fd_restore
, 再次释放了buf
.
为了利用漏洞首先我们需要知道 vuln obj
的情况,其大小为 4096 字节
#define BTRFS_PATH_NAME_MAX 4087
struct btrfs_ioctl_vol_args {
__s64 fd;
char name[BTRFS_PATH_NAME_MAX + 1];
};
所以它会在 kmalloc-4096 这个 slab 中分配内存,double free 漏洞的常见利用方式是将其转换为 UAF 漏洞:
- 首先我们分配 vuln obj
- 然后释放 vuln obj 并分配 target obj, 由于分配器的 LIFO 特性 target obj 会分配到之前 vuln obj 所在的内存
- 此时再次释放 vuln obj,就会把 target obj 所在的内存释放掉
- 最后我们通过堆喷等方式大量申请内存就可以修改 target obj 里面的内容完成利用
下一步就是需要找到合适的 target obj,作者采用的方式是开启 ftrace 中关于 kmalloc 和 kfree 的 log,然后随便运行一些应用,比如浏览器,然后分析该过程中分配的对象,寻找能够用于漏洞利用的对象。
最后发现在 __devinet_sysctl_register
中会分配 devinet_sysctl_table
,该结构中存在一些函数指针,然后利用程序的逻辑实现任意地址写。
简单来说就是把 handler
设置为 proc_doulongvec_minmax
,然后设置 data
指针为目标地址就可以实现任意地址写。
为了实现稳定地完成条件竞争,作者使用 userfaultfd
机制让内核在两次 free
之间阻塞,以便有充足的时间让 target obj
分配到 double free
的内存。
作者对 userfaultfd
的使用非常熟练,其中比较关键的点有:
- 首先可以通过将页面 unmap 然后再次 mmap 让一个页面的 fault 多次生效
- 通过把指针指向第一个页面的中间,让一次 copy_to_user 跨两个页面,在组合第一点的手法就可以控制两次 copy_to_user 的执行
- 通过修改第二个页面权限为只读,让 copy_to_user 返回非0,触发漏洞。
漏洞点如下
uint32_t cbTransfer = *pcTransfers * cb;
if (pVBoxSCSI->cbBufLeft > 0)
{
Assert(cbTransfer <= pVBoxSCSI->cbBuf); // --- [1] ---
if (cbTransfer > pVBoxSCSI->cbBuf)
{
memset(pbDst + pVBoxSCSI->cbBuf, 0xff, cbTransfer - pVBoxSCSI->cbBuf);
cbTransfer = pVBoxSCSI->cbBuf; /* Ignore excess data (not supposed to happen). */
}
/* Copy the data and adance the buffer position. */
memcpy(pbDst,
pVBoxSCSI->pbBuf + pVBoxSCSI->iBuf, // --- [2] ---
cbTransfer);
/* Advance current buffer position. */
pVBoxSCSI->iBuf += cbTransfer;
pVBoxSCSI->cbBufLeft -= cbTransfer; // --- [3] ---
/* When the guest reads the last byte from the data in buffer, clear
everything and reset command buffer. */
if (pVBoxSCSI->cbBufLeft == 0) // --- [4] ---
vboxscsiReset(pVBoxSCSI, false /*fEverything*/);
}
cbTransfer
由 Guest
控制,首先在 [1]
处会检查 cbTransfer
不能超过 pVBoxSCSI->cbBuf
,正确的逻辑应该是检查 pVBoxSCSI->cbBufLeft
。
结合后面拷贝内存 [2]
和 修改buffer
剩余空间大小 [3]
的操作就会导致越界读以及 pVBoxSCSI->cbBufLeft
的整数溢出,导致 pVBoxSCSI->cbBufLeft
成为一个很大的值,然后后面对 pbBuf
读写时就会导致越界读写。
为了进行利用首先需要布局,让 pbBuf
后面有一些可以用于利用的对象,文中选择的是 HGCMMsgCall
对象,由于 LFH 的一些随机化的原因,最终的利用流程:
- 多次布置漏洞触发场景,这里选择的是创建
64
个用于触发漏洞的布局。 - 每个布局中由一个
vun obj
和16
个HGCMMsgCall
组成。 - 然后利用越界读,来寻找一个符合预期的布局。
- 然后修改虚表做jop。
一个越界写,值不可控,利用思路是修改target obj 的 size 字段,然后利用 target obj 的逻辑实现越界写,最后完成任意地址读写。
漏洞成因是由于条件竞争导致的double free 漏洞
vuln obj 的大小是 kmalloc-256。
通过漏洞触发double free后, slub freelist 的布局如下:
如图所示slub的链表中会存在两个一样的块A,然后利用这个布局完成信息泄露和任意地址读写,具体流程如下图所示:
- 首先通过漏洞在slub中存在两个一样的释放块(A).
- 然后分配一个 seq_file,这时 head依然指向A,此时 next 和 buf 是一样的.
- 再次分配 seq_file,此时 A 和 B 的内存地址一样.
- 再次分配一个seq_file C,后续用于leak.
- 然后释放seq_file C, C 会链接到 head 链表.
- 释放 seq_file B,此时 B 的 next 指针指向 C,由于 A,B 共用一个内存地址空间,因此 seq_file A 的 buf 也是指向C,然后通过 A->buf 读出 op 的值,从而计算出内核镜像的基地址.
- 释放 seq_file A,此时链表再次存在两个一样的释放块(A).
- 分配 mc_list ,修改 mc_list->addr ,从而修改 freelist 的 Next 指针,实现任意地址写,最后利用 KSMA ,实现任意地址读写.
漏洞的条件如下:
- vuln obj 大小可控 0x100 ~ 0x2000.
- 溢出大小可控,但是溢出数据只能为
\x00
. - 分配和溢出在同一个系统调用中,因此需要采用占位式堆布局(占位->释放占位->触发漏洞->从而溢出目标对象).
利用思路是溢出修改相邻 msg_msg(V) 的 m_list->next 指针的低2字节,让其指向另外一个 msg_msg,使得有两个指针指向了同一个 msg_msg,从而实现 msg_msg 的 UAF.
具体流程如下
- 首先创建4096个消息队列
- 往每个队列中塞入 0x1000 msg_msg,将消息的 type设置为 MTYPE_PRIMARY,称其为 PRIMARY 消息
- 往每个队列中塞入 0x400 msg_msg,将消息的 type 设置为 MTYPE_SECONDARY,称其为 SECONDARY 消息
此时对于每个消息队列,其 PRIMARY 消息和SECONDARY 消息通过 msg_msg->m_list 链接,在堆喷的数目较多时,会有一些 msg_msg 按图中所示顺序排布.
然后间隔释放 PRIMARY 消息并触发溢出,这样就能修改到相邻 msg_msg(V) 的 m_list.next ,如果 V 的 m_list.next 的低2字节非0,就会让 m_list.next 指向另外一个 SECONDARY msg_msg.
然后通过 A2 所在的队列将其释放.
printf("[*] Freeing real secondary message...\n");
if (read_msg(msqid[real_idx], &msg_secondary, sizeof(msg_secondary),
MTYPE_SECONDARY) < 0)
goto err_rmid;
然后通过 sk_buff 堆喷重新分配刚刚释放的 0xaabb0000
,并伪造 msg_msg 结构体将 m_ts 设置为 PAGE_SIZE - MSG_MSG_SIZE
.
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
PAGE_SIZE - MSG_MSG_SIZE, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
然后通过 msg_msg(V) 所在的队列接收消息从而泄露出 B2 的 m_list,从而知道 B 的地址,因为 B 和 B2 通过 m_list 形成双向链表.
然后释放 sk_buff 并再次堆喷 sk_buff ,此时伪造 msg_msg->next 为 B 的地址,从而泄露出 B2 的地址,由于 B2 和 A2 相邻所以可通过计算得到A2的地址.
// Put kheap_addr at next to leak its content. Assumes zero bytes before
// kheap_addr.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
sizeof(msg_fake.mtext), kheap_addr - MSG_MSGSEG_SIZE);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
得到A2的地址后,就再次释放 sk_buff 并堆喷 sk_buff,此时可以伪造出 msg_msg 中的 m_list,让其能够正常 unlink,然后通过 V 所在队列将其释放.
// Put kheap_addr at m_list_next & m_list_prev so that list_del() is possible.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, kheap_addr, kheap_addr, 0, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
printf("[*] Freeing sk_buff data buffer...\n");
if (read_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), MTYPE_FAKE) < 0)
goto err_rmid;
释放后堆喷 pipe_buffer,就能够让 pipe_buffer 和 sk_buff 在一个内存中.
然后读取&释放 sk_buff 的内容即可泄露出 pipe_buffer->ops,最后再次堆喷修改 pipe_buffer->ops 进行 ROP
总结
- 可以根据 mtype 选择释放、读取特定的消息.
- msg_msg的接收通过遍历 m_list.next ,然后匹配 m_type 完成.
漏洞成因是由于整数溢出导致校验绕过,最终可以进行堆溢出,漏洞的条件如下:
- vuln obj 的大小为 0x1000
- 溢出大小可控,由于漏洞的限制溢出数据
\x00
结尾 - vuln obj 的分配和溢出的不同时进行
堆布局
由于vuln obj 的分配和溢出的不同时进行,使用占位式布局,或者常规布局(先分配溢出对象在分配target obj)均可,只是成功率的问题。
占位式的思路:
- 首先分配大量的 msg_msg 结构。
- 然后释放其中一个。
- 分配 vuln obj,vuln obj 大概率会落在第二步释放的内存
- 然后溢出,就可以修改到下一个 msg_msg(V),将 V 的 m_ts 改大。
- 然后将其他 msg_msg 和 msg_msgseg 释放,并分配大量 pipe_buffer 或者 seq_operations,目的是让 V 的 msg_msgseg 后面有可以用来泄露的对象。
- 接收 V 的消息,把数据泄露出来。
常规布局:
- 首先分配 vuln obj.
- 然后分配大量 msg_msg,目的是让 vuln obj 后面跟一个 msg_msg 结构体.
- 触发溢出,修改 vuln obj 后面的 msg_msg.
信息泄露
在目标 msg_msgseg 后面放 pipe_buffer,然后修改 m_ts.
溢出转换为 UAF
我们可以通过往一个 msg 队列中多次 send 小消息让消息队列中塞上多个 msg_msg 结构,这些msg_msg结构通过 m_list 的双向链表链接在一起.
于是我们新建一个消息队列 Q2,往里面塞一些小消息,希望在 V 的 msg_msgseg 后面放上一个 msg_msg, 如下图所示
通过对 msg_msg V 的修改我们可以泄露 msg_msg B 的内容,从 msg_msg B 的 m_list 可以拿到 A 和 C 的地址,并且通过在堆喷的时候在msg 的payload中插入 msg 序号,我们可以知道 msg_msg B 对应的消息序号
然后把 B 后面的消息释放掉(C 和 C 后面的)。
然后再次分配一个消息,在消息里面伪造一个 msg_msg 结构体 F, F 的 m_list.next 指向 A,然后再次leak 读取 B 的 next 指针可以知道 B 的 F 的地址.
然后溢出 V ,修改 V 的 m_list.next 为 F 的 payload 部分,及伪造的 msg_msg 结构体.
这个时候通过 B 所在的队列 Q2 将 A 释放,在通过 sk_buff 重新分配 A 所在的内存,并在 sk_buff 中伪造好 m_list ,避免后面释放的时候 unlink 失败。
然后通过 V 和伪造的 F ,将 sb_buff 所在内存释放,然后分配 pipe_buffer,这样 pipe_buffer 就会和 sk_buff 位于同一块内存,然后将 sk_buff 释放,并再次分配 sk_buff 就可以控制 pipe_buffer.
溢出转换为堆块重叠
首先在 msgseg 后面放 msg_msg 并通过 m_list 泄露两个 msg_msg 的地址.
然后将泄露的两个块释放,并用 pipe_buffer 和 msg_msg 重新分配出来并布置数据
然后利用溢出修改 next 为一个 pipe_buffer 前一个块的中间,让内核在释放 4k msg_msg 时把 next 释放掉.
ps: m_list 在unlink 的时候如果校验失败不会panic内核,只是不进行 unlink 操作了,所以将 m_list 设置为可读的内存即可.
然后再次分配 1k 的内存,就会分配到和 pipe_buffer 重叠的堆块,然后修改 pipe_buffer->ops 然后rop。
溢出+fuse 修改 msg_msg->next 任意地址写
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;
// 代码 1
msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);
alen = min(len, DATALEN_MSG);
// 代码 2
if (copy_from_user(msg + 1, src, alen))
goto out_err;
// 代码 3
for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}
在代码2时利用 fuse 阻塞内核,然后通过溢出修改 next 指针,然后代码3时就可以任意地址写.
总结
- 堆喷的时候在数据中带上序号,便于泄露后进行查找.
- 溢出转换为 UAF 的思路.
- kfree 释放一个非对齐的指针(即指向某个slab object的中间)时,可以构造重叠堆块.
漏洞成因是驱动往 timeline->fences 里面新增 fence 节点时没有增加 fence 的引用计数,且kgsl_ioctl_timeline_destroy 和 timeline_fence_release 函数是否 fence 时存在竞争,导致UAF。
触发 UAF 的代码执行时序图如下:
- 首先线程2调用 dma_fence_put 减少 fence A 的引用计数,此时 fence A 的引用计数为0,会调用timeline_fence_release函数释放fence A.
- 然后线程1通过 ioctl 进入 kgsl_ioctl_timeline_destroy 执行图中左侧标红代码,该代码会遍历timeline->fences链表中的所有fence,通过
dma_fence_get
增加 fence的引用计数,注意此时 fence A 的引用计数为0,最后会将 fences 链表放到 temp 节点后面。 - 然后线程二执行图中右侧标红代码,作用是尝试将 fence A 从 timeline->fences 链表中移除(此时链表已经为空),然后调用
dma_fence_free
释放其所在的内存空间。 - 然后线程1继续执行后面的代码,访问到 fence A 时就会导致UAF.
UAF漏洞的常见利用思路就是通过堆喷控制UAF的内存数据,然后利用程序后面使用该内存的逻辑来实现一些漏洞利用原语,比如:
- 控制函数指针-> JOP/ROP
- 控制数据指针->任意地址读/写
- 控制控制程序逻辑的关键数据,比如 DirtyPipe
- ......
通过分析作者发现 dma_fence_signal_locked 函数中会使用 fence 结构体中的函数指针
int dma_fence_signal_locked(struct dma_fence *fence)
{
...
list_for_each_entry_safe(cur, tmp, &cb_list, node) {
INIT_LIST_HEAD(&cur->node);
cur->func(fence, cur);
}
...
}
不过由于内核开启了CFI,因此并没有通过修改该函数指针来ROP,最后漏洞的利用方案是利用 dma_fence_put
来将 UAF 转换为 Double Free。
由于dma_fence_free 实际上是调用 kfree_rcu 释放内存
void dma_fence_free(struct dma_fence *fence)
{
kfree_rcu(fence, rcu);
}
在漏洞利用方面 kfree_rcu 和 kfree有两个需要注意的点:
- kfree_rcu 并不是立即释放,而是过一段时间后才会去释放内存(也很快),延时释放。
- kfree_rcu释放的内存可能会在其他CPU上被释放,即可能会被放到其他CPU的空闲块链表中,在尝试堆喷重用该内存时需要注意。
扩大条件竞争窗口
再次看看需要竞争的代码
long kgsl_ioctl_timeline_destroy(struct kgsl_device_private *dev_priv,
unsigned int cmd, void *data)
{
// [a]
spin_lock(&timeline->fence_lock);
list_for_each_entry_safe(fence, tmp, &timeline->fences, node)
dma_fence_get(&fence->base);
list_replace_init(&timeline->fences, &temp);
spin_unlock(&timeline->fence_lock);
// [b]
spin_lock_irq(&timeline->lock);
list_for_each_entry_safe(fence, tmp, &temp, node) {
dma_fence_set_error(&fence->base, -ENOENT);
dma_fence_signal_locked(&fence->base);
// [c]
dma_fence_put(&fence->base);
}
spin_unlock_irq(&timeline->lock);
}
static void timeline_fence_release(struct dma_fence *fence)
{
// [d]
spin_lock_irqsave(&timeline->fence_lock, flags);
// [e]
list_for_each_entry_safe(cur, temp, &timeline->fences, node) {
if (f != cur)
continue;
list_del_init(&f->node);
break;
}
spin_unlock_irqrestore(&timeline->fence_lock, flags);
...
kgsl_timeline_put(f->timeline);
// [f]
dma_fence_free(fence);
}
为了实现漏洞利用我们的语气执行顺序为:d -> a -> b -> e -> f
。
条件竞争的第一步是要在 fence A 引用计数为0进入 timeline_fence_release
后,再调用 dma_fence_get
,作者的做法如下:
- 通过往
timeline->fences
加多个节点,让线程1一直在[a] -- [b]
的list_for_each_entry_safe
循环中 - 然后线程2通过其他代码分支(kgsl_ioctl_timeline_wait、关闭 kgsl_ioctl_timeline_fence_get 分配出来的fd)调用 dma_fence_put 将链表中的最后一个fence(last_fence)的引用计数减少为0,触发 timeline_fence_release 的调用,进入
[d]
尝试获取锁。 - 线程1遍历完链表进入
[b]
. - 线程2执行
e -- f
的代码块释放 last_fence 所在的内存。
当线程2释放 last_fence
后,我们要在线程1进入 [c]
前,通过 sendmsg
堆喷占位并控制 last_fence
,然后利用线程1调用 [c]
时再次释放 last_fence
,从而将需要竞争的 fence
对象的UAF,转换为 sendmsg
分配的内存的 UAF.
而且由于 kfree_rcu
延迟释放特性,我们需要增加=线程2释放 last_fence
后,线程1执行 [c]
前的时间窗口,作者则利用了内核抢占调度的特性扩大了条件竞争的窗口,示意图如下:
工作流程:
- 首先创建4个线程,其中 A 和 B 运行在 DESTROY_CPU 上, C 运行在 SPRAY_CPU 上, Main 无所谓。
- C 执行 kgsl_ioctl_timeline_wait 阻塞在驱动中.
- A 的调度优先级为 IDLE,B的调度优先级为 NORMAL, B 线程通过 read pipe 阻塞,然后会让出CPU给 A 执行,当 A 还在遍历 timeline->fences 时,Main 向 C 发送一个信号,唤醒 C,调用 timeline_fence_release 释放 fence.
- 同时 Main 往 pipe 中写入数据,唤醒B,由于 B 的调度优先级高于 A ,因此 B 会抢占 A,从而扩大
[b] -- [c]
的竞争窗口。 - 然后线程 C 堆喷控制刚刚释放的 fence.
- A 执行
[c]
将 fence 再次释放.
利用 signalfd 修改 slub 的 next
将漏洞转换为 sendmsg 对象的 UAF后,作者再将其转换为 signalfd UAF,从而修改 slub 的next指针:
- 通过前面的步骤,将 sendmsg 分配的内存释放.
- 堆喷 signalfd 重新占位刚刚释放的内存,由于是kfree_rcu释放的内存,需要多CPU喷.
- 然后通过recvmsg 释放第一步分配的内存,此时 signalfd 所在内存被释放并链入 slub 的空闲块链表.
- 利用 signalfd 的系统调用接口,修改 freelist ptr,指向我们可控的内存区域.
- 然后触发其他对象的分配,这些对象就会落在我们可控的区域,通过修改这些对象完成提权.
内核中可预测的地址
作者通过分析在内核启动早期预分配的一些物理内存,其虚拟地址和物理地址之间的偏移是固定的,就是说这些地址是不随机化的,比如 [SWIOTLB ](https://securitylab.github.com/research/one_day_short_of_a_fullchain_android/ ,以及本文使用的 user_contig_region
ION 内存,这块内存可以通过打开 /dev/ion
文件,然后调用 ioctl 分配内存并可以通过 mmap 将其映射到用户空间,且其虚拟地址是固定的。
因此我们就有了可以在内核空间固定地址放置数据的能力,将 freelist ptr 指向 user_contig_region
,并在用户态将该内存映射到用户空间,当后面有对象分配时,我们就可以在用户态控制这些对象.
还有就是在三星内核中, vmemmap
的地址是固定的,这意味着我们可以知道任何一个物理地址对应的 page
结构体的地址 (vmemmap[pfn]
)。
https://labs.taszk.io/articles/post/exploiting_huaweis_npu_driver/
On the Galaxy S20 both the vmemmap and the linear mapping begins at a fixed address and they are not randomized. On the Huawei phones they are both randomized with the same seed, the addresses containing around 8 bits of entropy, consequently the base address of the vmemmap can be calculated from the linear map base and vice versa.
实现任意物理内存读写
ion_buffer 对象的 sg_table->sgl->page_link 是一个指向 page 结构体的指针,通过修改该 page 结构体指针到任意物理地址对应的page结构体,然后用户态mmap就可以实现任意物理内存读写.
struct ion_buffer {
...
struct ion_heap *heap;
...
struct sg_table *sg_table;
};
struct sg_table {
struct scatterlist *sgl; /* the list */
unsigned int nents; /* number of mapped entries */
unsigned int orig_nents; /* original size of list */
};
struct scatterlist {
unsigned long page_link;
unsigned int offset;
unsigned int length;
};
由于三星内核镜像的物理基地址是随机化的,首先需要泄露内核的虚拟基地址,流程如下:
- 泄露 ion_buffer->heap,由于 ion_buffer->heap 在 low memory 区域分配,其虚拟地址和物理地址之间的偏移固定,因此可以知道 ion_buffer->heap 的物理地址
- 修改 sg_table->sgl->page_link 指向 ion_buffer->heap 的物理地址,然后读取 ion_buffer->heap 的内容,通过 heap->ops 泄露内核的虚拟基地址,减去固定偏移得到内核的物理基地址。
总结
- 条件竞争的窗口可以通过结合程序自身逻辑(循环),调度器特性来增大.
- 对于对象中没有敏感成员(函数指针、数据指针)的UAF,可以将其转换为Double Free,进一步转换为另一种对象的UAF,来实现利用。
- ION这种不受随机化影响且用户态可控数据的地址可以看看其他平台或者手机存不存在.
- 通过修改 page 结构体指针实现任意物理内存读写的思路也非常有意思.