linux hook方法整理
在计算机中,基本所有的软件程序都可以通过hook方式进行行为拦截,hook方式就是改变原始的执行流,下面简要分类linux系统下的各种hook方式,主要有三类:修改函数指针,直接修改指令,利用系统提供的注册机制.
函数指针hook
C语言的一项强大的功能就是指针,指针代表一个地址,而函数指针就是指向一个函数地址的指针,通过函数指针来指向不同的函数地址控制执行流.
一般这类函数指针存在于软件运行的整个周期中,要实施这类hook首先就是找到关键的函数指针,之后就和普通的指针修改一样进行改变就OK.
函数指针的使用形式:
//函数定义
func() {
....
}
//函数指针
fun = func
//函数指针调用
fun()
通过修改函数指针进行hook:
origin_fun = fun; //保存原始函数指针
f = hook_func; //函数指针指向hook函数
hook_func() {
...
origin_fun(); //一般都会再次调用原始函数来完成实际功能
}
syscall table hook
在linux内核中,比较常见并且关键的就是系统调用表,系统调用表实际上是指针数组,下标是系统调用号,应用层发起的所有活动基本上都绕不过系统调用,控制了系统调用表基本上就是控制了整个系统.
hook只需要两个信息:系统调用表的起始地址和系统调用号,之后就能像修改指针一样进行劫持.
如何获取符号的地址参见另一篇linux内核查找符号
system_call_table_addr = (void*)0xffffffff81e001e0;
//return the system call to its original state
original_open = system_call_table_addr[__NR_open];
//Disable page protection
make_rw((unsigned long)system_call_table_addr);
system_call_table_addr[__NR_open] = open_hijack;
security_ops hook
和syscall table一样,security_ops也是函数指针数组,里面是所有的security提供的hook点,一般会有一些默认的LSM module(例如selinux,apparmor, tomoyo等)注册,即security_ops中的函数指针指向某个module提供的函数地址.
和hook syscall table指针一样,也需要两个信息:security_ops指向的地址和要hook的函数地址,前一个可以通过kallsyms_on_each_symbol找到对应的地址,而偏移根据security_operations结构体获取.
struct ksym {
char *name;
unsigned long addr;
};
unsigned long get_symbol ( char *name )
{
unsigned long symbol = 0;
struct ksym ksym;
ksym.name = name;
ksym.addr = 0;
kallsyms_on_each_symbol(&find_ksym, &ksym);
symbol = ksym.addr;
return symbol;
}
int hijack(void *func)
{
security_ops_ptr = get_symbol(security_ops);
target_addr = *security_ops_ptr + &(((struct security_operations *)0)->filp_open)
origin_open = target_addr
target_addr = func
}
指令hook
这种方式就是通过修改函数二进制指令来控制执行流,在x86上最常见的就是jmp指令.通常这种方式的修改做法有以下几种:
1. 将被损坏的指令拷贝出来,在需要回调旧函数时,先将指令恢复回去,再调用旧函数。
//典型的做法是kprobe插入的int3指令
2. 将被损坏的指令拷贝到另一个地方,并在末尾加上跳转指令转回旧函数体中相应的位置。
//典型的实现是khook
3. 将整个旧函数拷贝一份,并修复其中的跳转指令。
//典型的做法tpe-lkm,gohook
kprobe
参考:https://blog.csdn.net/faxiang1230/article/details/99328003
splice
参考:https://blog.csdn.net/faxiang1230/article/details/94459773
gohook
参考:https://www.cnblogs.com/catch/p/10973611.html
利用系统注册机制
系统也会提供一些注册机制,可以满足一些定制需求,可以利用这些基础设施实现hook功能.
LSM module
LSM(linux security module)是linux内核提供的一套框架,内核本身在大部分关键点路径下进行预先埋点,这样就能实现安全能力的全面覆盖;并且暴露出一套安全模块注册接口,允许内核模块注册为安全模块或者注销.在早期的内核中,"it’s impossible to have multiple LSMs even in theory"不允许多个安全module同时注册到LSM中,不过在后面2011年左右的探讨中允许多个module注册(https://lwn.net/Articles/426921/).目前主流的module有:selinux, apparmor,tomoyo.
2015年Casey Schaufler在4.2内核版本上支持了多个LSM module功能,通过链式方式进行管理.目前没有优先级概念,多个hook中其中一个返回失败就是失败.而且lsm的module只能内编进内核中,它提供的注册接口security_add_hooks,但是没有导出这个符号,这样使得以module形式存在的安全模块无法注册.
堆栈式文件系统
linux内核中提供了一个加密文件系统ecryptfs实现了数据加密功能,它通过register_filesystem方式注册了一个加密文件系统,并且支持mount,在用户mount时要求提供密码口令作为秘钥进行加密.
文件系统有几个主要对象:mount, superblock, dentry, inode等,在用户mount时,修改VFS的对象指向ecryptfs的对象,对于VFS而言,它看到的就是ecryptfs,和ext4这种物理文件系统是等同的;并且在mount时获取底层物理文件系统信息,实际对磁盘的操作都重定向到真实的物理文件系统上.具体更多的参考:https://www.linuxjournal.com/article/9400
还有很多以Module形式存在的堆栈文件系统:redirfs,cryptfs等,原理都是相似的.
堆栈类文件系统集散地:
https://www.filesystems.org/
netfilter
linux内核提供了netfilter的注册机制,允许自己对现有的netfilter进行扩充,例如实现防火墙,流控,负载均衡等功能.
static struct nf_hook_ops myhook_ops __read_mostly = {
.pf = NFPROTO_IPV6,
.priority = 1,
.hooknum = NF_INET_LOCAL_OUT,
.hookfn = myhook_fn,
};
static int __init myhook_init(void)
{
return nf_register_hook(&myhook_ops);
}
static void __exit myhook_exit(void)
{
nf_unregister_hook(&myhook_ops);
}
module_init(myhook_init);
module_exit(myhook_exit);
malloc_hook
glibc提供了malloc hook的注册接口,允许用户程序进行hook malloc/realloc/free的分配器接口,通过hook函数进行统计或者拦截工作.
/* Prototypes for our hooks. */
static void my_init_hook(void);
static void *my_malloc_hook(size_t, const void *);
/* Variables to save original hooks. */
static void *(*old_malloc_hook)(size_t, const void *);
/* Override initializing hook from the C library. */
void (*__malloc_initialize_hook) (void) = my_init_hook;
static void
my_init_hook(void)
{
old_malloc_hook = __malloc_hook;
__malloc_hook = my_malloc_hook;
}
static void *
my_malloc_hook(size_t size, const void *caller)
{
void *result;
/* Restore all old hooks */
__malloc_hook = old_malloc_hook;
/* Call recursively */
result = malloc(size);
/* Save underlying hooks */
old_malloc_hook = __malloc_hook;
/* printf() might call malloc(), so protect it too. */
printf("malloc(%u) called from %p returns %p\n",
(unsigned int) size, caller, result);
/* Restore our own hooks */
__malloc_hook = my_malloc_hook;
return result;
}
总结
系统提供的注册接口基本都是针对某一方面(LSM中埋点比较全面),netfilter只能审计网络流,redirfs只审计文件访问,所以大部分时候需要结合几种注册接口来完成实际需求,例如内核的smack就使用了两种基础设施:LSM和netfilter.
修改指令的方式也有自己的隐患,一个是性能的下降,另一个是hook module卸载的时候非常容易发生问题.
- 目前没有一个非常完美的解决方案来解决卸载的问题.以kprobe为例,优化的kprobe方式需要修改probe点的5个字节指令(jmp xxx),在hook和恢复的时候需要原子性修改指一条指令,但是并没有原子性修改5字节的指令.
- 在恢复hook的时候需要使用"引用计数",因为这个时候有可能有其他的进程是通过被我们劫持后的hook_function流程进入内核原始系统调用的,这些系统调用例如sys_socketcall的select动作,是一个阻塞型的系统调用,用户态会一直阻塞等待这次系统调用的返回,如果我们不等到引用计数降到0(即没人在使用)之后,而是采取直接卸载模块,会导致那些系统调用返回后,回到一个被释放掉的内核内存区域中
//使用"引用计数"会带来另一个问题,系统调用中有一些例如socket select这种阻塞性的系统调用,从用户态发起系统调用到最后从内核态返回会经历一个很长的时间,此时模块的引用计数会一直处于大于零的状态,而无法卸载
相关资料:
https://www.cnblogs.com/LittleHann/p/3854977.html?utm_source=tuicool&utm_medium=referral#_label0
https://blog.csdn.net/faxiang1230/article/details/105501718
https://www.filesystems.org/
更多推荐
所有评论(0)