Dirty Pagetable 学习 & 例题
Dirty Pagetable 学习 & 例题
Reference:
https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html
https://ptr-yudai.hatenablog.com/entry/2023/12/08/093606
1. Dirty Pagetable 介绍
顾名思义这是一个与页表相关的利用,该技巧可以基于堆漏洞完成利用,简单来说该技巧就是去操纵用户页表,通过这种方法可以让攻击者获取到强原语:任意物理地址读写。
该方法有以下优点:
- 基于数据(data-only exploitation)就可以完成利用,不同于其他的传统的劫持内核控制流的方法,这种方法能够绕过 CFI、KASLR、SMAP/PAN 等等保护。
- 该技巧可以用在最新的 linux 内核上。
- 该技巧可以显著提高基于堆漏洞利用成功的效率。
2. Dirty Pagetable 原理步骤
这里结合一个 UAF 来进行说明利用方式。
- 在一个包含 uaf object 的 victim slab 上触发 UAF,并且将该 slab 其他 object 释放从而让其被页分配器回收。
- 使用用户页表占用上一步释放的 victim slab,从下图中可以看到 pte 页表项占用了我们上一步释放的 victim object(UAF 堆块)。
这一步我们就可以建立写原语去修改页表项
通常来说这一步是最困难的,因为在真实环境中我们想通过一个 UAF,去建立写原语修改它是复杂的,但是这篇文章中作者通过 file UAF 和 pid UAF 给了我们如何将这种 UAF 漏洞去改变 PTE。
修改 PTE 给内核打“补丁”
为完成提权我们可以 patch 几个系统调用,例如
setresuid()
、setresgid()
等,我们可以从非特权进程中直接执行它们进行提权(参考我在 n1ctf pramoon 中博客的做法)。再开了 SELINUX 时,也可以对 SELINUX 中的变量进行 patch 使它的功能失效。enjoy your root!
3. 通过 file UAF 利用 Dirty Pagetable
原文中作者提出了三种 file uaf 的利用方式:
第一种
通过具有高优先级文件的 file object 去占用我们释放的 victim file object,比如 /etc/crontab
,在这之后我们可以通过写 victim object 去提权获取 root。这种方法的缺点在于:
- 对竞争的要求比较高需要一些运气
- 由于安卓上大多数高优先级文件都位于只读文件系统中,所以不能通过写原语来提权
- 这种方法不能够用来容器逃逸
第二种
第二种方法是攻击系统库和可执行程序的页缓存,可以向程序中注入恶意代码,类似与 Dirty Pipe 的利用手法,这种方法的最大优点是比较稳定,不要条件竞争就可以提权。但这种方法依然很难用来解决第一种方法中提到的后两个问题,而且该方法不能被用来其他类型的 UAF 漏洞。
第三种
第三种方法是基于 cross-cache attach
的视角来进行利用,不过要完成利用得先绕过 KASLR。
4. 例题 m0leCon Finals 2023-keasy
原文作者是通过 CVE-2022-28350 来实现的,但由于在网上没有找到关于该漏洞的详细介绍,也没有详细的 poc,所以对于一个 kernel 菜鸟来说想复现已经结束咧,但针对这种利用技巧,m0lecon 的出题人已经为我们准备好了一个题目 keasy, 这道题不得不说又涨了波思路,感觉这种上过 blackhat 的技巧都很实用。
话不多说,我们直接开始吧!
1 |
|
启动脚本如上所示,与我们常见的 kernel 题大差不差,该开的保护也都开了,不做过多说明。
静态分析
这道题源码其实很简单
漏洞点
漏洞点就在于上面代码中在 copy 失败后会直接 fput(v4) 释放 inode 对象。
利用过程
利用分析
Cross-Cache Attack
由于 file object 和 page table 所在的 kmem_cache 之间是独立的,所以说我们想仅在一个 slab 内完成 UAF 是不现实的,因此这里需要借助 Cross-Cache Attack 完成利用,关于这个技巧可以参考VERITAS501师傅 –> Cross Cache Attack ,里面讲的很明白。
堆喷 file object & get victim fd
为完成 Cross-Cache Attack 的需要,我们需要先进行堆喷 file object,先让 file object 占据足够多的 slab(一个页大小),步骤如下。
1 | /** |
为更进一步理解,这里用参考1 Nicolas Wu 的图说明(画的很好,懒得自己画了,只能盗大佬的图了)。
可以看到在经过上面的过程后 victim fd 指向我们前面所说的 UAF file object 对象,下图为调试过程中正要释放时的 file object 的数据,记住这个 f_count
待会会用到。
回收 victim slab 到 page allocator
这一步依然是完成 Cross-Cache Attack 的过程,将之前分配的所有 slab 全部释放,其中包含有 victim file object 的 slab 将会被 page allocator 回收。
1 | output_msg ("STEP.2 Release all the filp struct"); |
通过 pte 占用 victim slab
我们先通过 mmap 映射得到对应的虚拟地址,这一步内核并不会直接我们分配对应的物理地址空间,而是在执行 *(char *)(spray_page[i] + j*0x1000) = 'A' + j;
后,会触发缺页,此时内核才会为我们真正分配虚拟地址对应的物理地址空间,并填充页表项,建立虚拟地址和物理地址之间的映射关系。
1 | // This step is to prepare spray PTEs the follow |
从上图可以看到,与前面 file object 那张图比较可以发现,同样的位置已经被页表项填充了,这样就完成了我们这一步的目的,参考下图理解。
注意这里需要提一下,页表项的构造,在上面的页表项中低 12 位用作标记位,其中 0x122a0b 才是对应的页框号
建立增量原语并定位 victim PTE
这里的 victim PTE 可以理解为其实就是我们前面提到的跟 f_count 占据同一个 8 字节的页表项,增量原语也不要理解的过于复杂,实际上就是想办法能够让我们的 PTE 中的内容能够增加,而这个增量原语就是要通过我们前面一直提到的 f_count 来实现,先来看看一个 file object 结构体的字段有哪些(借助 phole 得到):
1 | struct file { |
可以看到 f_count 位于 56 字节到 64 字节,我们可以通过 dup(fd)
,来让 f_count 实现增原语,可是别忘了我们此时这个位置已经被页表项填充了,所以说我们实际上可以通过 dup(fd) 来完成修改页表项的内容,假设下图为我们初始情况,每一个页表项对应一个大小为 0x1000 的物理地址空间,此时 victim pte 还是指向自己的 page。
我们调用 dup(fd)
0x1000 次后的 victim pte 指向情况如下图所示,
可以看到此时 victim pte 已经指向了另外一个地址空间,这使得它从用户态访问其内容时就会发生变化,我们可以通过这一点来找到 victim pte,为了方便读者进一步理解,我这里在放出 pte 真实情况下的状态。
这里借助一个 gef 插件神器 可以直接看物理地址的内容,会自动完成地址转换,可以看到其内容已经由之前指向的 0x48 变为了 0x41(‘A’) 。
Catch a page table again
有了前面的那一步,细心地读者是不是想到如果一直 dup,让页表项指向一个内核 text/data 物理地址,是不是就可以任意读写呢,可事实是我们mmap出来的区域和内核 text/data 的位置差了很多,在资源有限的情况下我们不可能一直 dup,所以说这里我们需要用到另外一个办法。
通过前面的内容我们知道,我们已经实现了让两个页表项指向了同一个位置,如果我们使用 munmap()
释放 page,将会得到一个 page UAF,然后我们在用 page table 去填充该 page 就可以完成对 page table 的修改了,可是事实并非如此,一个重要原因是从 anonymous mmap()
中分配得到的页来自于 MIGRATE_MOVABLE free_area of the memory zone,而用户页表分配的 page 来自于 MIGRATE_UNMOVABLE free_area of the memory zone,所以说这使得我们要得到这个 page 的可能性非常低。
这里就需要使用到原文作者提到的另外一个手法 ,通过/dev/dma_heap/system
完成利用,有两点原因:一是该文件在安卓中可以由不受信任的 app 使用;二是 dma-buf 的实现相对来说比较简单。
使用代码如下:
1 | struct dma_heap_allocation_data data; |
我们可以通过 mmap() dma_buf_fd
实现共享页面在用户空间和内核之间的映射。这里我们需要注意到一件非常重要的事就是该共享页面从何而来,从那里分配得到,幸运的是其和 page table 一样都从 MIGRATE_UNMOVABLE free_area
分配得到,同时它们都从相同 order 下的分配页面。这使得我们可以有了如下做法:
- step.1 堆喷 page table
- step.2 申请一块共享页面
- step.3 堆喷 page table
然后就有了下面的分配情况,
下面要做的就是重新映射用户空间和内核空间地址(由munmap实现),再将 dma_buf 的物理空间映射到用户空间,需要知道的是 dma_buf 所在的物理位置是和 page_table 紧邻的(从相同order分配)。
可以看到重新映射的页表项内容和之前的页表项发生了很大的变化,上图红框中的页表项其实就对应下图中的一个状态(已经被 dup 0x1000 次后的情况)。
上图中 0x00000011e64d000
开始后的内容实际上就是我们 dma_buf 重新映射后的空间,而这是一个 page table,这已经意味着我们可以随心所欲的修改 page table 到任意地址,简单来讲我们已经可以**任意地址写(AAW)和任意地址读(AAR)**了。
AAR & AAW
leak kernel base
尽管有一些随机化的保护,可是仍然在一些固定地址处会存在内核地址之类的关键信息
可以参考上面一些地址区间(开了随机化),正常不开 kaslr 的时候,上面这些地址除了第一个都是从 0xffff888000000000
开始的,所以说自己找的时候可以加个偏移来看。
像下面 0x2c01000 物理地址处就会保存有一个 内核地址,至于为什么有两个,相信大家应该都听过 direct mapping area
,其中这个空间中会保存其他所有页面的映射,这也解释了为什么会出现两个。
nsjail 逃逸
这一块涉及到笔者盲区了,所以直接拿原作者中的 shellcode 来打了,思路是通过任意写去 patch do_symlinkat
系统调用的函数功能,从而可以该调用直接在 nsjail 中进行调用,完整 shellcode 如下:
1 | init_cred equ 0x1445ed8 |
最终结果
final exp
1 |
|
- Title: Dirty Pagetable 学习 & 例题
- Author: henry
- Created at : 2024-04-10 23:06:34
- Updated at : 2024-04-10 23:11:24
- Link: https://henrymartin262.github.io/2024/04/10/Dirty-pagetable-study/
- License: This work is licensed under CC BY-NC-SA 4.0.