ftrace是一个内部跟踪程序,旨在帮助系统的开发人员和设计人员弄清楚内核内部发生的情况。它可以用于调试或分析在用户空间之外发生的延迟和性能问题。虽然ftrace通常被认为是函数跟踪程序,但它实际上是几个不同的跟踪实用程序的框架。…
3.1安装编译环境
yum install gcc kernel-devel-$(uname -r)
3.2一个简单的内核模块
static int __init Initialize(void)
{
pr_info("Hello, world!n");
return 0;
}
static void __exit Finalize(void)
{
pr_info("Bye, world!n");
}
module_init(Initialize);
module_exit(Finalize);
obj-m += HelloWorld.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
make
insmod HelloWorld.ko
lsmod
rmmod HelloWorld
dmesg /*一次性打印整个缓冲区*/
dmesg --follow /*持续打印缓冲区,直到Ctrl+C中断*/
dmesg --clear /*清空缓冲区*/
3.3在内核模块中包含多个源文件
static int __init Initialize(void)
{
pr_info("3+5=%d!n",Add(3,5));
return 0;
}
static void __exit Finalize(void)
{
}
module_init(Initialize);
module_exit(Finalize);
int Add(int a,int b)
{
return a+b;
}
int Add(int a,int b);
obj-m += MultipleCFiles.o
MultipleCFiles-objs := entry.o function.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
图11:不正确的函数static修饰导致模块无法安装
//关于系统调用符号解析的版本差异处理
static size_t kallsyms_lookup_name(const char *name)
{
struct kprobe kp = { .symbol_name = name };
size_t retval;
if (register_kprobe(&kp) < 0)
return 0;
retval = (size_t)kp.addr;
unregister_kprobe(&kp);
return retval;
}
//关于ftrace框架的版本差异处理
static __always_inline struct pt_regs *ftrace_get_regs(struct ftrace_regs *fregs)
{
return fregs;
}
//关于系统调用函数签名的版本差异处理
//关于ret指令机器码的架构差异处理
struct FTraceHook;
struct FTraceHookContext;
struct FTraceHook
{
const char *Symbol;
bool (*Handler)(struct FTraceHookContext *);
size_t SysCallEntry;
struct ftrace_ops FTraceOPS;
};
struct FTraceHookContext
{
struct FTraceHook *const Hook;
struct pt_regs *const KernelRegisters;
struct pt_regs *const UserRegisters;
size_t *const SysCallNR;
const size_t *const Arguments[6];
size_t *const ReturnValue;
};
struct pt_regs *GetUserRegisters(struct task_struct *task);
size_t FTraceHookCallOriginal(struct FTraceHookContext *context);
int FTraceHookInstall(struct FTraceHook *hook);
int FTraceHookUninstall(struct FTraceHook *hook);
int FTraceHookInitialize(struct FTraceHook *hooks, size_t hooks_size);
int FTraceHookFinalize(struct FTraceHook *hooks, size_t hooks_size);
FTraceHook.c:
static size_t RET_ADDRESS;
//获取用户线程原本的寄存器保存位置
struct pt_regs *GetUserRegisters(struct task_struct *task)
{
//用户线程原本的寄存器保存在内核栈地址最高处,为pt_regs结构体
//实际上,我们只是想要unwind_state里面的stack_info,但有些编译环境中不知为何会报错:ERROR: modpost: "get_stack_info" undefined!,有知情的读者还请不吝赐教,非常感谢
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));
}
//代理调用原始函数
size_t FTraceHookCallOriginal(struct FTraceHookContext *context)
{
return ((asmlinkage size_t (*)(struct pt_regs *)) context->Hook->SysCallEntry)(context->UserRegisters);
//asmlinkage的参数,传多了似乎没什么影响,有switch的功夫还不如多push几个参数咧
return ((asmlinkage size_t (*)(size_t, size_t, size_t, size_t, size_t, size_t)) context->Hook->SysCallEntry)(*context->Arguments[0], *context->Arguments[1], *context->Arguments[2], *context->Arguments[3], *context->Arguments[4], *context->Arguments[5]);
}
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);
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
};
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
};
if (!context.Hook->Handler(&context)) //返回false则阻止原始函数执行(直接返回到原始函数的调用方),其余情况不需要特殊操作,任由ftrace框架恢复执行流程即可
INSTRUCTION_POINTER = RET_ADDRESS;
}
int FTraceHookInstall(struct FTraceHook *hook)
{
int err;
//使用kallsyms_lookup_name()在内核内存中查找地址。
hook->SysCallEntry = kallsyms_lookup_name(hook->Symbol);
if (!hook->SysCallEntry)
{
pr_err("[FTraceHook] Unresolved symbol: %sn", hook->Symbol);
return ENOENT;
}
//我们的hook子程并不是通过修改ip跳转过去的,可以使用ftrace自带的防递归,而且实测效率还不错
hook->FTraceOPS.func = FTraceHookHandler;
hook->FTraceOPS.flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_IPMODIFY | FTRACE_OPS_FL_RECURSION;
err = ftrace_set_filter_ip(&hook->FTraceOPS, hook->SysCallEntry, 0, 0);
if (err)
{
pr_err("[FTraceHook] ftrace_set_filter_ip() failed: %dn", err);
return err;
}
err = register_ftrace_function(&hook->FTraceOPS);
if (err)
{
pr_err("[FTraceHook] register_ftrace_function() failed: %dn", err);
return err;
}
pr_info("[FTraceHook] Installed hook '%s': %dn", hook->Symbol, err);
return err;
}
int FTraceHookUninstall(struct FTraceHook *hook)
{
int err;
//注意与安装过程相反的顺序
err = unregister_ftrace_function(&hook->FTraceOPS);
if (err)
{
pr_err("[FTraceHook] unregister_ftrace_function() failed: %dn", err);
return err;
}
err = ftrace_set_filter_ip(&hook->FTraceOPS, hook->SysCallEntry, 1, 0);
if (err)
{
pr_err("[FTraceHook] ftrace_set_filter_ip() failed: %dn", err);
return err;
}
pr_info("[FTraceHook] Uninstalled hook '%s': %dn", hook->Symbol, err);
return err;
}
int FTraceHookInitialize(struct FTraceHook *hooks, size_t hooks_size)
{
int err = 0;
size_t i;
//随便找一个ret指令的地址,基本上就用当前函数尾部的ret就好;如果求稳(比如担心当前函数内存在复杂的跳转等),可以另外定义一个空函数,注意避免选取内联函数
RET_ADDRESS = (size_t)FTraceHookInitialize;
while (* (unsigned char *) RET_ADDRESS != RET_CODE)
++RET_ADDRESS;
//安装钩子
for (i = 0; i < hooks_size && !err; ++i)
err = FTraceHookInstall(hooks + i);
return err;
}
int FTraceHookFinalize(struct FTraceHook *hooks, size_t hooks_size)
{
int err = 0;
size_t i;
for (i = 0; i < hooks_size; ++i)
err = FTraceHookUninstall(hooks + i);
return err;
}
Entry.c:
MODULE_LICENSE("GPL");//使用ftrace的模块必须是GPL License,不然不能编译
MODULE_VERSION("0.01");
static int ReturnSwitch = 0;
static bool MySysExecve(struct FTraceHookContext *context)
{
char filename[256];//用kmalloc+kfree也是可以的,但栈容量允许的情况下,时间效率还是局部变量比较快
//输出第一个参数值
if (strncpy_from_user(filename, (char *)*context->Arguments[0], sizeof(filename)) >= 0)
pr_info("[FTraceHook] PID %u calling sys_execve: %sn", current->pid, filename);
//轮流测试两种方法来恢复原系统调用流程
if (++ReturnSwitch % 2)
{
//模拟经典方案的机制,代理调用原始函数
*context->ReturnValue = FTraceHookCallOriginal(context);
pr_info("[FTraceHook] execve() return: %ldn", *context->ReturnValue);
return false;//中止系统调用
}
else
{
//优化方案的新机制,不重新push参数而直接恢复原系统调用流程,但此时无法获取原系统调用流程的返回值
return true;
}
}
static struct FTraceHook GlobalHooks[] =
{
FTRACE_HOOK("sys_execve", MySysExecve)
};
static int __init Initialize(void)
{
int err = FTraceHookInitialize(GlobalHooks, ARRAY_SIZE(GlobalHooks));
if(err)
pr_err("[FTraceHook] Failed to initialize ftrace hooks...n");
return err;
}
static void __exit Finalize(void)
{
if(FTraceHookFinalize(GlobalHooks, ARRAY_SIZE(GlobalHooks)))
pr_err("[FTraceHook] Failed to finalize ftrace hooks...n");
}
module_init(Initialize);
module_exit(Finalize);
obj-m += FTraceHookExample.o
FTraceHookExample-objs := Entry.o FTraceHook.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
实际上,上面的代码也参考自经典方案[4][5][6]中的内容,但进行了一些优化,比较重要的部分包括:
-
由于Linux内核4.17版本前后的系统调用函数签名不同,经典方案中需要通过条件编译的方式为每个hook定义两个功能相同但签名不同的hook子程: -
如果hook子程本身逻辑简单、数量少倒还好,否则每个hook子程都要写两份,实际使用中非常不便; -
经典方案(至少前述三个参考链接实现)中大多没有考虑不同位宽/架构的寄存器差异: -
例如,hook子程中始终读取pt_regs的成员di作为第一个参数,这在x86_64架构下没有问题,但对x86_32(应为bx)、arm(应为uregs[0])等架构则是不适用的; -
但是,如果在每个hook子程中都进行条件编译,实际使用中也是非常不便的; -
经典方案通过在回调函数(ftrace_set_filter_ip的第二个参数)中修改原始系统调用的ip(x86架构的指令指针寄存器,不是网际协议,后文皆同不再另行说明)来使得执行流程跳转到hook子程,因此hook子程的函数签名必须与原始系统调用一致: -
这导致hook子程中需要系统调用参数之外的信息时逻辑变得很复杂,hook子程通常需要提前将一些必要的信息(例如,原始函数的真实地址等)通过另外的机制保存或传递,而无法封装框架统一提供; -
除此之外,对多个不同的系统调用使用同一个hook子程也会比较麻烦(因为不易确定原始系统调用函数的地址以进行代理,可能需要通过系统调用号重新查表等),尤其是对于业务上希望监控大量系统调用的场景; -
如果hook子程中需要调用原始函数,通常需要将调用参数重新入栈(Linux系统调用多有asmlinkage修饰,即全部参数通过堆栈传递而不使用fastcall),而不易让执行流程直接恢复到原始函数中。这将导致少许额外开销,尤其是对于4.17以前的内核版本。 -
修改ip的跳转方法导致经典方案中对hook子程的执行发生在ftrace相关函数返回之后(而非ftrace相关函数栈内),因此ftrace自带的防递归功能无法作用于经典方案。为此,经典方案中自行实现了两套防递归方法,但它们看上去都不是非常完善: -
第一种方法通过within_module检查直接调用方是否位于当前模块中,这对于涉及多个模块的调用(hook子程位于模块A中,A调用了模块B的函数,而模块B尝试调用被hook的原始函数)可能是不完善的; -
第二种方法在执行递归调用时跳过系统调用开头的“空白区”,这意味着需要对于所有调用原始函数的代码进行修改。这不仅实现起来比较麻烦,而且同样面临多个模块间调用时可能不完善的问题(因为难以修改其它模块对原始函数的调用); -
(实现细节)关于FTRACE_OPS_FL_RECURSION标志的使用可能有误 -
ftrace框架的防递归选项在内核版本5.11前后发生了变化: -
在5.10.113及以前版本中,默认会添加防递归检查,除非ftrace_ops.flags设置了FTRACE_OPS_FL_RECURSION_SAFE标志; -
而从5.11rc1开始,默认不会添加防递归检查,除非ftrace_ops.flags设置了FTRACE_OPS_FL_RECURSION标志; -
因此,这实际上是两个功能相反的标志选项。第一个经典方案[4]中“#define FTRACE_OPS_FL_RECURSIONFTRACE_OPS_FL_RECURSION_SAFE”的写法可能是不正确的,而第二个经典方案[5]没有对这个标志进行版本差异处理; -
不过,因为经典方案并不需要ftrace框架提供防递归检查,所以这个错误应该不会造成什么实质上的影响。
参考文献
-
KERNELNEWBIES.ORG. Linux kernel 2.6.27 [J/OL]2008,
https://kernelnewbies.org/Linux_2_6_27.
-
ROSTEDT S. ftrace - Function Tracer [J/OL]2008,
https://www.kernel.org/doc/Documentation/trace/ftrace.txt.
-
YEMELIANOV A. Kernel Tracing with Ftrace[J/OL] 2017,
https://blog.selectel.com/kernel-tracing-ftrace/.
-
ALEXEY LOZOVSKY S S. Hooking Linux KernelFunctions, Part 2: How to Hook Functions with FtraceIt [J/OL] 2018,
https://www.apriorit.com/dev-blog/546-hooking-linux-functions-2.
-
PHILLIPS H. Linux Rootkits Part 2: Ftrace and Function Hooking [J/OL] 2020,
https://xcellerator.github.io/posts/linux_rootkits_02/.
-
OLEKSII LOZOVSKYI M G, KRZYSZTOF ZDULSKI.ftrace-hook [J/OL] 2021,
https://github.com/ilammy/ftrace-hook/.
本公众号原创文章仅代表作者观点,不代表绿盟科技立场。所有原创内容版权均属绿盟科技研究通讯。未经授权,严禁任何媒体以及微信公众号复制、转载、摘编或以其他方式使用,转载须注明来自绿盟科技研究通讯并附上本文链接。
关于我们
绿盟科技研究通讯由绿盟科技创新中心负责运营,绿盟科技创新中心是绿盟科技的前沿技术研究部门。包括云安全实验室、安全大数据分析实验室和物联网安全实验室。团队成员由来自清华、北大、哈工大、中科院、北邮等多所重点院校的博士和硕士组成。
绿盟科技创新中心作为“中关村科技园区海淀园博士后工作站分站”的重要培养单位之一,与清华大学进行博士后联合培养,科研成果已涵盖各类国家课题项目、国家专利、国家标准、高水平学术论文、出版专业书籍等。
我们持续探索信息安全领域的前沿学术方向,从实践出发,结合公司资源和先进技术,实现概念级的原型系统,进而交付产品线孵化产品并创造巨大的经济价值。
长按上方二维码,即可关注我
原文始发于微信公众号(绿盟科技研究通讯):Linux内核跟踪:ftrace hook入门手册(上)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论