Linux内核跟踪:ftrace hook入门手册(下)

VSole2022-07-22 15:38:22

一、前情提要

在前一篇文章《Linux内核跟踪:ftrace hook入门手册(上)》中,我们对部分ftrace hook经典方案中的实现细节进行了优化。本文会深入说明这些优化的原理和目的。

二、内核版本的差异

目前的ftrace hook实现中,总是需要使用大量条件编译以解决Linux内核的版本差异问题。其中较为关键的一个差异点,就是Linux内核从4.17版本开始修改了系统调用过程中的函数签名,这对ftrace hook的实现造成了较大的困扰。

下为4.16版本Linux内核源码/arch/x86/entry/common.c[1],尤其关注第287行,可见该版本Linux内核在执行系统调用时会将寄存器结构体中的6个参数展开来调用sys_call_table[nr]:

图1:Linux内核4.16版本do_syscall_64函数实现

而在4.17.0版本中,同样在第287行,可见已经改用单个参数(指向整个寄存器结构体的指针)来调用sys_call_table[nr]:

图2:Linux内核4.17版本do_syscall_64函数实现

而如前一篇文章所述,ftrace hook是通过编译时处理,在各个内核函数实现代码的开头插桩call指令,所以ftrace hook介入系统调用是在do_syscall_64之后:

图3:ftrace hook子程中打印的部分内核调用堆栈(上为栈顶,下为栈底)

因此ftrace中直接使用的hook子程在获取系统调用参数时,必须考虑这种差异才行。

三、 经典方案的缺陷

针对这个问题,在笔者找到的几乎所有经典方案[2]中,都通过条件编译定义了两套hook子程,分别适用于4.17版本前后的两种情况:

 图4:经典方案中条件编译两个hook子程

但这样实现的话,相同的功能都要写两套,代码开发和维护都十分不便。以一般的编程思路,我们可以封装定义一个形式上的hook子程函数(后简称外套子程),在这个外套子程中将传递到系统调用的参数统一结构后,再调用实际实现业务功能hook子程(后简称业务子程)。

然而事情并没有这么简单。经典方案通常针对x86架构,并不是在ftrace_set_filter_ip所设置的过滤器函数中调用hook子程,而是在这个过滤器函数中修改EIP/RIP寄存器到hook子程的入口地址。hook子程并非在ftrace框架内调用,而是在ftrace框架返回到系统调用时跳转到hook子程(而没有回到真正的系统调用函数)。

这种做法的好处是,hook子程在运行流程上直接替代了原有的系统调用函数,两者可以使用完全相同的函数签名处理业务,有点类似于修改系统调用表的hook方法。

hook子程可以直接定义与系统调用函数相同的形式参数来获取系统调用参数值,而返回时也会直接返回到系统调用函数的直接调用方(参考下图[3]):

 图5:经典方案中的hook执行流程

然而,由于Linux内核模块通常为纯C语言实现,缺少将参数值或者其它信息绑定到回调函数的原生支持。ftrace_set_filter_ip所设置的过滤器函数中姑且可以根据第三个参数所指向的地址来找到与当前hook实例有关的信息(即代码中的“container_of(ops,…)”)。但如果我们通过修改RIP跳转到外套子程,那就意味着所有的ftrace hook都会跳转到同一个外套子程,而此时外套子程所接收到的参数实际上是由系统调用函数的直接调用方(如do_syscall_64)提供的,我们很难在过滤器函数中修改或传递更多的参数给外套子程——结果导致在同时存在多个hook目标的情况下,外套子程内部难以确定应该调用哪个业务子程。

当然,并非完全没有方法来解决这个问题。我们可以将业务子程绑定到系统调用号,然后在外套子程中根据系统调用号(x86架构是AX)来找到对应的业务子程;还可以在过滤器函数中将额外信息存放在返回值寄存器(x86架构还是AX)中,而不影响其它运行流程。

四、 优化方案

不过,最为简单的优化方法,还是在过滤器函数内直接调用业务子程。经典方案中设置IP寄存器来进行跳转的根本目的,大概也只是为了让hook子程获取系统调用参数和执行返回逻辑。接下来,我们将会在过滤器函数内直接获取当前系统调用的参数,并设置它的返回值。

首先是参数值的获取。Linux系统调用的大致过程是,用户程序将系统调用的实际参数设置到特定的寄存器中,然后通过中断指令(int 30)切换到内核空间并实际执行系统调用过程。此时,用户空间的寄存器会以pt_regs结构体的形式,存储在当前内核栈空间的最高地址处。取得这个地址的方法有很多,前一篇文章中的代码可供参考:

//获取用户线程原本的寄存器保存位置struct pt_regs *GetUserRegisters(struct task_struct *task){    struct unwind_state state;    task = task ? : current;    unwind_start(&state, task, NULL, NULL);    return (struct pt_regs *)(((size_t)state.stack_info.end) - sizeof(struct pt_regs));}

或者下面的方法经验证也是可以的:

#include#if LINUX_VERSION_CODE>=KERNEL_VERSION(4,11,0)#include#endifstruct pt_regs *GetUserRegisters(struct task_struct *task){    return (struct pt_regs *)(task_stack_page(task ? : current) + THREAD_SIZE) - 1;}

获取到用户寄存器内容后,即可从中读取出系统调用的参数了。作为对经典方案的优化之一,我们可以在此处加入对架构和位宽等因素导致参数寄存器约定差异的处理:

static void notrace FTraceHookHandler(size_t ip, size_t parent_ip, struct ftrace_ops *ops, struct ftrace_regs *fregs){    struct pt_regs *kernel_regs = ftrace_get_regs(fregs);    struct pt_regs *user_regs = GetUserRegisters(NULL);#if PTREGS_SYSCALL_STUBS#define argument_regs user_regs#else#define argument_regs kernel_regs#endif#if defined(CONFIG_X86_64)#define INSTRUCTION_POINTER kernel_regs->ip    struct FTraceHookContext context =    {        .Hook = container_of(ops, struct FTraceHook, FTraceOPS),        .KernelRegisters = kernel_regs,        .UserRegisters = user_regs,        .SysCallNR = &argument_regs->ax,        .Arguments =        {            &argument_regs->di,            &argument_regs->si,            &argument_regs->dx,            &argument_regs->r10,            &argument_regs->r8,            &argument_regs->r9        },        .ReturnValue = &argument_regs->ax    };#elif defined(CONFIG_X86_32)#define INSTRUCTION_POINTER kernel_regs->ip    struct FTraceHookContext context =    {        .Hook = container_of(ops, struct FTraceHook, FTraceOPS),        .KernelRegisters = kernel_regs,        .UserRegisters = user_regs,        .SysCallNR = &argument_regs->ax,        .Arguments =        {            &argument_regs->bx,            &argument_regs->cx,            &argument_regs->dx,            &argument_regs->si,            &argument_regs->di,            &argument_regs->bp        },        .ReturnValue = &argument_regs->ax    };#else#error Unsupported architecture config?#endif    context.Hook->Handler(&context);    …其它hook业务流程…}

然后是返回流程和返回值的设置。如果过滤器函数正常返回,ftrace框架会让执行流程回到系统调用函数实现的开头。如果我们不希望这样,可以在代码中随便寻找一个返回指令(x86中为0xC3),然后在过滤器函数中修改IP寄存器到这个返回指令的位置即可:

#if defined(CONFIG_X86_64)||defined(CONFIG_X86_32)#define RET_CODE 0xC3#else#error Unsupported architecture config?#endifstatic size_t RET_ADDRESS; //在过滤器函数中static void notrace FTraceHookHandler(size_t ip, size_t parent_ip, struct ftrace_ops *ops, struct ftrace_regs *fregs){    struct pt_regs *kernel_regs = ftrace_get_regs(fregs);    struct pt_regs *user_regs = GetUserRegisters(NULL);#if PTREGS_SYSCALL_STUBS#define argument_regs user_regs#else#define argument_regs kernel_regs#endif    …其它hook业务流程…    if (希望跳过真实系统调用函数的执行而立即返回)    {        argument_regs->ax = 返回值;        kernel_regs->ip = RET_ADDRESS;    }} //在初始化函数中int FTraceHookInitialize(struct FTraceHook *hooks, size_t hooks_size){    //随便找一个ret指令的地址,基本上就用当前函数尾部的ret就好;如果求稳(比如担心当前函数内存在复杂的跳转等),可以另外定义一个空函数,注意避免选取内联函数    RET_ADDRESS = (size_t)FTraceHookInitialize;    while (* (unsigned char *) RET_ADDRESS != RET_CODE)        ++RET_ADDRESS;    …其它初始化流程…}

这样一来,我们就可以顺利地获取系统调用的参数、顺利地设置系统调用的返回值,因而没有必要再通过修改IP寄存器的方法跳转到hook子程了。

由于改在过滤器函数中调用hook子程,我们不仅可以轻易地根据过滤器函数的第三个参数确定hook实例信息,而且也不必再强制要求hook子程的函数签名保持与原始系统调用函数一致了。过滤器函数封装过程中,可以一站式解决大量的版本差异处理问题,包括对指令架构和位宽差异的处理等。

除此之外,由于优化方案中可以直接使用ftrace框架自带的防递归机制,经典方案中花费大量代码实现但仍然有所不足的防递归机制也就可以省略了。

五、 后记

实际上,相比于eBPF等用户空间的终端监控方法,ftrace hook这样的内核模块实现终究属于比较沉重的方案,尤其是开发过程中需要进行大量的系统适配处理和测试。

但相应地,ftrace hook可以实现很多eBPF中难以实现的功能,尤其是对系统调用的阻断等。如果您需要非常深入地监测和控制Linux主机上的应用活动,那么ftrace hook也不失为一种不错的选择。

更多前沿资讯,还请继续关注绿盟科技研究通讯。

如果您发现文中描述有不当之处,还请留言指出。在此致以真诚的感谢~

函数调用系统调用
本作品采用《CC 协议》,转载必须注明作者和本文链接
例如,木马病毒、勒索程序、系统漏洞等安全威胁给人们带来了巨大的经济损失,引发隐私泄露等安全问题。HMM-CBOW行为模型是王焱济在 2019 年提出的一种改进型序列行为模型。图 3 HMM-CBOW 行为模型HMM-CBOW 行为模型主要对软件运行过程中的系统调用序列、参数及返回值等进行细粒度的刻画,将序列进一步划分为方便存储、处理与查找的若干不同长度的短序列。
shellcode编写探究
2022-06-09 15:34:57
前言shellcode是不依赖环境,放到任何地方都可以执行的机器码。shellcode的应用场景很多,本文不研究shellcode的具体应用,而只是研究编写一个shellcode需要掌握哪些知识。要使用字符串,需要使用字符数组。所以我们需要用到 LoadLibrary 和 GetProcAddress 这两个函数,来动态获取系统API的函数指针。
API(Application Programming Interface),我们调用时只需提供正确的参数以及接收返回值就可以判断API执行是否成功或者通过GetLastError获得错误原因.
RCE系统交互条件与受限环境下的利用
威努特专家小组就某单位遭受勒索病毒攻击开展应急处置工作。
在学习漏洞的时候,按照0Day2书中第24章第1节的内容进行学习的,这章本来是远程拒绝服务的漏洞(CVE-2009-3103),但是当我在网上搜索这个漏洞的EXP时,意外的发现了Srv2.sys模块中的另一个漏洞(CVE-2009-2532),而这个漏洞竟然可以实现远程任意代码执行,诶,这我就不困了,然后顺手两个漏洞一起分析了,把Srv2.sys模块对数据包的接收处理过程逆向了一遍,了解了其中的漏
32位,调用了很多Win32 api,本次计时器的破解突破口就在这。这些操作本质是通过外设向操作系统发送了一些消息,操作系统通过窗口句柄把消息发送给对应窗口过程函数进行处理进而把处理结果通过窗口再次呈现给用户。将txt文件还原到文件夹中,程序在43FCE8顺序执行。patch文件以后,关闭网络连接发现仍然可以正常启动程序,至此成功绕过程序需要联网的要求。timekey破解开发者给出的限制是Timekey.txt里面的内容需要定期更新,大概是要每一个月更新一次。
前言本文主要着眼于glibc下的一些漏洞及利用技巧和IO调用链,由浅入深,分为 “基础堆利用漏洞及基本IO攻击” 与 “高版本glibc下的利用” 两部分来进行讲解,前者主要包括了一些glibc相关的基础知识,以及低版本glibc下常见的漏洞利用方式,后者主要涉及到一些较新的glibc下的IO调用链。
VMPWN的入门系列-2
2023-08-03 09:29:42
解释器是一种计算机程序,用于解释和执行源代码。与编译器不同,解释器不会将源代码转换为机器语言,而是直接执行源代码。即,这个程序接收一定的解释器语言,然后按照一定的规则对其进行解析,完成相应的功能,从本质上来看依然是一个虚拟机。总的来说,如果输入字符数小于0x10,string类的大概成员应该如下struct?
VSole
网络安全专家