kernel pwn(一)基础知识
从wiki和其它文章上学习一下kernel pwn的一些基础知识。
kernel基础
kernel是操作系统的核心,其实也可以看成一个程序。管理硬件设备,为应用程序提供运行环境,其实就是用来管理软件发出的数据 I/O 要求,将这些要求转义为指令,交给 CPU 和计算机中的其他组件处理
Linux内核是单内核结构。单内核效率高,但是体积大,可扩展性和可维护性相对较差。还有其他的一些内核,比如微内核效率低,但是体积小。
Ring Model
CPU 的特权级别分为 4 个级别,分别是Ring 0,Ring 1,Ring 2,Ring 3。
内层 Ring 可以随便使用外层 Ring 的资源。kernel运行于Ring 0等级,有自己的栈,和用户不共用。Ring 3所有程序都可以使用,使用Ring Model可以提升安全性,一个恶意程序运行在Ring 3级上,恶意程序需要打开摄像头时需要Ring 1级的权限,所以在不通知用户的情况下会被阻止。
Loadable Kernel Modules
上面说过单内核效率高,的同时也有缺点,为了弥补可扩展性和维护性相对较差的形势,模块机制就是为了弥补这一缺陷。模块通常用来实现一种文件系统、一个驱动程序或者其他内核上层的功能,内核模块就像运行在内核空间的可执行程序。
kernel pwn的一些漏洞好多都出现在Loadable Kernel Modules中
指令
- insmod: 讲指定模块加载到内核中
- rmmod: 从内核中卸载指定模块
- lsmod: 列出已经加载的模块
- modprobe: 添加或删除模块,modprobe 在加载模块时会查找依赖关系
syscall
系统调用,指的是用户空间的程序向操作系统内核请求需要更高权限的服务,比如 IO 操作或者进程间通信。系统调用提供用户程序与操作系统间的接口,部分库函数(如 scanf,puts 等 IO 相关的函数实际上是对系统调用的封装(read 和 write))
在 /usr/include/x86_64-linux-gnu/asm/unistd_64.h文件下可以查看64位程序的系统调用。32位的话就是将64改为32即可。
kernel 态
进入kernel态之前会保护用户态的各个寄存器,以及执行到代码的位置。相比用户态库函数,内核态的函数有了一些变化
- printf() -> printk(),但需要注意的是 printk() 不一定会把内容显示到终端上,但一定在内核缓冲区里,可以通过 dmesg 查看效果
- memcpy() -> copy_from_user()/copy_to_user()
- copy_from_user() 实现了将用户空间的数据传送到内核空间
- copy_to_user() 实现了将内核空间的数据传送到用户空间
- malloc() -> kmalloc(),内核态的内存分配函数,和 malloc() 相似,但使用的是 slab/slub 分配器
- free() -> kfree(),同 kmalloc()
1 | printk("<1>Hello World!\n"); |
<1>是输出的级别,表示立即在终端输出。
ioctl
ioctl 也是一个系统调用,用于与设备通信。int ioctl(int fd, unsigned long request, ...)
的第一个参数为打开设备 (open) 返回的文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。
使用 ioctl 进行通信的原因:
操作系统提供了内核访问标准外部设备的系统调用,因为大多数硬件设备只能够在内核空间内直接寻址, 但是当访问非标准硬件设备这些系统调用显得不合适, 有时候用户模式可能需要直接访问设备。
比如,一个系统管理员可能要修改网卡的配置。现代操作系统提供了各种各样设备的支持,有一些设备可能没有被内核设计者考虑到,如此一来提供一个这样的系统调用来使用设备就变得不可能了。
为了解决这个问题,内核被设计成可扩展的,可以加入一个称为设备驱动的模块,驱动的代码允许在内核空间运行而且可以对设备直接寻址。一个 Ioctl 接口是一个独立的系统调用,通过它用户空间可以跟设备驱动沟通。对设备驱动的请求是一个以设备和请求号码为参数的 Ioctl 调用,如此内核就允许用户空间访问设备驱动进而访问设备而不需要了解具体的设备细节,同时也不需要一大堆针对不同设备的系统调用。
状态切换
user space to kernel space
当发生系统调用,产生异常,外设产生中断等事件时,会发生用户态到内核态的切换
通过
swapgs
切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
通过 push 保存各寄存器值,具体的代码如下:
通过汇编指令判断是否为
x32_abi
通过系统调用号,跳到全局变量
sys_call_table
相应位置继续执行系统调用。
kernel space to user space
退出时,流程如下:
通过
swapgs
恢复 GS 值通过
sysretq
或者iretq
恢复到用户控件继续执行。如果使用iretq
还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)
kernel pwn基础
kernel pwn的保护机制
KASLR
:内核地址随机化,相当于ASLR(并非默认启用,需要在内核命令行中加入kaslr开启)
SMEP/SMAP
:[SMEP—->管理模式执行保护,禁止内核访问用户空间的数据],[SMAP—->管理模式访问保护,类似于NX,即内核态无法执行shellcode]
Stack Protector
:(canary)在编译内核时设置CONFIG_CC_STACKPROTECTOR选项,即可开启该保护,一般而言开了这个保护再编译驱动会发现有canary。
KPTI
:KPTI即内核页表隔离
(Kernel page-table isolation),内核空间与用户空间分别使用两组不同的页表集,这对于内核的内存管理产生了根本性的变化
攻击流程
1.在内核代码中找到漏洞。
2.利用Shellcode, ROP, 等攻击方式实现代码执行。
3.提权。
4.本地写好 exploit 后,可以通过 base64 编码等方式把编译好的二进制文件保存到远程目录下,进而拿到 flag。
struct cred
之前提到 kernel 记录了进程的权限,更具体的,是用 cred 结构体记录的,每个进程中都有一个 cred 结构,这个结构保存了该进程的权限等信息(uid,gid 等),如果能修改某个进程的 cred,那么也就修改了这个进程的权限。
例如:执行 commit_creds(prepare_kernel_cred(0)) 即可获得 root 权限
看一下源码
kernel pwn
一般会给以下三个文件
- boot.sh: 一个用于启动 kernel 的 shell 的脚本,多用 qemu,保护措施与 qemu 不同的启动参数有关
- bzImage: kernel binary
- rootfs.cpio: 文件系统映像
1 |
|
解释一下 qemu 启动的参数:
- -initrd rootfs.cpio,使用 rootfs.cpio 作为内核启动的文件系统
- -kernel bzImage,使用 bzImage 作为 kernel 映像
- -cpu kvm64,+smep,设置 CPU 的安全选项,这里开启了 smep
- -m 64M,设置虚拟 RAM 为 64M,默认为 128M 其他的选项可以通过 –help 查看。
本地写好 exploit 后,可以通过 base64 编码等方式把编译好的二进制文件保存到远程目录下,进而拿到 flag。同时可以使用 musl, uclibc 等方法减小 exploit 的体积方便传输。
测试exp:
1 | $ cp ./exp ./fs && cd fs |
Reference
http://taqini.space/2020/11/21/linux-kernel-pwn-learning/#%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4
https://ctf-wiki.org/pwn/linux/kernel-mode/basic-knowledge/#ctf-kernel-pwn
https://arttnba3.cn/2021/02/21/NOTE-0X02-LINUX-KERNEL-PWN-PART-I/#0xFF-reference