只会做几个烂大街的堆题目,,比赛堆题签个到走人
这好吗?这不好,,所以赶紧学学kernel pwn,记个笔记
环境搭建
根据钞sir师傅的博客搭建环境
基础知识
kernel的作用:
kernel也是一个程序,用来管理软件发出的数据 I/O 要求,将这些要求转义为指令,交给 CPU 和计算机中的其他组件处理
- 控制并与硬件进行交互
- 提供 application 能运行的环境
(kernel 的 crash 通常会引起重启)
intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0、Ring 1、Ring 2、Ring 3
但是其实一般来说只用Ring 0和Ring 3就可以区分(即内核态与用户态),Ring 0只能被操作系统使用,可以使用外层资源、可以修改用户权限,Ring 3则所有程序都可以使用
-
程序进入内核态之前要先保存用户态的寄存器
-
从内核态返回的时候
- 在栈上布置好寄存器的值并恢复
- 64位下才需要执行
swapgs
,用于置换GS
寄存器和KernelGSbase MSR
寄存器的内容 - 执行
sysretq
和iret
指令返回用户态(使用iretq
指令还需要给出CS、eflags/rflags、esp/rsp等一些用户空间的信息)
可以通过以下函数来获取并保存用户态寄存器信息
unsigned long user_cs, user_ss, user_eflags, user_sp; void save_stats(){ asm( "movq %%cs,%0\n" "movq %%ss,%1\n" "movq %%rsp,%3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags), "=r"(user_sp) : :"memory" ); }
之后恢复的时候可以直接用这些值恢复寄存器
在比赛中,通常漏洞会存在于动态装载模块中(比如驱动程序、内核扩展模块)
对模块的基本操作
命令
insmod:加载模块
lsmod:查看模块
rmmod:删除模块
函数
open:打开模块
ioctl:操作模块
read:读模块
write:写模块
close:关闭模块
内核态函数
-
printf() -> printk(),但需要注意的是 printk() 不一定会把内容显示到终端上,但一定在内核缓冲区里,可以通过
dmesg
查看效果 -
memcpy() ->copy_from_user()/copy_to_user()
copy_from_user(char *a1, char *a2, int a3);
实现了将用户空间a2的长度为a3的数据传送到内核空间a1copy_to_user(char *a1, char *a2, int a3) ;
实现了将内核空间a2的长度为a3的数据传送到用户空间a1
-
malloc() -> kmalloc(),内核态的内存分配函数,和 malloc() 相似,但使用的是
slab/slub 分配器
-
free() -> kfree(),同 kmalloc()
-
misc_register()用于注册一个驱动,其参数为
miscdevice
结构体指针miscdevice结构体定义为:
内核在加载驱动的时候,会调用驱动程序中的
module_init()
函数,module_init()
函数再调用misc_register()
来向内核注册驱动
设备类型
linux系统将设备分为三类:字符设备、块设备、网络设备
字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。 块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
设备的打开过程
由于ucore文件系统实验摸鱼了,先学学文件系统相关知识
注册设备驱动
如上文所说的,在使用insmod
加载驱动的时候,内核调用module_init()
函数,module_init()
函数再调用相应的注册函数来向内核注册驱动(比如misc_register()
函数)
由于miscdevice
结构体是misc_register()
函数的参数,所以在调用misc_register()
函数时通过miscdevice
结构体的成员fops
指针将file_operations
结构体连同其主设备号一起传入内核
file_operations结构体定义为:
其成员除了owner
指向的module
结构体之外,剩下的都是函数指针,可以通过修改其中的函数指针来达到重写某个函数的目的,如果对这个驱动调用某个其中的函数,就会调用结构体中的函数指针
举个栗子
在某个内核模块代码中
struct file_operations shf_fops = {
.owner = THIS_MODULE,
.open = shf_open,
.release = shf_release,
.unlocked_ioctl = shf_unlocked_ioctrl,
}
struct miscdevice shf_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "shf",
.fops = &shf_fops,
};
misc_regiseter(&shf_device);
从对file_operations
结构体的修改可以看出这里重写了open
函数release
函数和unlocked_ioctl
函数
misc_regiseter
函数会在/dev下创建shf节点,即/dev/shf
在用户程序中只要fd = open("/dev/shf",READONY);
就可以调用重写的open
函数来启动该驱动,然后通过ioctl
函数操作该驱动
打开设备
在Linux下一切皆文件,设备也不例外
内核会为每一个运行中的进程在进程控制块pcb中维护一个打开文件的记录表,也就是文件描述符表,文件描述符fd就是这个表的索引,该表每一个表项都是 已打开文件的file结构体指针
file结构体是内核中用来描述文件属性的结构体
进程通过系统调用open
系统调用来打开一个文件,会获得一个文件描述符,并为该文件创建一个file对象,并把该file对象存入进程打开文件表中(文件描述符数组),以便进程通过文件描述符为连接对文件进行其他操作
close
系统调用则反之
FILE与file傻傻分不清
file结构体是linux内核中的结构体,每一个被打开的广义的文件(包括设备、套接字等),都有一个file结构体与之对应
FILE结构体是libc中的结构体
#ifndef _FILE_DEFINED struct _iobuf { char *_ptr; //文件输入的下一个位置 int _cnt; //当前缓冲区的相对位置 char *_base; //指基础位置(即是文件的其始位置) int _flag; //文件标志 int _file; //文件描述符 int _charbuf; //检查缓冲区状况,如果无缓冲区则不读取 int _bufsiz; //缓冲区大小 char *_tmpfname; //临时文件名 }; typedef struct _iobuf FILE; #define _FILE_DEFINED
使用fopen,fclose,fread,fwrite返回FILE *文件指针,对狭义的文件(不包括设备、套接字等)进行操作
ioctl系统调用操作设备
在用户空间实用ioctl操作设备的时候,其接口为
int ioctl(int fd,unsigned long cmd,...);
/*
fd:文件描述符
cmd:控制命令
...:可选参数:插入*argp,具体内容依赖于cmd
*/
而在进行系统调用时,根据linux内核中关于ioctl系统调用的源代码
再经过一些检查之后,最终调用的vfs_ioctl(f.file, cmd, arg)
,其第一个参数变为由fd找到的对应的file结构体
然后再从file_operations
结构体中找到对应hook函数unlocked_ioctl
的函数指针进行调用
可以看到最终调用了ko文件中的内核函数unlocked_ioctl(filp, cmd, arg);
对于设备open
、read
、write
等系统调用的大致流程也都是如此,用户接口进入SYSCALL_DEFINE3
宏后调用vfs_XXXX
来调用file_operations
结构体中的函数
题目文件
baby.ko
就是有bug的程序(出题人编译的驱动),可以用IDA
打开
bzImage
是打包的内核,用于启动虚拟机与寻找gadget
Initramfs.cpio
文件系统
startvm.sh
启动脚本有时还会有
vmlinux
文件,这是未打包的内核,一般含有符号信息,可以用于加载到gdb
中方便调试(gdb vmlinux
),当寻找gadget
时,使用objdump -d vmlinux > gadget
然后直接用编辑器搜索会比ROPgadget
或ropper
快很多。没有
vmlinux
的情况下,可以使用linux
源码目录下的scripts/extract-vmlinux
来解压bzImage
得到vmlinux
(extract-vmlinux bzImage > vmlinux
),当然此时的vmlinux
是不包含调试信息的。还有可能附件包中没有驱动程序
*.ko
,此时可能需要我们自己到文件系统中把它提取出来,这里给出ext4
,cpio
两种文件系统的提取方法:
ext4
:将文件系统挂载到已有目录。
cpio
:解压文件系统、重打包
mkdir extracted; cd extracted
cpio -i --no-absolute-filenames -F ../rootfs.cpio
- 此时与其它文件系统相同,找到
rcS
文件,查看加载的驱动,拿出来find . | cpio -o --format=newc > ../rootfs.cpio
漏洞类型
主要有以下几种保护机制:
KPTI
:Kernel PageTable Isolation,内核页表隔离KASLR
:Kernel Address space layout randomization,内核地址空间布局随机化SMEP
:Supervisor Mode Execution Prevention,管理模式执行保护SMAP
:Supervisor Mode Access Prevention,管理模式访问保护Stack Protector
:Stack Protector又名canary,stack cookiekptr_restrict
:允许查看内核函数地址dmesg_restrict
:允许查看printk
函数输出,用dmesg
命令来查看MMAP_MIN_ADDR
:不允许申请NULL
地址mmap(0,....)
-
可以通过
cat /proc/cpuinfo
来查看开启了哪些保护 -
KASLR
和Stack Protector
类似于用户态下的ASLR
和Canary
-
开启
SMEP
,内核态运行时,不允许执行用户态代码,开启SMAP
,内核态不允许访问用户态数据;可通过修改cr4
寄存器的值来绕过SMEP
,SMAP
保护 -
调试时,
KASLR
、SMEP
、SMAP
可通过修改startvm.sh
来关闭;kptr_restrict
、dmesg_restrict
可在rcS
文件中修改;MMAP_MIN_ADDR
是linux
源码中定义的宏,可重新编译内核进行修改(.config
文件中),默认为4k
攻击目标
-
一般需要通过ROP等手段调用
commit_creds(prepare_kernel_cred(0));
来进行提权进程都有一个cred结构体
struct cred { atomic_t usage; uid_t uid; gid_t gid; struct rcu_head exterminate; struct group_info *group_info; }
用于标记权限,通过调用
commit_creds(prepare_kernel_cred(0));
函数语句可以重新分配一个uid和gid都为0的cred结构体,此时再打开新进程(比如/bin/sh)就是root权限了 -
在可以UAF的时候,将特定对象大小的地址放入freelist后创建新进程,新进程为cred结构体分配内存的时候申请到freelist中的这一个对象,再在父进程中修改子进程的uid、gid(似乎新版本内核已不可用)
原因在于,新版本内核中采用了
cred_jar
这个新的kmem_cache
,与kmalloc
使用的kmalloc-xx
是隔离开的。 -
在可以UAF的时候,利用seq_operations结构体,达到泄露地址或执行代码的目的
struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
对proc文件系统进行读取的时候,限制了一次最多读一页,如果超过那么只能多次读取,这样就会增加读取次数从而增加系统调用的次数,影响了效率。所以出现了
seq_file
的序列文件出现,该功能使得对于读取大文件更加容易。 至于其中更深层次的细节,我这里就不赘述了,总而言之,试图读取proc文件系统中的文件时,会创建一个seq_file
结构体,作为这个结构体成员的seq_operations
也相应产生。 在打开一个序列文件的时候会调用seq_open
,之后读取文件内容时,seq_operations
的执行顺序为:start() ==> next() ==> show() ==> ... ==> next() ==> show() ==> stop();
打开一个proc文件之后,总是第二个0x20的object被分配给
seq_operations
使用,但是我并没有深究第一个是被谁申请的,也并不清楚这在所有内核版本中是否是通用的情况。利用方法:将0x20大小的对象放到freelist后,打开一个proc文件,比如
/proc/self/stat
,然后通过分配到seq_operations结构体的对象泄露内核地址以及设置好寄存器后修改seq_opeartions->start
为类似xchg eax, esp; ret;
的栈迁移gadget,再进行后续的rop提权可以进行栈迁移的原因是在即将调用
seq_operations->start()
的时候rax寄存器中保存的就是这个函数指针,而xchg eax, esp; ret;
会将rsp和rax交换,并只保留低位数据,所以如果事先使用mmap在用户空间分配一个32位的地址空间,其低位与该gadget地址一致,则交换以后rsp指向mmap分配的内存,实现栈迁移rax : 0xffffffffaf1c4878 ◂— xchg eax, esp rsp : 0xffffbb04801ffdb8 (ni) rax : 0x801ffdb8 rsp : 0xaf1c4878(事先用mmap分配0xaf1c4000的内存空间,则可以将栈迁移到可控内存)
-
如果可以实现内核中的任意地址写,将
poweroff_force
字符串修改为需要执行的命令,将hp->hook.task_prctl
hook指针修改为poweroff_work_fun()
函数的地址,在用户态执行prctl(0)
的时候内核会调用hp->hook.task_prctl
指向的函数poweroff_work_fun()
,在poweroff_work_fun()
中会以root权限执行poweroff_force
中的命令 -
如果可以实现内核中的任意地址写,且没有栈地址或者存在fg_kaslr(函数级别的随机化,无法利用ROP),可以对modprobe_path处的路径进行修改,再将要执行的命令写入到修改后modprobe_path指向的文件中
modprobe_path指向了一个内核在运行未知文件类型时运行的二进制文件;当内核运行一个错误格式的文件的时候,会调用这个modprobe_path所指向的二进制文件去,如果我们将这个字符串指向我们的自己的二进制文件,那么在发生错误的时候就可以执行我们自己二进制文件了….
gdb调试的栗子
-
Shell1:
-
解压文件系统
mkdir extracted; cd extracted cpio -i --no-absolute-filenames -F ../rootfs.cpio
找到文件系统中的
rcS
文件/init
文件,从setsid
这一行修改权限为0,然后将文件系统打包find . | cpio -o --format=newc > ../rootfs.cpio
-
start.sh加上
-gdb tcp::1234
或者-s
,并关闭kaslr
qemu-system-x86_64 \ -m 256M -smp 2,cores=2,threads=1 \ -kernel ./vmlinuz-4.15.0-22-generic \ -initrd ./rootfs.img \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \ -cpu qemu64 -netdev user,id=t0, \ -device e1000,netdev=t0,id=nic0 \ -nographic \ -gdb tcp::1234 ##加上 -gdb
启动内核后查看驱动的基地址
/ # lsmod baby 16384 0 - Live 0xffffffffc031d000 (POE)
查找两个提权用的内核函数地址
cat /proc/kallsyms | grep "prepare_kernel_cred" #得到prepare_kernel_cred函数地址 cat /proc/kallsyms | grep "commit_creds" #得到commit_creds函数地址
-
-
Shell2:
-
在当前目录下配置.gdbinit文件,设置
vim .gdbinit
在里面写上
set architecture i386:x86-64
打开gdb
gdb ./baby.ko add-symbol-file ./baby.ko 0xffffffffc031d000 #附加驱动,让gdb对命令的反应速度快点 target remote :1234
-
之后就可以进行调试了
写好exp后编译为静态二进制文件运行进行提权
gcc exp.c -o exp -static
gcc exp.c -o exp -masm=intel -static #intel格式内联汇编
关于驱动在内核态的调试方法应该是安装驱动,对相应函数下断,运行poc,然后才可以断下来调试,和我们在用户态直接调试程序其实就是多了一个运行poc,其他方法都差不多的…
gadget查找
在寻找gadget的时候一般从vmlinux导出的gadget很少正好有漏洞利用需要的gadget,但是对于x86变长指令集来说,同样的字节数据从不同的偏移处执行就会有不同的效果
ffffffff810e3b21: 0f 94 c3 sete %bl
ffffffff810e3b22: 94 c3 xchg eax, esp; ret
所以在文本中搜索gadget的时候可以直接搜索对应的机器码,而不一定是汇编语句
常用指令
c3 : ret;
0f 22 e7 : mov cr4,rdi;
94 : xchg eax, esp;
5f : pop rdi;
5a : pop rdx;
48 89 c7 : mov rdi,rax
0f 01 f8 : swapgs;
48 cf : iretq
参考资料:
Linux kernel Exploit 内核漏洞学习(0)-环境安装
Linux Kernel Exploit 内核漏洞学习(4)-RW Any Memory - 先知社区 (aliyun.com)
kernel pwn: kernoob – 不仅仅是double fetch——Nop’s Blog (n0nop.com)