参考 学习 Linux 内核利用 - 第 1 部分 - Midas 的博客 学习一下kernel pwn 题 利用的流程
文件内容
pwn-kernel 题通常给出的是可以用qemu启动模拟器的镜像文件。
通常下述文件是重要的:
vmlinuz : 经过压缩的linux内核文件 , 有时命名为bzImage , 可以提取出vmlinux文件
initramfs.cpio.gz : 名字可能不同 但是一般里面包含”cpio” 。是文件系统的压缩文件
run.sh : qemu run command 。 也有叫做start.sh的。
之后内容以wmctf的easyker为例
处理文件
首先处理vmlinuz。 在easyker中叫做bzImage 。使用到了github上的一个脚本
❯ ../../extract-vmlinux/extract-vmlinux bzImage > vmlinux |
然后使用 ROPgadget 或者 ropper 提取其中的gadget
❯ ropper --file ./vmlinux --nocolor >gadgets.txt |
❯ cat gadgets.txt |head -10 |
由于每次提取会耗费较多时间 ,建议一次就把输出存储了
处理file system 即 cpio 文件,使用gunzip即可解压。 博客中给出了一个decompress.sh,我用ai稍微修改了一下,应对不同的文件名
!# /bin/sh |
一定要在题目目录里使用
对于gz文件上述脚本可能出问题 文件名太硬编码了
然后进入/etc/里面找到一个file 通常叫做rcS 或者 inittab 把 setuidgid 1000 /bin/sh 改成 setuidgid 0 /bin/sh
在easyker里用的是nsjial.conf 于是我把 inside_id 改成了 0
find . -print0 \ |
然后使用上述命令 重新生成cpio 或者 cpio.gz
我发现改了nsjail.conf还是没用 反复拷打了ai也没解决
run.sh 各参数意思:
-m 限制内存大小
-cpu 指定cpu型号
-kernel 指定压缩内核镜像文件
-initrd 指定压缩文件系统
-append 额外启动选项 此处可以添加保护
-monitor /dev/null 保证选手无法访问qemu monitor
-hdb flag.txt -drice file = ./flag …… : 将flag以设备的方式装在到内核中
首先要做的是在run.sh里加入 -s 参数,这允许我们对内核使用gdb
gdb vmlinux |
使用 –nx可以使用最普通的gdb进行调试
保护机制各种文章里都能看到 就不说了。
下一步分析内核模块
考虑到阅读文章的流畅性 我决定先总结此博客 然后再看wmctf的题
博客内的题目漏洞非常明显 就是 写入和读取时存在的大量的溢出。
Return-to-user : 从内核跳转到用户态任意代码的方式
但是如果开启了kpti 这个技术就不好用了
具体利用过程
内核模块相当于一个驱动,他会对设备的输入进行相应处理,在linux里设备会在/dev里映射到一个文件,我们可以通过与这个文件交互 模拟设备。 具体操作流程如下
int global_fd; |
上述代码打开了一个设备,然后我们可以读写这个设备
ret2usr 首先要有一个栈溢出 并且能覆盖到ret addr。 在覆盖时要注意看 return 的汇编代码。在内核中ret的过程相当的不统一,如果只看伪c代码,很可能在ret过程中一些重要的步骤就被漏掉了。(WMCTF就是在ret时给了个栈迁移)
一般的ret2usr首先是在返回地址里填入用户空间的一段汇编代码中
kernel exploition的目标时获得在系统中root 权限,然后返回一个shell 。 最常见的方式是通过
commit_creds(prepare_kernel_cred(0)) 0 参数表示权限配置和init一致
void escalate_privs(void){ |
回到userland
这里虽然我们已经获得了root权限,但是我们还需要回到用户态,可以用iret指令,它只需要我们在栈上准备好5个用户态寄存器的值:
RIP|CS|RFLAGS|SP|SS
CS: 段寄存器 用于指定当前代码的特权级 。
RFLAGS: 标志寄存器。
SP: 指示栈顶部 非常重要。
SS: 指定当前栈所在段的特权级
rip可以设置成自己想要的函数 ,但是其他寄存器需要提前保存
void save_state(){ |
pushf的功能是把RFLAGS push到栈上。
在x86_64 需要多一个指令叫swapgs . 它可以交换内核和用户态的GS寄存器。GS指示了某个段的基地址,我们不需要具体知道这是干嘛的。
结合前面说的 我们在返回前要做的就是
unsigned long user_rip = (unsigned long)get_shell; |
接下来看 WMCTF
首先要泄露kbase 。
这里利用了读取IDT table ,它在kernel中通常是固定映射,这里我发现我的gef根本没有kbase vmmap也什么都显示不了
IDT table一个表项是16字节 分布如下 : 按 offset,即标号是从左到右
0-1 | 2-3 | 4-4 | 5-5 | 6-7 | 8-11 | 12-15 |
---|---|---|---|---|---|---|
服务函数地址低2字节 | 段选择子 | 填充0x00 | 属性 | 地址第三、四字节 | 地址高4字节 | 0 |
具体为什么要这么做 我也不太清楚 非常玄学
但是泄露kbase并不需要逐字节对应: kbase通常后面0比较多 所以低二字节可以不管,我们从4下表开始读取 ,由于属性也是不会变的,不会导致计算失误,之后offset又是不变的。
因此一个常见的方法就是x/gx 0xfffffe0000000000 + 4 , 然后假装这玩意是地址 读取偏移