Inline Hook Syscall 详解

Ftrace训练营火热报名中:Ftrace训练营:站在设计者的角度来理解ftrace(限50人)。训练营第一期报名已圆满成功,好评如潮。第二期报名正在火爆进行中(咨询小月微信:linuxer2016)。


ARM安全架构训练营火热报名中:阅码场训练营:ARM安全架构之Trustzone-TEE实战报名咨询客服(小月微信:linuxer2016)。

作者简介

伟林,中年码农,从事过电信、手机、安全、芯片等行业,目前依旧从事Linux方向开发工作,个人爱好Linux相关知识分享,个人微博CSDN pwl999,欢迎大家关注!


文章目录

1. hook一般syscall

2. hook stub syscall

2.1 stub_xxx 原理

2.2 方法1:hook `stub_xxx`

2.3 方法2:hook `call sys_xxx`

参考文档:


1. hook一般syscall


在安全、性能分析等领域,经常会需要对系统调用syscall进行hook。有些模块在kernel代码中已经预先hook,例如syscall trace event。


通常syscall使用sys_call_table[]数组来间接调用:


kernel\arch\x86\kernel\entry_64.S:
ENTRY(system_call)
call *sys_call_table(,%rax,8) # XXX: rip relative


sys_call_table[]数组中保存的是所有系统调用的函数指针:


#define __SYSCALL(nr, sym) [nr] = sym,
const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { #define __NR_read 0 __SYSCALL(__NR_read, sys_read) #define __NR_write 1 __SYSCALL(__NR_write, sys_write) #define __NR_open 2 __SYSCALL(__NR_open, sys_open) #define __NR_close 3 __SYSCALL(__NR_close, sys_close)
...};


对于其他没有预置代码的模块来说,需要在运行的时候动态hook,通常我们使用inline hook。inline hook的好处是hook完以后,运行时零开销。


实例代码:


void syscallxxx_hook_init(void){  unsigned long *sct;    void ** g_syscall_table;
g_syscall_table = (void **)kallsyms_lookup_name("sys_call_table"); make_kernel_page_readwrite(); preempt_disable(); /* (1) 备份原有g_syscall_table[]数组中的函数指针 */ orig_syscallxxx = (void *)g_syscall_table[__NR_syscallxxx]; /* (2) 把g_syscall_table[]数组值改为新的函数指针 */ sct[__NR_syscallxxx] = (unsigned long)new_syscallxxx; preempt_enable(); make_kernel_page_readonly();}

asmlinkage long new_syscallxxx(...){ long rc; /* (2.1) 做一些hook增加的事情 */ rc = do_something(...); if (0 != rc) return rc; /* (2.2) 调用原有的syscall处理 */ return orig_syscallxxx(....); }


这种hook方式在大部分情况下工作正常,但是某些特殊的系统调用会工作异常。


2. hook stub syscall


2.1 stub_xxx 原理


在4.5版本及以下的内核中,x86架构对某些系统调用有特殊处理。我们可以在sys_call_table[]数组中看到的函数不是sys_xxx而是stub_xxx:


const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
#define __NR_rt_sigreturn 15 __SYSCALL(__NR_rt_sigreturn, stub_rt_sigreturn)
#define __NR_clone 56 __SYSCALL(__NR_clone, stub_clone) #define __NR_fork 57 __SYSCALL(__NR_fork, stub_fork) #define __NR_vfork 58 __SYSCALL(__NR_vfork, stub_vfork) #define __NR_execve 59 __SYSCALL(__NR_execve, stub_execve)
#define __NR_sigaltstack 131 __SYSCALL(__NR_sigaltstack, stub_sigaltstack)
#define __NR_iopl 172 __SYSCALL(__NR_iopl, stub_iopl) ...};


这有点出乎我们的意料,字面上理解是一些桩函数,我们看看其具体做了些什么:


kernel\arch\x86\kernel\entry_64.S:
/* * Certain special system calls that need to save a complete full stack frame. */ .macro PTREGSCALL label,func,argENTRY(\label) PARTIAL_FRAME 1 8 /* offset 8: return address */ subq $REST_SKIP, %rsp CFI_ADJUST_CFA_OFFSET REST_SKIP call save_rest DEFAULT_FRAME -2 8 /* offset 8: return address */ leaq 8(%rsp), \arg /* pt_regs pointer */ call \func /* (1.1) 调用实际的系统调用sys_xxx()函数 */ jmp ptregscall_common CFI_ENDPROCEND(\label) .endm
/* (1) stub_clone/fork/vfork/sigaltstack/iopl 函数的定义 */ PTREGSCALL stub_clone, sys_clone, %r8 PTREGSCALL stub_fork, sys_fork, %rdi PTREGSCALL stub_vfork, sys_vfork, %rdi PTREGSCALL stub_sigaltstack, sys_sigaltstack, %rdx PTREGSCALL stub_iopl, sys_iopl, %rsi
ENTRY(ptregscall_common) DEFAULT_FRAME 1 8 /* offset 8: return address */ RESTORE_TOP_OF_STACK %r11, 8 movq_cfi_restore R15+8, r15 movq_cfi_restore R14+8, r14 movq_cfi_restore R13+8, r13 movq_cfi_restore R12+8, r12 movq_cfi_restore RBP+8, rbp movq_cfi_restore RBX+8, rbx ret $REST_SKIP /* pop extended registers */ CFI_ENDPROCEND(ptregscall_common)
/* (2) stub_execve函数的定义 */ENTRY(stub_execve) CFI_STARTPROC addq $8, %rsp PARTIAL_FRAME 0 SAVE_REST FIXUP_TOP_OF_STACK %r11 movq %rsp, %rcx call sys_execve /* (2.1) 调用实际的系统调用sys_execve()函数 */ RESTORE_TOP_OF_STACK %r11 movq %rax,RAX(%rsp) RESTORE_REST jmp int_ret_from_sys_call CFI_ENDPROCEND(stub_execve)
/* * sigreturn is special because it needs to restore all registers on return. * This cannot be done with SYSRET, so use the IRET return path instead. */ /* (3) stub_rt_sigreturn函数的定义 */ENTRY(stub_rt_sigreturn) CFI_STARTPROC addq $8, %rsp PARTIAL_FRAME 0 SAVE_REST movq %rsp,%rdi FIXUP_TOP_OF_STACK %r11 call sys_rt_sigreturn /* (3.1) 调用实际的系统调用sys_rt_sigreturn()函数 */ movq %rax,RAX(%rsp) # fixme, this could be done at the higher layer RESTORE_REST jmp int_ret_from_sys_call CFI_ENDPROCEND(stub_rt_sigreturn)


为什么系统要对这几个系统调用做stub_xxx的特殊处理?

注释中的一段话说明了大概原因:


/* * Certain special system calls that need to save a complete full stack frame. * 某些特殊的系统调用需要保存完整的完整堆栈帧。 */


针对这类特殊的系统调用,我们有两种方法来进行hook。


2.2 方法1:hook stub_xxx


第一种方法我们还是继续替换sys_call_table[]数组中函数指针,但是要自己处理hook函数的栈平衡。


写一段自己的stub_new_syscallxxx函数来替换原有的stub_syscallxxx函数:


stub_new_syscallxxx:    /**     * (1.1) 保存寄存器状态, 保证之后调用原来的stub_syscallxxx的时候CPU执行环境一致     * 其中rdi,rsi,rdx,rcx,rax,r8,r9,r10,r11保存sysenter的参数,rbx作为临时变量     */    pushq   %rbx    pushq   %rdi    pushq   %rsi    pushq   %rdx    pushq   %rcx    pushq   %rax    pushq   %r8    pushq   %r9    pushq   %r10    pushq   %r11   /* (1.2) 调用自己的hook函数 */    call    new_syscallxxx    test    %rax, %rax    movq    %rax, %rbx     /* (1.3) 恢复寄存器状态 */    pop     %r11    pop     %r10    pop     %r9    pop     %r8    pop     %rax    pop     %rcx    pop     %rdx    pop     %rsi    pop     %rdi     jz      new_syscallxxx_done        /* (2.1) new_syscallxxx返回值为非0时 */    movq    %rbx, %rax    pop     %rbx    ret   /* 这里不一定要jmp int_ret_from_sys_call,反正syscallxxx已经被我们拦截了 */        /* (2.2) new_syscallxxx返回值为0时 */new_syscallxxx_done:    pop     %rbx    jmp     *orig_sys_call_table(, %rax, 8) /* 调用原始的stub_syscallxxx */


这种方法要小心处理调用堆栈,在我们hook函数运行之前要小心的保护堆栈,在hook函数运行完成后要完全恢复堆栈。而且不方便实现post hook。


2.3 方法2:hook call sys_xxx


另一种方法我们替换stub_syscallxxx函数中的call sys_syscallxxx语句。例如:


ENTRY(stub_execve)  CFI_STARTPROC  addq $8, %rsp  PARTIAL_FRAME 0  SAVE_REST  FIXUP_TOP_OF_STACK %r11  movq %rsp, %rcx  call sys_execve             // 替换call语句中的sys_execve为new_sys_execve  RESTORE_TOP_OF_STACK %r11  movq %rax,RAX(%rsp)  RESTORE_REST  jmp int_ret_from_sys_call  CFI_ENDPROCEND(stub_execve)


查看原始指令码:


(gdb) disassemble /r stub_execveDump of assembler code for function stub_execve:   0xffffffff8146f7e0 <+0>:     48 83 c4 08     add    $0x8,%rsp   ...   0xffffffff8146f847 <+103>:   e8 74 b2 b9 ff  callq  0xffffffff8100aac0 <sys_execve>  // call sys_execve   ...   0xffffffff8146f890 <+176>:   e9 77 fd ff ff  jmpq   0xffffffff8146f60c <int_ret_from_sys_call>End of assembler dump.(gdb) p sys_execve$2 = {long (const char *, const char * const *, const char * const *, struct pt_regs *)} 0xffffffff8100aac0 <sys_execve>


我们可以看到call sys_execve对应的命令码为e8 74 b2 b9 ff,其中:


  • e8对应call指令。

  • ffb9b274表示被调用函数和当前pc的偏移:


call函数地址 - 当前地址 - 当前指令长度 = offset0xffffffff8100aac0 - 0xffffffff8146f847 - 5 = 0xFFFFFFFFFFB9B274 & 0xFFFFFFFF = 0xFFB9B274


所以我们只要定义个参数完全一致的新函数new_sys_execve(),把sys_execve()的对应偏移ffb9b274替换成new_sys_execve()的相对偏移即可。


static asmlinkage long new_sys_execve(const char __user * filename,        const char __user * const __user * argv,        const char __user * const __user * envp, struct pt_regs *regs) {  size_t exec_line_size;  char * exec_str = NULL;  char ** p_argv = (char **) argv;    long ret = 0;
/* (1) pre hook 点 */
/* Finally, call the original sys_execve */ /* (2) 调用原始系统调用 */ ret = orig_sys_execve_fn(filename, argv, envp, regs);
/* (3) post hook 点 */ printk("orig_sys_execve_fn ret = %d\n", ret);
return ret;}


具体代码放在inlinehook_syscall_example。


参考文档:

1.x86平台inline hook原理和实现

2.execmon

3.Linux x64下hook系统调用execve的正确方法


精华文章:【精华】Linux阅码场原创精华文章汇总



阅码场付费会员专业交流群

会员招募:各专业群会员费为88元/季度,权益包含群内提问,线下活动8折,全年不定期群技术分享(普通用户直播免费,分享后每次点播价为19元/次),有意加入请私信客服小月(小月微信号:linuxer2016)


专业群介绍:

彭伟林-阅码场内核性能与稳定性
本群定位内核性能与稳定性技术交流,覆盖云/网/车/机/芯领域资深内核专家,由阅码场资深讲师彭伟林主持。


甄建勇-性能优化与体系结构

本群定位Perf、cache和CPU架构技术交流,覆盖云/网/车/机/芯领域资深用户,由阅码场资深讲师甄建勇主持。


李春良-Xenomai与实时优化

本群定位Xenomai与实时优化技术交流,覆盖云/网/车/机/芯领域资深用户,由阅码场资深讲师李春良和彭伟林共同主持。


周贺贺-Tee和ARM架构

本群定位Tee和ARM架构技术交流,覆盖云/网/车/机/芯领域资深用户,由阅码场资深讲师周贺贺主持。


谢欢-Linux tracers

本群定位Linux tracers技术交流,覆盖云/网/车/机/芯领域资深用户,由阅码场资深讲师谢欢主持。