0x07. Linux kernel基础:cross-cache & heap-level heap fengshui
buddy system的利用手法
[toc]
Linux kernel之cross-cache & page-level heap fengshui
0x00. 基础知识:为何需要 cross-cache
之前的利用手法基本上是针对slub allocator的,但这种手法并不是万能的,例如在corCTF 2022-cache-of-castaways这道题目中,题目在常见保护全开的情况下,还开启了CONFIG_MEMCG_KMEM=y,并在此基础上给出了一个自定义的cache中的6字节堆溢出。此时,由于其位于一个独立的cache,我们无法使用将其转换为UAF来进行结构体复用的思路。
cross-cache是一种针对buddy system的利用手法。buddy system 的分配基础是page,每一层的page数量比上一层多一倍。当本层的page用完后,则取出下一层的所有pages,其中一半返回给本次请求的上层调用者,而剩下的一半挂入当前层。如图:

由此,我们可以得知:当前order的pages消耗完毕时,buddy system将会分配一段物理内存上连续的pages给上层调用者和当前order。这就使得我们有机会使得我们在私有cache上的溢出能够在物理内存上溢出到任何结构体。但很显然这种溢出还需要严苛的页级堆风水,这就需要用到下面的page-level heap fengshui来完成后面的操作。
0x01. 基础知识:page-level heap fengshui
理想情况下,当我们获得了一片连续的物理内存页,如下(a)所示:

假设在a我们已经获得了一片连续的物理内存页,此时我们每间隔一页,free掉整页,就来到了如图b中的情景。
此时,我们将释放掉的内存页面通过申请victim obj来全部占用,就来到了如图c中的场景。
接下来如法炮制,释放刚刚没有释放的页面,来到如图d中的场景。
再将释放的页面通过vulnerable obj来申请,就到了如图e中的场景。此时,vulnerable obj若发生溢出,则在每个page相邻处,就有可能可以溢出到victim obj,完成cross-cache的利用。
注意到,我们上述操作中有对整个页面的申请和释放操作。而这种操作可以利用CVE-2017-7308提供的方式来完成,即使用 setsockopt 与 pgv 完成页级内存占位与堆风水。这里笔者不多赘述,感兴趣的读者可以参考arttnba3师傅的博客和星盟师傅的博客进行详细的了解。此处我们使用arttnba3师傅写好的板子,来将其视为一个api来进行调用QAQ。
0x02. 基础知识:板子一览
我们定义好如下变量和函数:
1 | |
随后,使用时,通过如下方式初始化:
1 | |
该函数会创建一个子进程,该子进程作为一个服务一直在后台运行。
可以简单地通过alloc_page和free_page函数来向该服务发送请求,申请或者释放整个页面。
一个demo如下:
1 | |
0x03. corCTF 2022-cache-of-castaways
题目信息
题目给出了kconfig.txt。除了开启常见的所有保护外,还开启了CONFIG_MEMCG,这使得不同flag下创建的obj将不会合并。
而题目自己手动创建了一个cache:
1 | |
可以看到,创建了一个名为castaway_cache的cache,且其中的obj的大小为0x200。新版内核中不同名称的cache不再会相互复用,因此题目的cache和kmalloc-512将为两个不同的cache。
题目逻辑很简单,有创建和编辑obj两个功能:
1 | |
其中,编辑obj的过程中,出题人特意给出了一个6字节的溢出:
1 | |
而这里,由于cache的隔离,我们无法通过该堆溢出来溢出到任何可用的结构体,又由于不知道任何地址,因此也无法简单地利用freelist来进行劫持。因此,我们这里需要一种cross-cache的利用方法。
解题思路概览
这里,我们若能够完成cross-cache的溢出,则不难想到可以让6字节溢出到别的结构体,而最简单的方法就是溢出到cred结构体。其定义如下:
1 | |
若我们能够溢出到某个子进程的cred结构体,并覆盖其uid为0,则该子进程就会表现出root权限的状态。需要注意引用计数不能为0。
而溢出到该结构体的方法也就是cross-cache的打法了。考虑如下流程:
- 首先,利用
fork()来消耗当前cred的slab中剩余的cred obj。由于一个slab中默认有21个cred obj,那么我们只需要创建大约35个子进程即可以用来消耗当前cred属于的slab。 - 利用上面提到的方法大量喷射
pages,即申请大量的页面以备用。在这个过程中,由于当前的slub的pages被消耗完毕,因此会从buddy system来申请物理地址上连续的内存页。 - 随后,释放奇数即
1、3、5、7、9...的页面,并通过clone系统调用来大量创建子进程,从而在子进程创建时申请cred obj来占用刚刚释放的奇数页面。 - 释放偶数即
0、2、4、6、8...的页面,并通过题目功能,申请题目的obj(称之为vulnerable obj),并编辑该申请到的obj使其溢出6个字节。 - 在偶数页面和奇数页面相邻处,偶数页面上的
vulnerable obj便有可能溢出到奇数页面上的cred obj。在溢出时设置溢出的uid为0,同时保持引用计数usage不为0,即可让该cred obj对应的子进程的用户权限变为root。我们让该变为root的子进程使用execve来启动一个shell即可。
使用clone创建子进程
在上述过程中,我们提到使用clone来创建子进程,从而申请cred结构体。有的师傅可能注意到,这里并没有使用fork来进行申请,而是使用了不太常见的clone。这是因为fork过程中会申请很多的obj来干扰到堆块,称之为“噪声”。因此我们选用clone来进行子进程的创建。
而clone()过程中,也并不是不会产生噪声——会产生,而且根据其参数不同,执行的分支不同,产生的噪声也不相同。这里我们选择一个噪声最少的分支,即使用如下参数(标志位)来调用clone():
1 | |
而该分支会创建的obj如下,共有4个order 0的obj:
1 | |
而当设置了CLONE_VM这个标志位时,子进程和父进程的内存会共享:这意味着子进程若调用函数,很可能会影响父进程的执行状态。因此在子进程中执行的函数中,我们要完全使用shellcode来编写。
子进程检查是否获得 root 权限
通过clone()产生的子进程需要时刻注意自己是否已经获得了root权限。通过轮询来检查自己是否获得root权限显然不够优雅,因此可以利用pipe管道来实现该功能。每个子进程都使用read(check_root_pipe[0], child_buf, 1);这样的方式来从管道中读取一个字节的内容,而管道中没有数据时,其属于阻塞状态。当整个流程执行完毕时,我们只需要向管道另一侧写入数据,即可触发子进程检查自己的uid的流程。
例如一个示例如下:
1 | |
解题脚本
带注释的exp如下:
1 | |
0x04. 常见问题
我如何知道某个obj位于哪个order?
查看/proc/slabinfo中的pagesperslab字段。若其值为1,很明显其位于order 0 ,其值为2则位于order 1以此类推。
为什么子进程检查是否获得root的函数要使用shellcode来编写?
这是因为该子进程是和父进程共享内存的。若子进程修改了父进程的内存,则可能导致父进程执行出错。因此,子进程这里的函数使用纯基于寄存器的shellcode来完成,便不会影响到父进程的内存。
获得shell后,程序寄掉了。
这里在子进程中获得shell后,还需要在后面让该子进程sleep,否则子进程继续执行其他内容会出错。
我如何知道宏定义中,每种obj需要喷射的数量?
这里,笔者的计算方式如下:
- 对于
cred初始状态下将其消耗的数量,我们查看/proc/slabinfo,得知需要消耗21以上的该obj。经过测试这个数量需要略大于一些,例如设置为30-35才比较稳妥。 - 对于
cred obj喷射的数量,我的计算方式如下:题目提供的obj大小为0x200,而可以喷射的数量为400;因此,我们需要使得喷射的cred obj占满的页面数量和题目obj占满的页面数量差不多大才可以尽可能大概率地溢出到cred。因此,简单地让cred结构体占用的大小和题目obj的大小差不多就可以了。计算方式为:(0x200 * 400) / 192 ≈ 1066。