使用ftrace进行Linux内核hooking解决方案

通常命令行解释器(比如Bash)使用标准C库中的常用函数fork()和execve()来启动一个新进程。在系统内部,这些函数分别通过系统调用clone()和execve()来实现。我们hook execve()系统调用,以获得启动新进程的控制权。
下面的图给出了一个ftrace示例,并说明了hooking处理函数的过程。
在这里插入图片描述
在此图中,我们可以看到用户进程(蓝色)如何执行对内核(红色)的系统调用,其中ftrace框架(紫色)从我们的模块(绿色)调用函数。
下面,我们详细描述这个过程的每一步:

  1. SYSCALL指令由用户进程执行。该指令允许切换到内核模式,并让低级系统调用处理程序entry_SYSCALL_64()负责。此处理程序负责64位内核上64位程序的所有系统调用。
  2. 一个特定的处理器接收控制。内核快速完成汇编程序上实现的所有低级任务,并将控制权移交给高级的do_syscall_64()函数。该函数到达系统调用处理程序表sys_call_table,并通过系统调用号调用特定的处理程序。在我们的示例中,它是sys_execve()函数。
  3. 调用ftrace。在每个内核函数的开头都有一个fentry()函数调用。该函数由ftrace框架实现。在不需要跟踪的函数中,这个调用被替换为nop指令。然而,对于sys_execve()函数,没有这样的调用。
  4. Ftrace调用我们的回调。Ftrace调用所有注册的跟踪回调,包括我们的。其他回调不会干扰,因为在每个特定的位置,只能安装一个回调来更改%rip寄存器的值。
  5. 回调函数执行hooking。这个回调函数查看在do_syscall_64()函数内部的parent_ip引导的值——因为它是调用sys_execve()处理程序的特定函数——并决定hook函数,在pt_regs结构中更改寄存器%rip的值。
  6. Ftrace恢复寄存器的状态。在FTRACE_SAVE_REGS标志之后,框架在调用处理程序之前将注册状态保存在pt_regs结构中。当处理结束时,从相同的结构恢复寄存器。我们的处理程序修改了寄存器%rip——一个指向下一个执行函数的指针——这会导致将控制传递到一个新的地址。
  7. 包装函数接收控制。无条件跳转使它看起来像sys_execve()函数的激活已经终止。不是这个函数,而是fh_sys_execve()函数。同时,处理器和内存的状态保持不变,因此我们的函数接收原始处理程序的参数,并将控制权返回给do_syscall_64()函数。
  8. 原函数是由包装函数调用的。现在,系统调用在我们的控制之下。在分析系统调用的上下文和参数之后,fh_sys_execve()函数可以允许或禁止执行。如果禁止执行,函数返回一个错误代码。否则,函数需要重复对原始处理程序的调用,并且通过钩子设置期间保存的real_sys_execve指针再次调用sys_execve()。
  9. 回调获得控制权。就像在sys_execve()的第一次调用期间,控件通过ftrace到我们的回调。但这一次,这个过程以不同的方式结束。
  10. 回调什么也不做。sys_execve()函数不是由内核从do_syscall_64()调用的,而是由我们的fh_sys_execve()函数调用的。因此,寄存器保持不变,sys_execve()函数照常执行。唯一的问题是,ftrace两次看到sys_execve()的入口点。
  11. 包装函数获得控制权。系统调用处理程序sys_execve()第二次将控制权交给我们的fh_sys_execve()函数。现在,一个新进程的启动已经接近完成。我们可以看到execve()调用是否完成了一个错误,研究新的进程,对日志文件做一些注释,等等。
  12. 内核接收控制。最后,运行完fh_sys_execve()函数,并返回do_syscall_64()函数。该函数将调用视为正常完成的调用,而内核照常运行。
  13. 控制权转交给用户进程。最后,内核执行IRET指令(或SYSRET,但对于execve()只能执行IRET),为新用户进程安装寄存器,并将处理器切换到用户代码执行模式。系统调用结束了,新进程的启动也结束了。
  14. 显然,用ftrace hooking Linux内核函数调用的过程相对并不复杂。
    使用ftrace的利弊

Ftrace使Linux内核函数更容易hook,并具有几个关键优势:

  • 一个成熟的API和简单的代码。在内核中利用现成的接口大大降低了代码的复杂性。只需要进行两个函数调用,填充两个结构字段,并在回调中添加一些magic,就可以用ftrace来hook内核函数。剩下的代码只是围绕跟踪函数执行的事件逻辑。
  • 能够根据名称跟踪任何函数。使用ftrace跟踪Linux内核是一个相当简单的过程——用常规字符串编写函数名就足够指向你需要的函数名了。不需要纠结于链接器、扫描内存或研究内部内核数据结构。只要知道它们的名称,就可以使用ftrace跟踪内核函数,即使这些函数没有为模块导出。

但就像我们描述的其他方法一样,ftrace也有一些缺点。
内核配置要求。 确保成功进行Linux内核跟踪需要几个内核要求:

  • – 用于按名称搜索功能的kallsyms符号列表
  • – 用于执行跟踪的整个ftrace框架
  • – Ftrace选项对钩子函数来说至关重要

所有这些功能都可以在内核配置中禁用,因为它们对系统的运行并不重要。 但是,通常流行发行版使用的内核仍然包含所有这些内核选项,因为它们不会显著影响系统性能,并且可能对调试很有用。

  • 开销成本。因为ftrace不使用断点,所以它的开销比kprobes低。但是,这种方法的开销比手工拼接要高。实际上,动态ftrace是拼接的变体,它执行了不必要的ftrace代码和其他回调。
  • 函数被包装成一个整体。与通常的拼接一样,ftrace将函数包装为一个整体。虽然从技术上讲,拼接可以在函数的任何部分执行,而ftrace只在入口点工作。这种限制可以视为一种缺点,但通常它不会引起任何并发症。
  • 双重调用ftrace。正如我们之前解释过的,使用parent_ip指针进行分析会导致对同一个钩子函数调用两次ftrace。这增加了一些间接成本,并可能干扰其他跟踪的读取,因为他们将看到两次的调用。这个问题可以通过将原来的函数地址移动5个字节(调用指令的长度)来解决,这样基本上就可以在ftrace上跳转了。

内核配置要求
内核必须同时支持ftrace和kallsyms。这需要启用两个配置选项:

  • CONFIG_FTRACE
  • CONFIG_KALLSYMS

接下来,ftrace必须支持动态寄存器修改,开启以下选项:

  • CONFIG_DYNAMIC_FTRACE_WITH_REGS

要访问FTRACE_OPS_FL_IPMODIFY标志,你使用的内核必须基于版本3.19或更高版本。旧的内核版本仍然可以修改%rip寄存器,但是在版本3.19中,只有在设置标志之后才能修改这个寄存器。在较老版本的内核中,出现此标志将导致编译错误。在较新的版本中,缺失这个标志意味着一个non-operating hook。
我们还需要注意函数内部的ftrace调用位置。 ftrace调用必须位于函数的开头,在函数序言之前(形成堆栈帧并分配局部变量的空间)。 以下选项考虑了此功能:

  • CONFIG_HAVE_FENTRY

虽然x86_64架构支持这个选项,但i386架构不支持。由于i386架构的ABI限制,编译器不能在函数序言之前插入ftrace调用。因此,当执行ftrace调用时,函数堆栈已经被修改了,并且更改寄存器的值不足以hook函数。并且还需要撤消在序言中执行的操作,这些操作在不同的函数中有所不同。
这就是为什么ftrace函数hooking不支持32位x86体系结构。

结论
尽管ftrace的主要目的是跟踪Linux内核函数调用,而不是hook它们,但是,允许我们启用系统活动监控和阻止可疑进程,并且对于Hooking Linux内核函数非常有用。虽然这种方法有一些缺点,但是它的代码和hook过程整体简单,并且可以简单有效的用于钩子函数调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值