RWCTF2023体验赛 write up for Digging into kernel 3
Kernel Pwn 碰到 initrd 只要无脑爆搜内存就行
前言
因为学业上的问题摆了挺久所以一直没咋弄 Pwn…所以你可以看到这篇文章距离咱的上一篇文章的间隔大概有 2 年(咕咕咕🕊🕊🕊
不过计科基础也一直有在夯实(毕竟科班出身基础不夯实就挂科(*´∀`*) ←面对课业压力的绝望眼神),在巩固了 OS 基础之后最近也开始稍微入门一些 Linux kernel,正好听说 RWCTF2023 体验赛有一道入门级的 kernel pwn,所以打算从这里入手(←毕竟这个菜🐕也做不来难题
按照arttnba3 师傅的建议深入学了 OS 之后确实有种不一样的感觉,就是花的时间有些久 XD(←以及这个懒🐕实际上根本没咋动手
0x00.题目分析
首先按惯例查看 run.sh
,可以发现开启了 SMEP、SMAP、KASLR、KPTI 四大保护,这里我们注意的文件系统用的是最基础的 initrd
而并非是常规的 ext4 格式的磁盘镜像:
1 | #!/bin/sh |
注意到指定了 init
进程为 /init
,所以接下来我们来分析文件系统下的 init
文件,主要就是载入了一个内核模块 rwctf.ko
,以及设置了 flag 和 kallsyms 的权限,这意味着我们无法从 /proc/kallsyms
中获取到内核函数的基地址:(
1 | #!/bin/sh |
接下来我们对 rwctf.ko
进行逆向分析,发现有用的主要就是 ioctl 函数,提供了两个功能:
- 0xDEADBEEF:分配一个指定大小的堆块并能写入数据
- 0xC0DECAFE:释放一个指定大小的堆块,存在 UAF
1 | __int64 __fastcall rwmod_ioctl(int *a1, int a2, __int64 a3) |
这里我们先将与题目进行交互的 API 写出来:
1 | int dev_fd; |
0x01.漏洞利用
由于题目直接就有一个没有任何限制的 UAF,所以这道题的解法就是多种多样的了,这里咱选择用一种比较简单的内存空间搜索解法
Local Descriptor Table in kernel
在 Linux 内核当中使用 ldt_struct
来表示一个 Local Descriptor Table
,其定义如下所示,有一个 entries
指针指向一块描述符表的内存,nr_entries
表示 LDT 中的描述符数量:
1 | struct ldt_struct { |
我们主要关注于从用户空间的角度该结构体如何能够为我们所用,Linux 提供了一个 modify_ldt()
系统调用来操纵该结构体:
1 | MODIFY_LDT(2) Linux Programmer's Manual MODIFY_LDT(2) |
在内核空间中对应该函数,主要是读和写两个功能:
1 | SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr , |
对于 write_ldt()
而言其最终会调用 alloc_ldt_struct()
分配 ldt 结构体,由于走的是通用的分配路径所以我们可以在该结构体上完成 UAF :)
1 | /* The caller must call finalize_ldt_struct on the result. LDT starts zeroed. */ |
而 read_ldt()
就是简单的读出 LDT 表上内容到用户空间,由于我们有无限制的 UAF,故可以修改 ldt->entries 完成内核空间中的任意地址读:
1 | static int read_ldt(void __user *ptr, unsigned long bytecount) |
但是本题开启了 KASLR,因此我们还需要泄露内核空间的地址 QAQ
不过这一步也可以通过修改 ldt_struct
完成,这里我们要用到 copy_to_user()
的一个特性:对于非法地址,其并不会造成 kernel panic,只会返回一个非零的错误码,我们不难想到的是,我们可以多次修改 ldt->entries 并多次调用 modify_ldt() 以爆破内核的 page_offset_base,若是成功命中,则 modify_ldt 会返回给我们一个非负值
page_offset_table
对应的实际上是直接映射区(direct mapping area),即该块虚拟内存为对所有物理内存的映射,因此我们可以直接搜索整个物理内存空间:
1 | ffff888000000000 | -119.5 TB | ffffc87fffffffff | 64 TB | direct mapping of all physical memory (page_offset_base) |
内核的堆分配也是从这一块内存上取
但若是内核开启了 hardened usercopy 检查,则我们不能直接通过修改 ldt->entries
的方式拷贝内核空间的数据,因为该保护会检查源地址对应的内存类型(是否为一个堆块或是其他的类型,若是堆块则检查其大小之类的)> <
这里我们参照 arttnba3 师傅博客中 利用 fork 进行 hardened usercopy 绕过 的做法,当父进程 fork 时,会通过 memcpy 将父进程的 ldt->entries 拷贝给子进程:
1 | /* |
该操作是完全处在内核中的操作,因此不会触发 hardened usercopy 的检查,我们只需要在父进程中设定好搜索的地址之后再开子进程来用 read_ldt() 读取数据即可
initrd 与 flag 搜索
Initrd ramdisk 或者 initrd
是指在启动阶段被Linux内核调用的临时文件系统,用于根目录被挂载之前的准备工作,其通常被压缩成gzip类型,开机时由bootloader(如LILO、GRUB)来告知核心initrd的位置,使其被核心存取,挂载成一个loop型态的档案;在2.6版本内核之后出现了initramfs,它的功能类似initrd,但是它基于CPIO格式,无须挂载就可以展开成一个文件系统
这段抄自维基百科 > <
现在 CTF 中的 kernel pwn 大都使用 initramfs 构建文件系统,在带来出题便利性的同时也带来了一个问题:文件系统中所有的内容都会被载入到内存当中,这也包括 flag
,因此我们可以通过搜索内存空间的方式直接获取到 flag 的内容(・ω・´)
exploit
最后的 exp 就是下面这个样子啦,先用 ldt_struct
爆破 page_offset_base
再搜索 flag 即可(・ω・´)
1 | #define _GNU_SOURCE |
运行就可以获得 flag 了:
RWCTF2023体验赛 write up for Digging into kernel 3
http://meteorpursuer.github.io/2023/02/05/RWCTF2023_Digging_into_kernel3_wp/
来做第一个留言的人吧!