查看原文
其他

Ftrace Hook (Linux内核热补丁) 详解

伟林 Linux阅码场 2022-12-14

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


文章目录

1. Ftrace Hook 原理

1.1 Ftrace Hook框架

1.2 对外接口

2. Ftrace Hook 实例

2.1 hook 过程

2.2CONFIG_DYNAMIC_FTRACE_WITH_REGS 特性支持

3. 内核热补丁实例

3.1 热补丁原理

3.2 实例

参考文档:


1.Ftrace Hook 原理

关于ftrace hook的原理在Linux ftrace一文中有详细的解析,本文就简单的阐述一下核心框架。



1.1 Ftrace Hook框架


Ftrace Hook的初始设计主要是给ftrace独家使用的,它的主要框架如下:


  • 1、在gcc使用了“-pg”选项以后,会在每个函数的入口插入桩函数_mcount()。ftrace为了不影响性能会在系统初始化时把_mcount()桩函数全部替换成nop指令。在ftrace开始工作时需要配置以下几步。

  • 2、首先,被hookfunc()入口桩call _mcount()被替换成ftrace_caller()/ftrace_regs_caller(),这里称为1级hook点。

  • 3、下一步,ftrace_caller()/ftrace_regs_caller()函数内的call ftrace_stub被替换成ftrace_ops_no_ops()/ftrace_ops_list_ops(),这里称为2级hook点。

  • 4、最后,在ftrace_ops_no_ops()/ftrace_ops_list_ops()函数中会逐个调用ftrace_ops_list链表中的函数。我们ftrace保存数据的函数也是注册到这个链表当中。这里称为3级链表调用点。


1.2 对外接口


对性能和安全应用来说,都需要在系统的关键路径上加上监控。ftrace hook这种能hook每个函数的机制是人人都想利用的。


针对大家的强烈需求,ftrace把自己的hook功能封装好给大家都能使用。


核心函数就2个:


  • 1、ftrace_set_filter_ip()。该函数的主要功能就是针对需要hookfunc(),使能其1级hook点和2级hook点。

  • 2、register_ftrace_function()。该函数的主要功能就是把新的hook函数加入到ftrace_ops_list链表中,使其在3级链表调用点能正常工作。

2.Ftrace Hook 实例

2.1 hook 过程


本例假设我们要hookcat /proc/cmdline的原有函数cmdline_proc_show()


  • 1、首先使用上一节的两个对外接口函数ftrace_set_filter_ip()register_ftrace_function(),把新的ops注册上去:


struct ftrace_hook { const char *name; void *function; void *original;
unsigned long address; struct ftrace_ops ops;};
#define HOOK(_name, _function, _original) \ { \ .name = (_name), \ .function = (_function), \ .original = (_original), \ }
static struct ftrace_hook hooked_functions[] = { HOOK("cmdline_proc_show", fh_cmdline_proc_show, &real_cmdline_proc_show),};

static int fhook_init(void){ int ret; int i;
for (i=0; i<(sizeof(hooked_functions)/sizeof(struct ftrace_hook)); i++){
/* (1) 对"cmdline_proc_show()"函数进行hook */ ret = fh_install_hook(&hooked_functions[i]); if (ret){ printk(" install ftrace hook fail! \n"); return ret; } }
return 0;}module_init(fhook_init);

int fh_install_hook (struct ftrace_hook *hook){ int err;
/* (1.1) 查找"cmdline_proc_show()"函数地址并且备份 */ err = resolve_hook_address(hook); if (err) return err;
/* (1.2) 初始化ops结构,ops的处理函数为fh_ftrace_thunk() */ hook->ops.func = fh_ftrace_thunk; hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_IPMODIFY;
/* (1.3) 使能"cmdline_proc_show()"函数对应的1级hook点和2级hook点 */ err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); if (err) { pr_debug("ftrace_set_filter_ip() failed: %d\n", err); return err; }
/* (1.4) 把ops注册到ftrace_ops_list链表中 */ err = register_ftrace_function(&hook->ops); if (err) { pr_debug("register_ftrace_function() failed: %d\n", err);
/* Don’t forget to turn off ftrace in case of an error. */ ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
return err; }
return 0;}

static int resolve_hook_address (struct ftrace_hook *hook){ /* (1.1.1) 根据函数名找到对应地址 */ hook->address = kallsyms_lookup_name(hook->name);
if (!hook->address) { pr_debug("unresolved symbol: %s\n", hook->name); return -ENOENT; }
/* (1.1.2) 备份原有函数指针 */ *((unsigned long*) hook->original) = hook->address;
return 0;}

      


  • 2、使用ops函数fh_ftrace_thunk()作为跳板,把被原函数cmd-line_proc_show()替换成hook函数fh_cmdline_proc_show()


上一步,我们把ops函数fh_ftrace_thunk()插入到了cmdline_proc_show()的入口ftrace hook点当中。


但是如果我们的函数只是运行在这个上下文的话,我们只能知道原函数运行的时机,但是我们拿不到原函数运行的数据。想要拿到数据,最好的方法是定义一个和原函数参数一致的函数,并且插入到原函数原有调用点。


ftrace hook使用fh_ftrace_thunk()作为跳板,实现了上述功能:


static void notrace fh_ftrace_thunk (unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs){ struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops);
/* Skip the function calls from the current module. */ /* (1) 防止递归 */ if (!within_module(parent_ip, THIS_MODULE)) /* (2) 最核心的技巧: 通过修改`ftrace_caller()/ftrace_regs_caller()`函数的返回函数来实现hook 原本执行完ftrace hook后返回原函数`cmdline_proc_show()` 将其替换成新函数`fh_cmdline_proc_show()` */ regs->ip = (unsigned long) hook->function;}


上述的技巧需要CONFIG_DYNAMIC_FTRACE_WITH_REGS特性的支持。


新的函数fh_cmdline_proc_show()接管原函数cmdline_proc_show()以后,可以做3类事情:pre hook调用原函数post hook


这样hook函数既能插入新的处理逻辑又能和原函数保持兼容。


/* 定义和原函数参数一致的fh_cmdline_proc_show()函数 */static int fh_cmdline_proc_show(struct seq_file *m, void *v){ int ret; /* (1) pre hook 点 */ seq_printf(m, "%s\n", "this has been ftrace hooked");
/* (2) 调用原函数 */ ret = real_cmdline_proc_show(m, v);
/* (3) post hook点 */ pr_debug("cmdline_proc_show() returns: %ld\n", ret); return ret;}



  • 3、hook 时序图


下图以fh_sys_execve()hook原函数sys_execve()为例,描述了整个ftrace hook的调用时序:




2.2 CONFIG_DYNAMIC_FTRACE_WITH_REGS 特性支持


上一节中说过fh_ftrace_thunk()中的跳板功能需要CONFIG_DYNAMIC_FTRACE_WITH_REGS的支持,我们来进一步看一下实现细节。


  • 1、没有CONFIG_DYNAMIC_FTRACE_WITH_REGS:


ftrace_caller()static void ftrace_ops_no_ops(unsigned long ip, unsigned long parent_ip){ __ftrace_ops_list_func(ip, parent_ip, NULL, NULL);}



  • 2、有CONFIG_DYNAMIC_FTRACE_WITH_REGS:


ftrace_regs_caller() { save pt_regs call ftrace_stub // ftrace_ops_list_func()
restore pt_regs}static void ftrace_ops_list_func(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *op, struct pt_regs *regs){ __ftrace_ops_list_func(ip, parent_ip, NULL, regs);}static void notrace ftrace_hook(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *ops, struct pt_regs *regs){ struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops);
/* 通过更改堆栈中的返回地址来插入hook, 如果没有传入pt_regs,就不能插入hook */ regs->ip = (unsigned long) hook->function;}



  • 3、版本支持


arm64 :kernel 5.5版本后才支持CONFIG_DYNAMIC_FTRACE_WITH_REGS
x86_64  :kernel 3.19版本后才支持CONFIG_DYNAMIC_FTRACE_WITH_REGS


3.内核热补丁实例

3.1 热补丁原理


通过上一节的原理分析,我们可以看到使用ftrace hook我们可以轻松替换掉内核中的一个函数。这种操作可以用来做内核的热补丁。


毫无疑问内核的开发者同样想到了这一点。我们看看内核热补丁的核心函数实现:


klp_enable_patch() → __klp_enable_patch() → klp_enable_object() → klp_enable_func()
static int klp_enable_func(struct klp_func *func){ struct klp_ops *ops; int ret;
if (WARN_ON(!func->old_addr)) return -EINVAL;
if (WARN_ON(func->state != KLP_DISABLED)) return -EINVAL;
ops = klp_find_ops(func->old_addr); if (!ops) { ops = kzalloc(sizeof(*ops), GFP_KERNEL); if (!ops) return -ENOMEM;
/* (1) 初始化ops和跳板函数klp_ftrace_handler() */ ops->fops.func = klp_ftrace_handler; ops->fops.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_DYNAMIC | FTRACE_OPS_FL_IPMODIFY;
list_add(&ops->node, &klp_ops);
INIT_LIST_HEAD(&ops->func_stack); list_add_rcu(&func->stack_node, &ops->func_stack);
/* (2) 使能1级hook点和2级hook点 */ ret = ftrace_set_filter_ip(&ops->fops, func->old_addr, 0, 0); if (ret) { pr_err("failed to set ftrace filter for function '%s' (%d)\n", func->old_name, ret); goto err; }
/* (3) 将ops加入ftrace_ops_list链表 */ ret = register_ftrace_function(&ops->fops); if (ret) { pr_err("failed to register ftrace handler for function '%s' (%d)\n", func->old_name, ret); ftrace_set_filter_ip(&ops->fops, func->old_addr, 1, 0); goto err; }

} else { list_add_rcu(&func->stack_node, &ops->func_stack); }
func->state = KLP_ENABLED;
return 0;
err: list_del_rcu(&func->stack_node); list_del(&ops->node); kfree(ops); return ret;}

static void notrace klp_ftrace_handler(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *fops, struct pt_regs *regs){ struct klp_ops *ops; struct klp_func *func;
ops = container_of(fops, struct klp_ops, fops);
rcu_read_lock(); func = list_first_or_null_rcu(&ops->func_stack, struct klp_func, stack_node); if (WARN_ON_ONCE(!func)) goto unlock;
/* (1.1) 通过修改`ftrace_caller()/ftrace_regs_caller()`函数的返回函数来实现hook 原本执行完ftrace hook后返回原函数 将其替换成新函数`func->new_func()` */ klp_arch_set_pc(regs, (unsigned long)func->new_func);unlock: rcu_read_unlock();}


可以看到hook的原理和上一节完全一致。


3.2 实例


kernel\samples\livepatch\livepatch-sample.c路径下有一个内核热补丁的简单例子,大家可以自行阅读。


/* * This (dumb) live patch overrides the function that prints the * kernel boot cmdline when /proc/cmdline is read. * * Example: * * $ cat /proc/cmdline * <your cmdline> * * $ insmod livepatch-sample.ko * $ cat /proc/cmdline * this has been live patched * * $ echo 0 > /sys/kernel/livepatch/livepatch_sample/enabled * $ cat /proc/cmdline * <your cmdline> */
#include <linux/seq_file.h>static int livepatch_cmdline_proc_show(struct seq_file *m, void *v){ seq_printf(m, "%s\n", "this has been live patched"); return 0;}
static struct klp_func funcs[] = { { .old_name = "cmdline_proc_show", .new_func = livepatch_cmdline_proc_show, }, { }};
static struct klp_object objs[] = { { /* name being NULL means vmlinux */ .funcs = funcs, }, { }};
static struct klp_patch patch = { .mod = THIS_MODULE, .objs = objs,};
static int livepatch_init(void){ int ret;
ret = klp_register_patch(&patch); if (ret) return ret; ret = klp_enable_patch(&patch); if (ret) { WARN_ON(klp_unregister_patch(&patch)); return ret; } return 0;}
static void livepatch_exit(void){ WARN_ON(klp_disable_patch(&patch)); WARN_ON(klp_unregister_patch(&patch));}


参考文档:

1.Linux ftrace

2.如何使用Ftrace hook函数

3.揭露内核黑科技 - 热补丁技术真容

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存