Ftrace Hook (Linux内核热补丁) 详解
伟林,中年码农,从事过电信、手机、安全、芯片等行业,目前依旧从事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、首先,被hook的func()入口桩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()。该函数的主要功能就是针对需要hook的func(),使能其1级hook点和2级hook点。
2、register_ftrace_function()。该函数的主要功能就是把新的hook函数加入到ftrace_ops_list链表中,使其在3级链表调用点能正常工作。
2.Ftrace Hook 实例
2.1 hook 过程
本例假设我们要hook掉cat /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.揭露内核黑科技 - 热补丁技术真容