当前位置: 首页 > news >正文

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_comparesystem_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>

sys_cloned调用流程

3. 解决

发现了问题,该如何解决呢?通过理清ptregs_sys_clone的调用路径,发现在执行stub_ptregs_64前,使用lea -0x80a6f7(%rip),%raxsys_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中,系统调用是用户态应用程序请求内核为其执行特权操作的唯一方式。其流程可以概括为以下几步:

  1. 将系统调用号写入RAX寄存器,同时将参数按顺序存入 rdi, rsi, rdx, r10, r8, r9 寄存器
  2. 通过特殊指令触发软中断int 0x80或者直接syscall进入内核
  3. 进入内核系统调用的入口代码entry_xxx(通常定义在entry_32.S和entry_64.S中)
  4. 执行系统调用前的准备工作
  5. 根据系统调用号执行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)
http://www.dtcms.com/a/393007.html

相关文章:

  • 了解 Highcharts 响应式功能:构建适配各种屏幕的图表界面
  • 逻辑分析仪解码脚本实例解析——UART
  • 垃圾回收中的STW是什么?
  • redis未授权漏洞扫描器
  • LTE/EPC 架构
  • ANSYS学习
  • 【python】安装jieba库
  • tyza66的博客:专注软件开发、全栈开发与开源项目的技术分享
  • Redis最佳实践——购物车优化详解
  • Netty从0到1系列之Netty内存管理【下】
  • 【使用函数求余弦COS函数的近似值】2022-11-27
  • 前端违规页面车主信息优化说明
  • 成功安装了 Anaconda3。要启动它,您有以下几种主要方式:方式一:通过“开始菜单”启动(最直接的方法)1. 点击您电脑屏幕左下角的 “开始菜单”(Win
  • flex布局实现导航栏横向滚动切换
  • 改进过程缺乏数据驱动会带来哪些后果
  • 实验1.1点亮led灯
  • 林粒粒的视频笔记13-数据清洗
  • Java进阶教程,全面剖析Java多线程编程,线程出让,笔记09
  • 大模型微调之 用LoRA微调Llama2(附代码)——李宏毅2025大模型作业5笔记-上
  • Matplotlib地理数据可视化技术详解:Cartopy与Basemap实战指南
  • wordpress 图片不显示 后台无法登陆的问题之一
  • TFS-2023《Local-Global Fuzzy Clustering With Anchor Graph》
  • Spring —— AOP
  • 讲一下ZooKeeper的持久化机制
  • 【Java后端】深入理解 Spring Security:从原理到实战
  • LeetCode:31.K个一组翻转链表
  • openharmony之系统亮度范围定制
  • 一种利用串口51单片机远程升级 OTA
  • Redis三种集群模式
  • C++ map_set封装