Linux 4.x hook系统调用的问题
1. 背景
在基于Linux 4.9.0的某国产定制内核系统上,hook系统调用,导致systemctl命令无法正常启动服务。
2. 调查
使用strace命令追踪systemctl命令启动时的系统调用,比较两种情况下的输出结果,发现启用hook之后,主进程clone出的新进程执行被主进程发送的SIGTERM
杀死,因此怀疑clone调用的hook出现了问题。
strace -f systemctl start sshd
使用kprobe向sys_clone
注册探针,打印触发sys_clone
的堆栈,依旧比较两种情况下的输出。
static int pre_handler(struct kprobe *p, struct pt_regs* regs)
{return 0;
}static void post_handler(struct kprobe *p, struct pt_regs* regs, unsigned long flags)
{dump_stack();return ;
}static struct kprobe kp =
{.symbol_name = "sys_clone",.pre_handler = pre_handler,.post_handler = post_handler,
};static int __init kprobe_init(void)
{int ret = 0;ret = register_kprobe(&kp);if(ret < 0)return ret;
}static void __exit kprobe_exit(void)
{unregister_kprobe(&kp);
}MODULE_LICENSE("GPL");
module_init(kprobe_init);
module_exit(kprobe_exit);
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
[414707.002758] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 11/12/2020
[414707.002760] ffffb10d4001bd38 ffffffff8654e0d3 ffffffffc0841000 0000000000000292
[414707.002764] ffffb10d4001bd48 ffffffffc083f01e ffffb10d4001bd88 ffffffff862597a4
[414707.002768] ffffffff8627bad5 ffffffff86f24a40 0000000000000800 ffffffff8627bad0
[414707.002771] Call Trace:
[414707.002777] [<ffffffff8654e0d3>] dump_stack+0x63/0x90
[414707.002783] [<ffffffffc083f01e>] post_handler+0xe/0x10 [clone]
[414707.002787] [<ffffffff862597a4>] kprobe_ftrace_handler+0x104/0x110
[414707.002791] [<ffffffff8627bad5>] ? _do_fork+0x5/0x410
[414707.002793] [<ffffffff8627bad0>] ? fork_idle+0xe0/0xe0
[414707.002795] [<ffffffff8627bf89>] ? SyS_clone+0x19/0x20
[414707.002799] [<ffffffff8633cdbd>] ftrace_ops_list_func+0xcd/0x1b0
[414707.002802] [<ffffffff86842f65>] ftrace_regs_call+0x5/0x72
[414707.002805] [<ffffffff8627bf70>] ? sys_vfork+0x30/0x30
[414707.002807] [<ffffffff8627bad5>] ? _do_fork+0x5/0x410
[414707.002809] [<ffffffff8627bad5>] _do_fork+0x5/0x410
[414707.002811] [<ffffffff8627bf89>] SyS_clone+0x19/0x20
[414707.002813] [<ffffffff8627bad5>] ? _do_fork+0x5/0x410
[414707.002815] [<ffffffff8627bf89>] ? SyS_clone+0x19/0x20
[414707.002821] [<ffffffffc0b65db6>] my_clone+0x66/0xd0 [my_hook]
[414707.002826] [<ffffffff868414bb>] system_call_fast_compare_end+0xc/0x9b
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
在结果中发现hook之后,sys_clone
的调用比正常情况下多出了system_call_fast_compare_end
从进入的调用。
由于system_call_fast_compare_end
的调用方不明确,因此尝试从4.9内核源码中找到相应的函数。
ENTRY(entry_SYSCALL_64)/* ... */movq PER_CPU_VAR(current_task), %r11testl $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags(%r11)jnz entry_SYSCALL64_slow_pathentry_SYSCALL_64_fastpath:TRACE_IRQS_ONENABLE_INTERRUPTS(CLBR_NONE)
#if __SYSCALL_MASK == ~0cmpq $__NR_syscall_max, %rax
#elseandl $__SYSCALL_MASK, %eaxcmpl $__NR_syscall_max, %eax
#endifja 1f /* return -ENOSYS (already in pt_regs->ax) */movq %r10, %rcx/** This call instruction is handled specially in stub_ptregs_64.* It might end up jumping to the slow path. If it jumps, RAX* and all argument registers are clobbered.*/call *sys_call_table(, %rax, 8)
.Lentry_SYSCALL_64_after_fastpath_call:movq %rax, RAX(%rsp)
1:/* ... */
entry_SYSCALL64_slow_path:/* IRQs are off. */SAVE_EXTRA_REGSmovq %rsp, %rdicall do_syscall_64 /* returns with IRQs disabled */return_from_SYSCALL_64:/* ... */
END(entry_SYSCALL_64)
其中的关键代码如上所示,当syscall进入内核后,根据TASK_TI_flags
决定走entry_SYSCALL64_slow_path
或者entry_SYSCALL_64_fastpath
。
由于system_call_fast_compare_end
不存在源码中,因此选择查询系统符号表的地址一看究竟。
$ grep system_call_fast_compare /boot/System.map-4.9.0-0
ffffffff816414a5 T system_call_fast_compare
ffffffff816414af T system_call_fast_compare_end
$ grep entry_SYSCALL /boot/System.map-4.9.0-0
ffffffff81641450 T entry_SYSCALL_64
ffffffff81641453 T entry_SYSCALL_64_after_swapgs
ffffffff8164149d t entry_SYSCALL_64_fastpath
ffffffff8164154a t entry_SYSCALL64_slow_path
ffffffff81642d00 T entry_SYSCALL_compat
通过反汇编vmlinux文件,找到对应的汇编函数。(这里由于有问题的内核无法展开,因此使用了ubuntu16预编译的4.9内核文件)
ffffffff8188d81d: 50 push %rax
ffffffff8188d81e: ff 15 f4 d5 59 00 callq *0x59d5f4(%rip)
ffffffff8188d824: 58 pop %rax # 对应的源码
ffffffff8188d825: 25 ff ff ff bf and $0xbfffffff,%eax # andl $__SYSCALL_MASK, %eax ①
ffffffff8188d82a: 3d 23 02 00 00 cmp $0x223,%eax # cmpl $__NR_syscall_max, %eax
ffffffff8188d82f: 77 0f ja 0xffffffff8188d840 # ja 1f ②
ffffffff8188d831: 4c 89 d1 mov %r10,%rcx # movq %r10, %rcx
ffffffff8188d834: ff 14 c5 80 01 a0 81 callq *-0x7e5ffe80(,%rax,8) # call *sys_call_table(, %rax, 8)
ffffffff8188d83b: 48 89 44 24 50 mov %rax,0x50(%rsp) # movq %rax, RAX(%rsp)
ffffffff8188d840: 50 push %rax
ffffffff8188d841: ff 15 c9 d5 59 00 callq *0x59d5c9(%rip)
ffffffff8188d847: 58 pop %rax
从反汇编代码中,结合符号地址的偏移,可以看出 ①和②分别对应了system_call_fast_compare
和system_call_fast_compare_end
的地址。
因此,得出结论system_call_fast_compare_end
实际上走的是entry_SYSCALL_64_fastpath
这一路径,与通常调用不同。
查看快调用路径的执行逻辑,可以发现这是专门用于处理stub_ptregs_64
的,而stub_ptregs_64
又被ptregs_xxx
调用。
这里,我们发现了问题:通常情况下,系统调用表指向的函数形如sys_clone
,而在这一版本中,部分函数被ptregs_stub
额外封装了一层。
我们本来想要替换sys_clone
函数的地址,而最终替换的确是ptregs_sys_clone
,而这就导致了快路径的调用无法正常执行。
ENTRY(stub_ptregs_64)cmpq $.Lentry_SYSCALL_64_after_fastpath_call, (%rsp)jne 1fDISABLE_INTERRUPTS(CLBR_NONE)TRACE_IRQS_OFFpopq %raxjmp entry_SYSCALL64_slow_path1:jmp *%rax /* Called from C */
END(stub_ptregs_64).macro ptregs_stub func
ENTRY(ptregs_\func)leaq \func(%rip), %raxjmp stub_ptregs_64
END(ptregs_\func)
.endm/* Instantiate ptregs_stub for each ptregs-using syscall */
#define __SYSCALL_64_QUAL_(sym)
#define __SYSCALL_64_QUAL_ptregs(sym) ptregs_stub sym
#define __SYSCALL_64(nr, sym, qual) __SYSCALL_64_QUAL_##qual(sym)
#include <asm/syscalls_64.h>
3. 解决
发现了问题,该如何解决呢?通过理清ptregs_sys_clone
的调用路径,发现在执行stub_ptregs_64
前,使用lea -0x80a6f7(%rip),%rax
将sys_clone
的地址保存到rax中,而在stub_ptregs_64
的最后,通过jmp *%rax
直接跳转执行。因此,可以想到使用inline hook的方式替换原本sys_clone
的地址。
$grep ptregs_ /boot/System.map-4.9.0-040900-generic
ffffffff8188d9a0 T stub_ptregs_64
ffffffff8188d9c0 T ptregs_sys_rt_sigreturn
ffffffff8188d9d0 T ptregs_sys_clone
ffffffff8188d9e0 T ptregs_sys_fork
ffffffff8188d9f0 T ptregs_sys_vfork
ffffffff8188da00 T ptregs_sys_execve
ffffffff8188da10 T ptregs_sys_iopl
ffffffff8188da20 T ptregs_sys_execveat
ffffffff8188da30 T ptregs_compat_sys_execve
ffffffff8188da40 T ptregs_compat_sys_execveat
$ sed -n "$((N)),$((N+1))"p vmlinux.out
ffffffff8188d9d0: 48 8d 05 09 59 7f ff lea -0x80a6f7(%rip),%rax # 0xffffffff810832e0
ffffffff8188d9d7: eb c7 jmp 0xffffffff8188d9a0
4. 扩展
4.1 x86-64架构Linux系统调用
1. 调用流程
在Linux中,系统调用是用户态应用程序请求内核为其执行特权操作的唯一方式。其流程可以概括为以下几步:
- 将系统调用号写入RAX寄存器,同时将参数按顺序存入
rdi
,rsi
,rdx
,r10
,r8
,r9
寄存器 - 通过特殊指令触发软中断
int 0x80
或者直接syscall
进入内核 - 进入内核系统调用的入口代码
entry_xxx
(通常定义在entry_32.S和entry_64.S中) - 执行系统调用前的准备工作
- 根据系统调用号执行
sys_call_table
中的函数
2. 系统调用表
系统调用表本质上是一个函数指针数组,在 Linux 内核中,这个表通常命名为 sys_call_table
。在大多数内核中,这个表的地址都作为全局符号导出,可以在系统符号中查看。
$ grep sys_call_table /boot/System.map-4.9.0-040900-generic
ffffffff81a00180 R sys_call_table
ffffffff81a01520 R ia32_sys_call_table
那么sys_call_table
作为一个数组,是在什么时候初始化的呢?通过查看内核源码,我们可以发现以下定义:
#define __SYSCALL_64_QUAL_(sym) sym
#define __SYSCALL_64_QUAL_ptregs(sym) ptregs_##sym
#define __SYSCALL_64(nr, sym, qual) [nr] = __SYSCALL_64_QUAL_##qual(sym),extern long sys_ni_syscall(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long);asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {/** Smells like a compiler bug -- it doesn't work* when the & below is removed.*/[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};
其中的编译时生成的文件asm/syscalls_64.h
文件内容如下所示:
__SYSCALL_64(0, sys_read, )
__SYSCALL_64(1, sys_write, )
//...
__SYSCALL_64(331, sys_pkey_free, )
//...
结合以上两个文件的内容,就可以得到最终的调用表。
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {/** Smells like a compiler bug -- it doesn't work* when the & below is removed.*/[0 ... __NR_syscall_max] = &sys_ni_syscall,[0] = sys_read,[1] = sys_write,//...[331] = sys_pkey_free,//...
};
4.2 内核反汇编
1. 内核提取
vmlinux
是编译过程结束后生成的最原始、最完整的 Linux 内核可执行文件(ELF 格式)vmlinuz
是经过压缩的、经过处理的、可以直接被引导程序加载并用于启动系统的 Linux 内核镜像
由于系统提供的往往都是vmlinuz文件,因此想要进行反汇编的话就需要使用extract-vmlinux
脚本从vmlinuz提取vmlinux文件。
$ /usr/src/linux-headers-4.9.0-040900/scripts/extract-vmlinux /boot/vmlinuz-4.9.0-040900-generic > vmlinux
$ file /boot/vmlinuz-4.9.0-040900-generic vmlinux
/boot/vmlinuz-4.9.0-040900-generic: Linux kernel x86 boot executable bzImage, version 4.9.0-040900-generic (kernel@tangerine) #201612111631 SMP Sun D, RO-rootFS, swap_dev 0x7, Normal VGA
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=08aef252a04f81d14fae766d7fca34fca847c8ec, stripped
2. 内核反汇编
$ objdump -D vmlinux > vmlinux.out
3. 查看函数符号
这里以函数ptregs_sys_clone
为例。/proc/kallsyms存储了所有的内核符号表,/boot/System.map则存储了静态的内核符号表。
$ grep ptregs_sys_clone /boot/System.map-$(uname -r)
ffffffff8188d9d0 T ptregs_sys_clone$ egrep -in ffffffff8188d9d0 vmlinux.out
2416033:ffffffff8188d9d0: 48 8d 05 09 59 7f ff lea -0x80a6f7(%rip),%rax # 0xffffffff810832e0$ N=2416033
$ sed -n "$((N)),$((N+3))"p vmlinux.out
ffffffff8188d9d0: 48 8d 05 09 59 7f ff lea -0x80a6f7(%rip),%rax # 0xffffffff810832e0
ffffffff8188d9d7: eb c7 jmp 0xffffffff8188d9a0
ffffffff8188d9d9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)