欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  网络运营

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

程序员文章站 2022-03-28 17:55:28
文章目录1. Ftrace Hook 原理1.1 Ftrace Hook框架1.2 对外接口2. Ftrace Hook 实例2.1 hook 过程2.2 CONFIG_DYNAMIC_FTRACE_WITH_REGS 特性支持3. 内核热补丁实例3.1 热补丁原理3.2 实例参考文档:1. Ftrace Hook 原理关于ftrace hook的原理在Linux ftrace一文中有详细的解析,本文就简单的阐述其原理。1.1 Ftrace Hook框架Ftrace Hook的初始设计主要是给ft...

1. Ftrace Hook 原理

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

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

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的调用时序:

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

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

本文地址:https://blog.csdn.net/pwl999/article/details/107426138

相关标签: Trace Security