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
。