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
ecryptfs
还有很多以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卸载的时候非常容易发生问题.

  1. 目前没有一个非常完美的解决方案来解决卸载的问题.以kprobe为例,优化的kprobe方式需要修改probe点的5个字节指令(jmp xxx),在hook和恢复的时候需要原子性修改指一条指令,但是并没有原子性修改5字节的指令.
  2. 在恢复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/
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页