core 复现
文件如下:
- bzImage:压缩的内核映像
- core.cpio:文件系统映像
- start.sh:用于启动 kernel 的 shell 的脚本
- vmlinux:静态链接的可执行文件格式的 Linux 内核
如果没有 vmlinux,就需要使用 extract-vmlinux 进行提取,不过我更喜欢用 vmlinux-to-elf:
1 | /* vmlinux-to-elf [core.cpio] [vmlinux] */ |
- 提取出来的 vmlinux 文件没有原版的好用
- 但是在搜索 gadget 时,尽量使用提取出来的 vmlinux,防止两个 vmlinux 不一样
然后使用 Ropper 来寻找 gadget:
1 | ➜ give_to_player time ropper --file ./vmlinux --nocolor > g1 |
看一下启动脚本 start.sh:
1 | ➜ give_to_player cat start.sh |
- 内核开启了 kaslr 保护
尝试启动时我遇见了问题:
- 按照 wiki 上的提示,把 start.sh 中的 64M 改为 128M,但是还是无效
- 于是我改为 256M,成功了
1 | [ 0.023472] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie |
解压 core.cpio:
1 | ➜ core gunzip ./core.cpio.gz |
- 发现除了常规的文件目录外,还有个 gen_cpio.sh
1 | ➜ core cat gen_cpio.sh |
- 这是一个打包的脚本(shell脚本有点看不懂,还要多多学习)
看一下 core.cpio->init:(获取重要信息)
1 | ➜ core cat init |
- /proc/kallsyms 其实是内核符号表,拥有内核符号的地址
重新打包后,我们着重分析一下 core.ko 驱动文件:
1 | ➜ core checksec core.ko |
64位,dynamically,开了carnay,开了NX
这些函数就是驱动函数,也被称为 ioctl 函数:
- ioctl 是设备驱动程序中对设备的 I/O 通道进行管理的函数
- ioctl 函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对 ioctl 的支持,用户就可以在用户程序中使用 ioctl 函数控制设备的 I/O 通道
- 在驱动程序中实现的 ioctl 函数体内,实际上是有一个 switch{case} 结构,每一个 case 对应一个命令码,做出一些相应的操作,怎么实现这些操作,这是由每一个程序员自己控制的,因为设备都是特定的
驱动函数是 kernel 中容易出问题的点,接下来就看看这些函数:
- init_module:注册了
/proc/core
1 | void __fastcall init_module() |
- exit_core:删除
/proc/core
1 | void __fastcall exit_core() |
- core_ioctl:定义了三条命令,分别调用 core_read(),core_copy_func() 和设置全局变量 off
1 | void __fastcall core_ioctl(__int64 a1, int choice, __int64 a3) |
- core_read:从内核空间 from[off] 拷贝 64 字节到用户空间
1 | void __fastcall core_read(char *a1) |
- core_write:向全局变量
name
上写
1 | void __fastcall core_write(__int64 a1, __int64 from, unsigned __int64 len) |
- core_copy_func:从全局变量
name
中拷贝数据到局部变量中
1 | void __fastcall core_copy_func(__int64 a1) |
这是我的第一个内核题,我全程都是按照 wiki 上的提示做的,所以我会尽可能的复述解题的过程和思路,有些必要的知识也会进行补充
当我们打开这个 kernel 时:
1 | / $ whoami |
我们是普通用户权限,需要提权拿“flag”,这里先介绍一下权限和提权:
- 内核会通过进程的
task_struct
结构体中的 cred 指针来索引 cred 结构体,然后根据 cred 的内容来判断一个进程拥有的权限,如果 cred 结构体成员中的 uid-fsgid 都为 0,那一般就会认为进程具有 root 权限 - 内核提权指的是普通用户可以获取到 root 用户的权限,访问原先受限的资源,这里从两种角度来考虑如何提权
- 改变自身(Change Self):通过改变自身进程的权限,使其具有 root 权限
- 直接修改 cred 结构体的内容(需要先定位 cred,然后将其修改)
- 修改 task_struct 结构体中的 cred 指针指向一个满足要求的 cred
- 改变别人(Change Others):通过影响高权限进程的执行,使其完成我们想要的功能
- 改数据
- 改代码
- 改变自身(Change Self):通过改变自身进程的权限,使其具有 root 权限
具体的过程就不展开了
- 在本题目的环境中,因为程序有栈溢出漏洞可以控制程序执行流,所以可以通过 ROP 来调用 commit_creds(prepare_kernel_cred(0)) 进行提取
- 该方式会自动生成一个合法的 cred,并定位当前线程的 task_struct 的位置,然后修改它的 cred 为新的 cred
- 另外,该方式属于“改变自身”中的“修改 task_struct 结构体”
为了调用 prepare_kernel_cred 首先需要实现 ROP:
- 通过 ioctl 设置 off,然后通过 core_read() leak 出 canary
- 通过 core_write() 向 name 写,构造 ropchain
- 通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop
- 通过 rop 执行
commit_creds(prepare_kernel_cred(0))
- 返回用户态,通过 system(“/bin/sh”) 等起 shell
这又有一个问题,如何在 shell 中使用这些驱动函数呢?在C语言中有专门的接口:
1 | ioctl(fd, function_num, var); |
使用这个函数的前提是知道 function_num(可以直接在 core_ioctl 的 Switch-Case 中找到它)
接下来介绍利用GDB调试的方法:
- 使用
gdb ./vmlinux
可以进行调试 - 虽然加载了 kernel 的符号表,但没有加载驱动
core.ko
的符号表,可以通过add-symbol-file core.ko textaddr
加载 - .text 段的地址可以通过
/sys/modules/core/section/.text
来查看,查看需要 root 权限,因此为了方便调试,我们再改一下init
1 |
|
1 | 0xffffffffc0257000 |
接下来进行调试:
- 先使用 start.sh 打开 kernel
- 然后打开 GDB
1 | ➜ core gdb ./vmlinux |
- 使用 add-symbol-file 加载符号,然后就可以利用符号进行断点了
1 | pwndbg> add-symbol-file core.ko 0xffffffffc0257000 |
- 尝试用 GDB 连接 kernel
1 | pwndbg> target remote localhost:1234 |
最后就来学习学习官方的exp:
1 | /* <-- 直接ROP --> */ |
先介绍几个概念:
- raw_vmlinux_base:kaslr 加工前的内核加载基址
- vmlinux_base:kaslr 加工后的内核加载基址
kaslr,类似ASLR,内核基址地址加载随机化
- 通过泄露内核地址,通过偏移计算出内核基址(如果没有开PIE,就可以直接获取 raw_vmlinux_base)
- 再计算 kaslr 对内核基址的偏移(offset = vmlinux_base - raw_vmlinux_base)
- 用 offset 修正其他函数的地址
官方exp选择从“内核符号表”泄露以下两个函数:
- prepare_kernel_cred:构造一个新的 cred
- commit_creds:更新当前进程的 cred
- commit_creds(prepare_kernel_cred(0)):构造一个 cred(0),并把它更新为当前进程的 cred
在主函数中,程序打开了 proc 目录中的某个文件(这个 proc 和 PROC 虚拟文件系统有关),然后调用 ioctl 来执行驱动函数,利用其本身的漏洞泄露 canary,触发 ROP链(就是想方设法构造出“commit_creds(prepare_kernel_cred(0))”,并返回用户空间)
最后看看这几个偏移是怎么计算出来的:
1 | from pwn import * |
结果:
1 | [ 0.022212] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie |
除了 prepare_kernel_cred,还有其他方式来提权,这里介绍一下 ret2usr:
- ret2usr 攻击利用了用户空间的进程不能访问内核空间,但内核空间能访问用户空间这个特性来定向内核代码或数据流指向用户控件,以
ring 0
特权执行用户空间代码完成提权等操作
以本题为例,exp 分析:
1 | /* <-- ret2usr --> */ |
前面的过程都相同,但 ROP 链的构造有所不同
- 直接ROP:
- 把 prepare_kernel_cred 和 commit_creds 拆散,放到 ROP 链中执行
- ret2usr:
- 直接返回到用户空间构造的
commit_creds(prepare_kernel_cred(0))
(通过函数指针实现)来提权 - 虽然这两个函数位于内核空间,但此时我们是
ring 0
特权,因此可以正常运行 - 之后也是通过
swapgs; iretq
返回到用户态来执行用户空间的system("/bin/sh")
- 直接返回到用户空间构造的
结果:
1 | [ 0.021763] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie |
小结:
这是我的第一个 kernel pwn,刚刚进入 kernel 感觉有点迷茫,不知道该获取什么信息,修改什么数据,完成此题后我的思路清晰了一点:
- 驱动函数是 kernel 中容易出问题的点,可以用C语言中的 ioctl 来执行驱动函数
- /proc/kallsyms 是内核符号表,拥有内核符号的地址,需要收集此信息
- kaslr,类似ASLR,内核基址地址加载随机化
- 使用
commit_creds(prepare_kernel_cred(0))
进行提权(可以放入ROP链中,也可以通过函数指针执行这个整体)
踩到的坑:
- start.sh 中给的内存太小,导致 kernel 跑不起来
- 题目自带的 vmlinux 和 core.cpio 中的 vmlinux 不一样,导致 gadget 出问题