Linux : lkmpg 粗淺筆記 (2) — system call

吳建興
12 min readSep 16, 2021

前言

這是閱讀 Jserv 老師與其學生共同維護的 linux kernel module programming guide 的簡易筆記。

System Calls

這個主題因為 linux kernel 有新的保護機制,變的想要執行這個範例前,需要做一些準備工作。

簡單來說,當我們使用系統呼叫 ( system call) 時,會準備一些參數,並使用特定的 instruction ( e.g. 在 x86 架構就是 int 0x80, x64 則是 syscall。除此之外還有 sysenter …等等) 從 user space 跳進 kernel space。在 kernel space 時,會依照給定的參數以及 “sys_call_table” 來選擇要使用哪一個 system call。例如給定的參數為 10,我們就會使用 sys_call_table 裡記錄的第 10 個 function。

簡易 system call 流程 ( x86_64 )

  1. 使用組合語言 syscall 進入 kernel,glibc 有對此組合語言進行簡易的包裝,並會將參數以 rax 傳遞。

2. entry_SYSCALL_64 ( /arch/x86/entry/entry_64.S )

3. do_syscall_64 ( /arch/x86/entry/common.c )

這裡的 nr 就是步驟 1 的 rax 值。
我們會從 sys_call_table 裡利用 nr 找出相對應的 system call 來執行。

__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
.
.
.
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
} else if (likely((nr & __X32_SYSCALL_BIT) &&
(nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
X32_NR_syscalls);
regs->ax = x32_sys_call_table[nr](regs);
#endif
}
.
.
.
.

這個範例裡,我們希望能夠修改 “sys_call_table” 的內容,來劫持原本紀錄在 “sys_call_table” 裡的 function。但因為資安的考量 ( 總不能讓高強的駭客輕輕鬆鬆就修改掉 sys_call_table ),想要修改 sys_call_table 有一些困難需要克服。

要找出 “sys_call_table” 的位址

想要修改 sys_call_table 的內容,就需要先知道 sys_call_table 到底在哪裡。因為 sys_call_table 是 unexported symbol,所以沒辦法在 kernel module 裡直接使用,但還是有幾種方法取得 sys_call_table 的位址。

  1. 查詢後,再經由參數傳入 kernel module 裡

在 /proc/kallsyms 裡,我們就能查詢到 sys_call_table 的位址了,所以我們可以不用藉由符號,而是直接藉由位址來修改 sys_call_table 的內容。

$ sudo cat /proc/kallsyms | grep sys_call_table
ffffffffaf1aa0a0 t proc_sys_call_handler
ffffffffafe002a0 R sys_call_table
ffffffffafe01320 R ia32_sys_call_table
ffffffffafe020e0 R x32_sys_call_table

拿到了位址後,再利用參數傳入 kernel module 裡面。

$ sudo insmod syscall.ko sym=0xffffffff820013a0

另外也可使用 /boot/System.map-$(uname -r) 來查詢,但若有開啟 KASLR 的功能的話,真正的位址會有所偏移,這時候就要照著 lkmpg 裡的步驟將 KASLR 給關掉了。

$ sudo cat /proc/kallsyms | grep sys_call_table
ffffffffafe002a0 R sys_call_table
ffffffffafe01320 R ia32_sys_call_table
ffffffffafe020e0 R x32_sys_call_table
$ cat /boot/System.map-$(uname -r) | grep sys_call_table
ffffffff820002a0 R sys_call_table
ffffffff82001320 R ia32_sys_call_table
ffffffff820020e0 R x32_sys_call_table
# 可以看到因為 KASLR 的緣故,兩邊會有偏移

2. 使用 kallsyms_lookup_name 來查找某個 symbol 的位址。

只要使用 kallsyms_lookup_name 就能利用下面的方式來查到 sys_call_table 的位址了!

int init_module(void)
{
printk(KERN_INFO "Hello world 1.\n");
unsigned long ** ret = kallsyms_lookup_name("sys_call_table"); return 0;
}

但是,這樣怎麼會沒辦法編譯成功呢?

ERROR: modpost: "kallsyms_lookup_name" [...] undefined!

原來是因為 "kallsyms_lookup_name" 在 linux kernel v5.7 以後, 這個 symbol 也變成 unexported 了。但沒關係,只要在編譯 linux kernel 時,有將 CONFIG_KPROBES 打開,就能使用 Kprobes 的功能找出“kallsyms_lookup_name” 的位址了!

# file : /usr/src/kernels/linux-5.8/.config
# 位置因人而異,看個人在編譯 linux kernel 時,將 source code 放在哪裡
CONFIG_HAVE_OPROFILE=y
CONFIG_OPROFILE_NMI_TIMER=y
################
CONFIG_KPROBES=y
################
CONFIG_JUMP_LABEL=y

至此,我們拿到了 “sys_call_table” 的位址!

要能夠修改 “sys_call_table” 的值

從下面的資訊可以知道, sys_call_table 是唯讀的,通常是沒辦法進行寫入的。

$ sudo cat /proc/kallsyms | grep sys_call_table
ffffffffaf1aa0a0 t proc_sys_call_handler
ffffffffafe002a0 R sys_call_table
ffffffffafe01320 R ia32_sys_call_table
ffffffffafe020e0 R x32_sys_call_table

這時候我們就需要將 cr0 的 WP( Write protect ) flag 給關起來,這時候我們就能對原本唯讀的資料進行寫入了。

這時候可以用 write_cr0 來對 cr0 進行設定。但自從 linux kernel v5.3 之後,也沒辦法使用這個 function 了,這時候就需要寫些組語來 bypass 這個問題。

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 3, 0) 
static inline void __write_cr0(unsigned long cr0)
{
asm volatile("mov %0,%%cr0" : "+r"(cr0) : : "memory");
}
#else
#define __write_cr0 write_cr0
#endif

到此為止,大略把需要做的事情講述了一遍,再來可以簡單寫個 user space 的程式來測試看看這個 module 了。

先用 insmod 把 module 安裝好,然後再寫一個簡單的 open 程式。

$ sudo insmod syscall.ko uid=1000
---
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int fd = 0;
fd = open("hh.txt", O_RDONLY);
close(fd);
return 0;
}

但實際運行起來後,用 dmesg 卻沒有看到相對應的 log ( “Opened file by ..” )
這時候用 strace 來觀察一下,發現原來 glibc 的 open,會使用 “openat” 這個系統呼叫,而不是 “open”。

$ strace ./a.out
mprotect(0x7f3bccf41000, 4096, PROT_READ) = 0
munmap(0x7f3bccf27000, 105219) = 0
openat(AT_FDCWD, "hh.txt", O_RDONLY) = 3
close(3) = 0
exit_group(0) = ?

這時候我們可以依賴 glibc 包裝好的 syscall 來直接呼叫 open。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/syscall.h>
/*
/usr/src/kernels/linux-5.8/arch/x86/include/generated/asm/syscalls_64.h
__SYSCALL_COMMON(0, sys_read)
__SYSCALL_COMMON(1, sys_write)
__SYSCALL_COMMON(2, sys_open)
__SYSCALL_COMMON(3, sys_close)
*/
int main(int argc, char *argv[]) { int fd = 0;
fd = syscall(2, "hh.txt", O_RDONLY);
close(fd);
return 0;
}
-----------------------------------------
$ strace ./a.out
mprotect(0x7f9c26927000, 16384, PROT_READ) = 0
mprotect(0x55ba3811c000, 4096, PROT_READ) = 0
mprotect(0x7f9c26b5a000, 4096, PROT_READ) = 0
munmap(0x7f9c26b40000, 105219) = 0
open("hh.txt", O_RDONLY) = 3
close(3) = 0
exit_group(0) = ?
+++ exited with 0 +++
-----------------------------------------
$ dmesg | grep Open
[ 3709.392500] Opened file by 1000:

這時候我們從 strace 的結過就可以看到,終於是使用 "open" 這個系統呼叫了!而 dmesg 也終於出現我們想看到的 log。

Reference

https://www.stolaf.edu/people/rab/os/lab/newsyscall.html

Control-flow integrity (CFI) in the linux kernel

--

--