Linux System Calls Hooking Method Summary

1. 相关学习资料

http://xiaonieblog.com/?post=121
http://hbprotoss.github.io/posts/li-yong-ld_preloadjin-xing-hook.html
http://www.catonmat.net/blog/simple-ld-preload-tutorial/
http://os.51cto.com/art/201004/195510.htm
http://sebug.net/paper/pst_WebZine/pst_WebZine_0x03/html/%5BPSTZine%200x03%5D%5B0x03%5D%5B%E9%AB%98%E7%BA%A7Linux%20Kernel%20Inline%20Hook%E6%8A%80%E6%9C%AF%E5%88%86%E6%9E%90%E4%B8%8E%E5%AE%9E%E7%8E%B0%5D.html
http://blog.chinaunix.net/uid-26310563-id-3175021.html
http://laokaddk.blog.51cto.com/368606/d-26/p-2
http://m.blog.csdn.net/blog/panfengyun12345/19480567
https://www.kernel.org/doc/Documentation/kprobes.txt
http://blog.chinaunix.net/uid-23769728-id-3198044.html
https://sourceware.org/systemtap/
http://alanwu.blog.51cto.com/3652632/1111213
http://laokaddk.blog.51cto.com/368606/421862
http://baike.baidu.com/view/336501.htm
http://blog.csdn.net/dog250/article/details/6451762
http://blog.csdn.net/sanbailiushiliuye/article/details/7552359

 

2. 系统调用Hook简介

系统调用(syscall)是一个通用的概念,它既包括应用层系统函数库的调用,也包括ring0层系统提供的syscall_table提供的系统api。

我们必须要明白,Hook技术是一个相对较宽的话题,因为操作系统从ring3到ring0是分层次的结构,在每一个层次上都可以进行相应的Hook,它们使用的技术方法以及取得的效果也是不尽相同的。本文的主题是"系统调用的Hook学习","系统调用的Hook"是我们的目的,而要实现这个目的可以有很多方法,本文试图尽量覆盖从ring3到ring0中所涉及到的Hook技术,来实现系统调用的监控功能。

 

3. Ring3中Hook技术

0x1: LD_PRELOAD动态连接.so函数劫持

LD_PRELOAD hook技术属于so依赖劫持技术的一种实现,所以要讨论这种技术的技术原理,我们先来看一下linux操作系统加载so的底层原理。

括Linux系统在内的很多开源系统都是基于Glibc的,动态链接的ELF可执行文件在启动时同时会启动动态链接器(/lib/ld-linux.so.X),程序所依赖的共享对象全部由动态链接器负责装载和初始化,所以这里所谓的共享库的查找过程,本质上就是动态链接器(/lib/ld-linux.so.X)对共享库路径的搜索过程,搜索过程如下:

  • /etc/ld.so.cache:Linux为了加速LD_PRELOAD的搜索过程,在系统中建立了一个ldconfig程序,这个程序负责
    • 将共享库下的各个共享库维护一个SO-NAME(一一对应的符号链接),这样每个共享库的SO-NAME就能够指向正确的共享库文件
    • 将全部SO-NAME收集起来,集中放到/etc/ld.so.cache文件里面,并建立一个SO-NAME的缓存
    • 当动态链接器要查找共享库时,它可以直接从/etc/ld.so.cache里面查找。所以,如果我们在系统指定的共享库目录下添加、删除或更新任何一个共享库,或者我们更改了/etc/ld.so.conf、/etc/ld.preload的配置,都应该运行一次ldconfig这个程序,以便更新SO-NAME和/etc/ld.so.cache。很多软件包的安装程序在结束共享库安装以后都会调用ldconfig
  • 根据/etc/ld.so.preload中的配置进行搜索(LD_PRELOAD):这个配置文件中保存了需要搜索的共享库路径,Linux动态共享库加载器根据顺序进行逐行广度搜索
  • 根据环境变量LD_LIBRARY_PATH指定的动态库搜索路径
  • 根据ELF文件中的配置信息:任何一个动态链接的模块所依赖的模块路径保存在".dynamic"段中,由DT_NEED类型的项表示,动态链接器会按照这个路径去查找DT_RPATH所指定的路径,编译目标代码时,可以对gcc加入链接参数"-Wl,-rpath"指定动态库搜索路径。
    • DT_NEED段中保存的是绝对路径,则动态链接器直接按照这个路径进行直接加载
    • DT_NEED段中保存的是相对路径,动态链接器会在按照一个约定的顺序进行库文件查找下列路径
      • /lib
      • /usr/lib
      • /etc/ld.so.conf中配置指定的搜索路径

可以看到,LD_PRELOAD是Linux系统中启动新进程首先要加载so的搜索路径,所以它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前"优先加载"的动态链接库。

我们只要在通过LD_PRELOAD加载的.so中编写我们需要hook的同名函数,根据Linux对外部动态共享库的符号引入全局符号表的处理,后引入的符号会被省略,即系统原始的.so(/lib64/libc.so.6)中的符号会被省略。

通过strace program也可以看到,Linux是优先加载LD_PRELOAD指明的.so,然后再加载系统默认的.so的:

1. 通过自写.so文件劫持LD_PRELOAD

1)demo例子

正常程序main.c:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    if( strcmp(argv[1], "test") )
    {
        printf("Incorrect password\n");
    }
    else
    {
        printf("Correct password\n");
    }
    return 0;
}

用于劫持函数的.so代码hook.c

#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
/*
hook的目标是strcmp,所以typedef了一个STRCMP函数指针
hook的目的是要控制函数行为,从原库libc.so.6中拿到strcmp指针,保存成old_strcmp以备调用
*/
typedef int(*STRCMP)(const char*, const char*);

int strcmp(const char *s1, const char *s2)
{
    static void *handle = NULL;
    static STRCMP old_strcmp = NULL;

    if( !handle )
    {
        handle = dlopen("libc.so.6", RTLD_LAZY);
        old_strcmp = (STRCMP)dlsym(handle, "strcmp");
    }
    printf("oops!!! hack function invoked. s1=<%s> s2=<%s>\n", s1, s2);
    return old_strcmp(s1, s2);
}

编译:

gcc -o test main.c
gcc -fPIC -shared -o hook.so hook.c -ldl

运行:

LD_PRELOAD=./hook.so ./test 123

2)hook function注意事项

在编写用于function hook的.so文件的时候,要考虑以下几个因素

1. Hook函数的覆盖完备性
对于Linux下的指令执行来说,有7个Glibc API都可是实现指令执行功能,对这些API对要进行Hook
/*
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
http://www.2cto.com/os/201410/342362.html
*/

2. 当前系统中存在function hook的重名覆盖问题
    1) /etc/ld.so.preload中填写了多条.so加载条目
    2) 其他程序通过"export LD_PRELOAD=.."临时指定了待加载so的路径
在很多情况下,出于系统管理或者集群系统日志收集的目的,运维人员会向系统中注入.so文件,对特定function函数进行hook,这个时候,当我们注入的.so文件中的hook function和原有的hook function存在同名的情况,Linux会自动忽略之后载入了hook function,这种情况我们称之为"共享对象全局符号介入"

3. 注入.so对特定function函数进行hook要保持原始业务的兼容性
典型的hook的做法应该是
hook_function()
{
    save ori_function_address;
    /*
    do something in here
    span some time delay
    */
    call ori_function;
}
hook函数在执行完自己的逻辑后,应该要及时调用被hook前的"原始函数",保持对原有业务逻辑的透明

4. 尽量减小hook函数对原有调用逻辑的延时
hook_function()
{
    save ori_function_address;
    /*
    do something in here
    span some time delay
    */
    call ori_function;
}
hook这个操作是一定会对原有的代码调用执行逻辑产生延时的,我们需要尽量减少从函数入口到"call ori_function"这块的代码逻辑,让代码逻辑尽可能早的去"call ori_function"
在一些极端特殊的场景下,存在对单次API调用延时极其严格的情况,如果延时过长可能会导致原始业务逻辑代码执行失败

如果需要不仅仅是替换掉原有库函数,而且还希望最终将函数逻辑传递到原有系统函数,实现透明hook(完成业务逻辑的同时不影响正常的系统行为)、维持调用链,那么需要用到RTLD_NEXT

当调用dlsym的时候传入RTLD_NEXT参数,gcc的共享库加载器会按照"装载顺序(load order)(即先来后到的顺序)"获取"下一个共享库"中的符号地址
/*
Specifies the next object after this one that defines name. This one refers to the object containing the invocation of dlsym(). The next object is the one found upon the application of a load order symbol resolution algorithm (see dlopen()). The next object is either one of global scope (because it was introduced as part of the original process image or because it was added with a dlopen() operation including the RTLD_GLOBAL flag), or is an object that was included in the same dlopen() operation that loaded this one.
The RTLD_NEXT flag is useful to navigate an intentionally created hierarchy of multiply-defined symbols created through interposition. For example, if a program wished to create an implementation of malloc() that embedded some statistics gathering about memory allocations, such an implementation could use the real malloc() definition to perform the memory allocation-and itself only embed the necessary logic to implement the statistics gathering function.
http://pubs.opengroup.org/onlinepubs/009695399/functions/dlsym.html
http://www.newsmth.net/nForum/#!article/KernelTech/413
*/

code example

// used for getting the orginal exported function address
#if defined(RTLD_NEXT)
#  define REAL_LIBC RTLD_NEXT
#else
#  define REAL_LIBC ((void *) -1L)
#endif

//REAL_LIBC代表当前调用链中紧接着下一个共享库,从调用方链接映射列表中的下一个关联目标文件获取符号
#define FN(ptr,type,name,args)  ptr = (type (*)args)dlsym (REAL_LIBC, name)

...
FN(func,int,"execve",(const char *, char **const, char **const));

我们知道,如果当前进程空间中已经存在某个同名的符号,则后载入的so的同名函数符号会被忽略,但是不影响so的载入,先后载入的so会形成一个链式的依赖关系,通过RTLD_NEXT可以遍历这个链

3)SO功能代码编写

这个小节我们来完成一个基本的进程、网络、模块加载监控的小demo。

1. 指令执行
    1) execve
    2) execv
2. 网络连接
    1) connect
3. LKM模块加载
    1) init_module

hook.c

#include <stdio.h>
#include <string.h>
#include <dlfcn.h>

#include <stdlib.h>
#include <sys/types.h>  
#include <string.h>
#include <unistd.h>
#include <limits.h>

#include <netinet/in.h> 
#include <linux/ip.h>
#include <linux/tcp.h>
 
#if defined(RTLD_NEXT)
#  define REAL_LIBC RTLD_NEXT
#else
#  define REAL_LIBC ((void *) -1L)
#endif

#define FN(ptr, type, name, args)  ptr = (type (*)args)dlsym (REAL_LIBC, name)
 
int execve(const char *filename, char *const argv[], char *const envp[])
{
    static int (*func)(const char *, char **, char **);
    FN(func,int,"execve",(const char *, char **const, char **const)); 

    //print the log
    printf("filename: %s, argv[0]: %s, envp:%s\n", filename, argv[0], envp);

    return (*func) (filename, (char**) argv, (char **) envp);
} 

int execv(const char *filename, char *const argv[]) 
{
    static int (*func)(const char *, char **);
    FN(func,int,"execv", (const char *, char **const)); 

    //print the log
    printf("filename: %s, argv[0]: %s\n", filename, argv[0]);

    return (*func) (filename, (char **) argv);
}  
  
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 
{ 
    static int (*func)(int, const struct sockaddr *, socklen_t);
    FN(func,int,"connect", (int, const struct sockaddr *, socklen_t)); 

    /*
    print the log
    获取、打印参数信息的时候需要注意
    1. 加锁
    2. 拷贝到本地栈区变量中
    3. 然后再打印
    调试的时候发现直接获取打印会导致core dump
    */
    printf("socket connect hooked!!\n");

    //return (*func) (sockfd, (const struct sockaddr *) addr, (socklen_t)addrlen);
    return (*func) (sockfd, addr, addrlen);
}  

int init_module(void *module_image, unsigned long len, const char *param_values) 
{ 
    static int (*func)(void *, unsigned long, const char *);
    FN(func,int,"init_module",(void *, unsigned long, const char *)); 

    /*
    print the log
    lkm的加载不需要取参数,只需要捕获事件本身即可
    */
    printf("lkm load hooked!!\n");

    return (*func) ((void *)module_image, (unsigned long)len, (const char *)param_values);
} 

编译,并装载

//编译出一个so文件
gcc -fPIC -shared -o hook.so hook.c -ldl

添加LD_PRELOAD有很多种方式

1. 临时一次性添加(当条指令有效)
LD_PRELOAD=./hook.so nc www.baidu.com 80  
/*
LD_PRELOAD后面接的是具体的库文件全路径,可以连接多个路径
程序加载时,LD_PRELOAD加载路径优先级高于/etc/ld.so.preload
*/

2. 添加到环境变量LD_PRELOAD中(当前会话SESSION有效)
export LD_PRELOAD=/zhenghan/snoopylog/hook.so
//"/zhenghan/snoopylog/"是编译.so文件的目录
unset LD_PRELOAD

3. 添加到环境变量LD_LIBRARY_PATH中
假如现在需要在已有的环境变量上添加新的路径名,则采用如下方式
LD_LIBRARY_PATH=/zhenghan/snoopylog/hook.so:$LD_LIBRARY_PATH.(newdirs是新的路径串)
/*
LD_LIBRARY_PATH指定查找路径,这个路径优先级别高于系统预设的路径
*/

4. 添加到系统配置文件中
vim /etc/ld.so.preload
add /zhenghan/snoopylog/hook.so

5. 添加到配置文件目录中
cat /etc/ld.so.conf
//include ld.so.conf.d/*.conf

效果测试

1. 指令执行
在代码中手动调用: execve(argv[1], newargv, newenviron);

2. 网络连接
执行: nc www.baidu.com 80

3. LKM模块加载
编写测试LKM模块,执行: insmod hello.ko

在真实的环境中,socket的网络连接存在大量的连接失败,非阻塞等待等等情况,这些都会触发connect的hook调用,对于connect的hook来说,我们需要对以下的事情进行过滤

1. 区分IPv4、IPv6
根据connect参数中的(struct sockaddr *addr)->sa_family进行判断

2. 区分执行成功、执行失败
如果本次connect调用执行失败,则不应该继续进行参数获取
int ret_code = (*func) (sockfd, addr, addrlen);
int tmp_errno = errno;
if (ret_code == -1 && tmp_errno != EINPROGRESS)
{
    return ret_code;
}

3. 区分TCP、UDP连接
对于TCP和UDP来说,它们都可以发起connect请求,我们需要从中过滤出TCP Connect请求
#include <sys/types.h>
#include <sys/socket.h>

int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
/*
#include <sys/types.h>
#include <sys/socket.h>
main()
{
   int s;
   int optval;
   int optlen = sizeof(int);
   if((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)
   perror("socket");
   getsockopt(s, SOL_SOCKET, SO_TYPE, &optval, &optlen);
   printf("optval = %d\n", optval);
   close(s);
}
*/
执行:
optval = 1 //SOCK_STREAM 的定义正是此值

Relevant Link:

http://m.blog.csdn.net/blog/cdhql/42081029
http://blog.csdn.net/xioahw/article/details/4056514
http://c.biancheng.net/cpp/html/357.html
http://m.blog.csdn.net/blog/cdhql/42081029

4)劫持效果测试

1. 指令执行监控

execve.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
   char *newargv[] = { NULL, "hello", "world", NULL };
   char *newenviron[] = { NULL };

   if (argc != 2) 
   {
       fprintf(stderr, "Usage: %s <file-to-exec>\n", argv[0]);
       exit(EXIT_FAILURE);
   }

   newargv[0] = argv[1];

   execve(argv[1], newargv, newenviron);
   perror("execve");   /* execve() only returns on error */
   exit(EXIT_FAILURE);
}
//gcc -o execve execve.c

myecho.c

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int j;

    for (j = 0; j < argc; j++)
        printf("argv[%d]: %s\n", j, argv[j]);

    exit(EXIT_SUCCESS);
}
//gcc -o myecho myecho.c