内核pwn
今天就先不刷pwnabe.tw上的题了,开整内核方面的东西。
0x1 环境搭建
搭建1起来没有lot环境搭建困难,通过搭建环境也了解了很多东西,我第一次知道文件系统居然是独立的,以前以为一直和内核紧密相连,也越来越理解一切皆文件这句话了,不管是设备还是什么,都可以当做文件操作,真的很神奇。
1 | ::sysinit:/etc/init.d/rcS |
正确指令
1 | ::sysinit:/etc/init.d/rcS |
至于原因,我没看懂😃,剩下的照抄就行了,然后kernel就能通过qemu正确运行了。
1 | rootzhang@rootzhang-virtual-machine:~/pwn$ ./boot.sh |
0x2 内核初理解
内核是什么东西
内核并不是什么神奇的东西,他也是一个程序,一切的行为都有对应代码支撑,他的定位就是中间层,对上提供用户态程序的运行和对用户态提供抽象的操作硬件的api,对下直接和硬件相互交互的程序。
内核和用户态的切换
当发生系统调用,产生异常,外设产生中断等事件的时候,会发生用户态到内核太的切换,一般的函数调用思路就是保存当前栈帧的状态,然后跳到新地址,内核态和用户态的切换也是类似,其中用户态到内核态的切换具体过程如下(照搬的)
通过
swapgs
切换 GS 段寄存器(代码段寄存器),将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入
RSP/ESP
。通过 push 保存各寄存器值,具体的
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26ENTRY(entry_SYSCALL_64)
/* SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令 */
SWAPGS_UNSAFE_STACK
/* 保存栈值,并设置内核栈 */
movq %rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
/* 通过push保存寄存器值,形成一个pt_regs结构 */
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
pushq %rdi /* pt_regs->di */
pushq %rsi /* pt_regs->si */
pushq %rdx /* pt_regs->dx */
pushq %rcx tuichu /* pt_regs->cx */
pushq $-ENOSYS /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */
内核态到用户态的切换也是如此
内核的分级保护域
简称rings,感觉就是权限的意思,大部分现代操作系统只采用ring0权限和ring3权限,ring0权限就是root,kernel就是ring0,ring3权限较低,一般用户态程序就是ring3,以我目前的粗略理解,kernelpwn就是通过内核模块的漏洞对用户态程序进行提权到root然后getshell。
进程管理
kernel调度着所有系统资源,进程也是资源,也归kernel管,管理的策略就是使用结构体 task_struct
记录进程信息,每个task_struct
都记录着一个进程的信息,其中又有专门的结构体cred记录程序的权限,每个程序都有一个cred结构,如果能修改对应进程的cred,那也就能修改这个进程的权限了,结构体源码如下
1 | struct cred { |
进程权限改变
进程提权主要依靠两个协力完成。
struct cred* prepare_kernel_cred(struct task_struct* daemon)
:该函数用以拷贝一个进程的cred结构体,并返回一个新的cred结构体,需要注意的是daemon
参数应为有效的进程描述符地址或NULL,假如传入的是null,那函数就会返回一个root权限的cred.int commit_creds(struct cred *new)
:该函数用以将一个新的cred
结构体应用到进程
通过commit_creds(prepare_kernel_cred(NULL))这套组合拳就能把用户态进程的cred更新为一个root的cred.
(也有可能直接溢出改,我猜的,毕竟还没做过题😃)
内核保护机制
KASLR:内核地址随机化,和aslr类似,知道了一个地址再加上一个偏移就能知道基地址了,在没开启KASLR的时候,内核的基地址是0xffffffff81000000
STACK PROTECTOR:和canary类似,用以检测是否发生了内核堆栈溢出,如果发生内核堆栈溢出则会产生kernel panic.
SMAP/SMEP:smap的作用时阻止内核空间直接访问用户空间的数据,smep用以阻止内核空间执行用户空间的数据。目的是让内核空间和用户空间完全隔开。
一共有两种绕过(照搬)
- 在设计中,为了使隔离的数据进行交换时具有更高的性能,隐性地址共享始终存在(VDSO & VSYSCALL),用户态进程与内核共享同一块物理内存,因此通过隐性内存共享可以完整的绕过软件和硬件的隔离保护,这种攻击方式被称之为
ret2dir
(return-to-direct-mapped memory ) - Intel下系统根据CR4控制寄存器的第20位标识是否开启SMEP保护(1为开启,0为关闭),若是能够通过kernel ROP改变CR4寄存器的值便能够关闭SMEP保护,完成SMEP-bypass,接下来就能够重新进行ret2usr
KPTI:KPTI即内核页表隔离
(Kernel page-table isolation),内核空间与用户空间分别使用两组不同的页表集,这对于内核的内存管理产生了根本性的变化(完全没理解)
LKMs
可装载内核模块,简称LKMs,顾名思义就是内核中可装载可拆卸的程序,可以提供内核原本不具备的服务又不至于重新更新整个系统的东西,一般的kernelpwn题的漏洞都是出在出题人自己写的lkms上面。
lsmod
:列出现有的LKMsinsmod
:装载新的LKM(需要root)rmmod
:从内核中移除LKM(需要root)
内核态函数调用(照搬)
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()
对内核的搭建和认识就到这了,下面进入实战阶段