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

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()作为跳板,把被原函数cmdline_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()会把上一级函数的寄存器环境保存到pt_regs中,并传递给ftrace_ops_list_func()。

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.揭露内核黑科技 - 热补丁技术真容

posted @ 2020-07-18 11:21  pwl999  阅读(395)  评论(0编辑  收藏  举报