treasure_hunter
1 | GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.6) stable release version 2.35. |
1 | treasure_hunter: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=31386191e745f7d03c572b792bd501c102ba33f3, for GNU/Linux 3.2.0, not stripped |
- 64位,dynamically,全开
1 | You are a treasure hunter hoping to dig out as much gold as you can. |
漏洞分析
堆溢出漏洞:
1 | page = malloc(size); |
入侵思路
题目有两处可能可以利用的打印:
1 | if ( random_list[site] ) |
- 打印位于堆(mmap)上的随机数
1 | printf( |
- 打印位于 BSS 的堆地址
通过伪造 hash 可以使 site 特别大,从而在 random_list 上造成溢出
下面是提取出的 hash 逻辑:
1 |
|
转化为 python 脚本:
1 | def hash(input): |
泄露样例如下:
1 | site = [228,1368,3783,3182,3625,3347,349,1557,798,2864,1766,3149,2107,3684,601,1260,235] |
接下来只能尝试劫持 tls_dtor_list_addr,但在此之前需要先泄露位于 tls 上的 key 值(全局变量名称为 __pointer_chk_guard_local
)
由于题目本身的限制,不能直接泄露 tls 上的 key 值,只能从其他地方泄露 key 值,全局搜索 key 值可以找到如下的地址:
1 | pwndbg> search -t qword 0xfcbb79539c458355 |
- PS:该地址位于 ld 上,需要先泄露 ld 的基地址(由于 cpp 的影响,偏移不唯一但命中概率挺高的)
泄露 key 值后,尝试劫持 tls_dtor_list_addr(fs_base-0x78)
即可
完整 exp 如下:
1 | # -*- coding:utf-8 -*- |
kpid
1 | Linux version 6.1.75 (hyh@uu22) (Ubuntu clang version 17.0.6 (++20231209124227+6009708b4367-1~exp1~20231209124336.77), GNU ld (GNU Binutils for Ubuntu) 2.38) #2 SMP PREEMPT_DYNAMIC |
1 | !/bin/sh |
- smep,smap,kaslr,pti
1 | !/bin/sh |
漏洞分析
1 | memset(buf.field_28, 0, sizeof(buf.field_28)); |
- 内核模块会调用
kernel_clone
,可以将这个功能当成一个 fork
漏洞点是 pid UAF:
1 | if ( dest_cnt ) |
- 释放了 pid 却没有释放该进程,导致后续可以通过该进程对 UAF slab 进行修改
题目给出提示:Dirty Pagetable
- Dirty PageTable 是一种针对堆相关漏洞的利用手法,主要就是针对 PTE 进行攻击
- 利用手段可以参考:Dirty_Pagetable (yanglingxi1993.github.io)
在 x86-64 Linux 中,通常使用 4 级页表将虚拟地址转换为物理地址
- Dirty Pagetable 以 PTE(页表条目)为目标,这是物理内存之前的最后一个级别
- 在 Linux 中,当需要新的 PTE 时,PTE 的页面也会使用 Buddy 系统进行分配
受害 pid 对象的计数字段与有效的 PTE 重合
1 | struct pid |
- count 字段是 pid 对象的第一个字段(8 字节对齐),尽管 count 字段大小为 4 个字节,但它恰好与 PTE 的较低 4 字节重合,因此我们可以通过计数器来修改 PTE
- 由于进程中的 fd 资源有限,它最多只能添加 32768 进行计数,为了打破这个限制,我们可以利用 fork 在多个进程中执行增量原语,此操作允许我们向受害者 PTE 添加足够大的数字
我们可以通过 mmap 来快速分配大量页表:
1 | void *page_spray[N_PAGESPRAY]; |
- Linux 内核是惰性的,当 mmap 创建内存时并不会为其绑定页表,只有在第一次读写时才会通过缺页处理来进行绑定
在某些情况下,内核空间和用户空间需要共享一些物理页面,实现的机制很多但这里选择 dma-buf 系统堆:
1 | dma_heap_fd = creat("/dev/dma_heap/system", O_RDWR);; |
- 共享页面由用户空间中的
dma_buf_fd
表示,可以通过 mmap 将共享页面映射到用户空间 - 从 dma-buf 系统堆分配的共享页面基本上是从页面分配器分配的(实际上 dma-buf 子系统调整页面池进行优化,但这在利用时不会打扰我们,所以这里不再讨论)
入侵思路
由于开启了 CONFIG_SLAB_MERGE_DEFAULT
选项,UAF 对象 pid 和普通 slab 隔离,这里需要一种 Cross Cache UAF 的技术
- 这里使用的技术和页级堆风水实现 Cross Cache Overflow 的有所不同
- 但利用伙伴系统回收整个页面的思路是一致的
在 free victim slab 之后,free 掉同页面其他 object,再满足一系列条件就可以让整个 page 被 buddy system 回收:
- 目标 object 所在的 page 不是
s->cpu_slab->page
- 目标 object 所在 page 满足
page->pobjects > (s)->cpu_partial
- 目标 object 所在 page 位于
freelist
且page.inuse
为 “0”
触发方法:(参考文章:Linux 内核利用技巧: Slab UAF to Page UAF-安全客)
- 创建一批 objects 占满 cpu_partial + 2 个 pages,保证 free 的时候
page->pobjects > (s)->cpu_partial
- 创建 objects 占据一个新的 page,但不占满,保证
c->page
指向这个 page - free 掉目标 page 的所有 objects,使这个 page 的
page.inuse == 0
- 剩下的每个 page free 一个 object 用完 partial list 后就会 free 掉目标 page
查看基本信息:
1 | /home/ctf |
- 可以通过
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set)
将cpu_partial
降低为“8”
入侵的第一步是整理 pid slab,为此我们需要大量调用 fork 来申请足够的 pid 直到 pid slab 耗尽,进而向伙伴系统申请空间
1 | for(int i=0;i<2*objs_per_slab;i++){ // alloc 2 pages |
- 先申请 2 个 page 大小的 pid slab
接下来可以调用内核模块的 UAF,申请剩下 cpu_partial 个 page 大小的 pid slab:
1 | kernel_fork(); |
- UAF slab 将会是这些页面中的其中一个 pid
- 这里选择使用 AF_UNIX socket 来修改
pid->count
- 子进程开启监听
- 父进程通过 connect 来增加
pid->count
- 由内核模块生成的子进程将是控制 UAF slab 的关键,需要专门创建两个管道来维持父子进程的通信
接下来我们需要将 UAF pid 释放掉,并用 PTE 将其覆盖,主要就是满足 pid page 被伙伴系统回收的条件:
1 | /* <----- free 掉目标 page 的所有 objects && 剩下的每个 page free 一个 object -----> */ |
- 被伙伴系统回收的条件即将满足(除了最后一个 page 为半满,其他的都为空)
1 | int start = 0; |
- 接下来释放的每一个 pid 都有可能导致带有 UAF pid 的 page 被伙伴系统回收
- 我们需要一边释放一边用 PTE 来占用 UAF pid
现在可控的 UAF pid 已经被某个 PTE 占据,我们可以先用增量原语修改这个 PTE 条目,然后将命中的 PTE 堆喷出来:
1 | for (int i = objs_per_slab * 8; i < objs_per_slab * 9 - 1; i++) { // 128*31 + 127 = 4095 |
使用 munmap 释放 PTE,然后用 dma-buf 占据 UAF pid:
1 | munmap(evil, 0x1000); |
如果我们执行增量原语,将 0x1000、0x2000、0x3000 等添加到受害者 PTE 中,我们将有很大的机会使受害者 PTE 与用户页表相关联:
- 通过增量使 victim PTE 索引到另一个 PTE
- 该 PTE 极有可能为
page_spray[i]
中某个页面的页表项
通过 victim PTE 修改页表项为内核代码段的页表项,堆喷并泄露数据:
1 | char *victim_ptable = NULL; |
最后调整好偏移,修改 setresuid 函数的权限检查逻辑即可
完整 exp 如下:
1 |
|