...
在学习hook之前需要对Linux的动态链接有一定的了解,建议阅读《程序员的自我修养 —— 链接、装载与库》第7章。本站 关于链接与装载的几个测试代码 提供了一些示例,有助于理解动态链接的具体行为。
hook要实现的目地非常简单,就是用自定义的接口来替换掉原始的系统调用接口,比如用自定义的write函数来替换掉系统提供的write函数来进行数据发送。hook实现的功能非常简单,就是通过动态库的全局符号介入机制,用自定义的接口来替换掉原始的系统调用接口,比如用自定义的write函数来替换掉系统提供的write函数来进行数据发送。由于系统调用接口基本上是由C标准函数库libc提供的,所以这里要做的事情就是用自定义的动态库来覆盖掉libc中的同名符号。
基于动态链接的hook有两种方式,第一种是外挂式hook,也称为非侵入式hook,通过优先加载动态库来实现对后加载的动态库进行hook,这种hook方式不需要重新编译代码,考虑以下例子:
代码块 |
---|
#include <unistd.h> #include <string.h> int main() { write(STDOUT_FILENO, "hello world\n", strlen("hello world\n")); // 调用系统调用write写标准输出文件描述符 return 0; } |
在这个例子中,可执行程序调用write向标准输出文件描述符写数据。对这个程序进行编译和执行,预期效果如下:在这个例子中,可执行程序调用write向标准输出文件描述符写数据。对这个程序进行编译和执行,效果如下:
代码块 |
---|
# gcc main.c # ./a.out hello world |
使用ldd命令查看可执行程序的依赖的共享库,如下:
代码块 |
---|
# ldd a.out
linux-vdso.so.1 (0x00007ffc96519000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fda40a61000)
/lib64/ld-linux-x86-64.so.2 (0x00007fda40c62000) |
可以看到其依赖libc共享库,write系统调用就是由libc提供的。
gcc编译生成可执行文件时会默认链接libc库,所以不需要显式指定链接参数,这点可以在编译时给 gcc 增加一个 "-v" 参数,将整个编译流程详细地打印出来进行验证,如下:
代码块 |
---|
# gcc -v main.c
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:hsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
...
/usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/ccZQ60eg.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/9 -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/9/../../.. /tmp/ccnT2NOd.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/9/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crtn.o
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64' |
注意上面的 "/usr/lib/gcc/x86_64-linux-gnu/9/collect2 ... -pop-state -lc -lgcc ..."
,这里的 -lc
就说明程序在进行链接时会自动链接一次libc。
下面在不重新编译代码的情况下,用自定义的动态库来替换掉可执行程序a下面在不重新编译代码的情况下,用自定义的write函数来替换掉可执行程序a.out中的write实现,新建hook.c,内容如下:
代码块 |
---|
#include <unistd.h> #include <sys/syscall.h> #include <string.h> ssize_t write(int fd, const void *buf, size_t count) { syscall(SYS_write, STDOUT_FILENO, "12345\n", strlen("12345\n")); } |
将hook这里实现了一个write函数,这个函数的签名和libc提供的write函数完全一样,函数体是用syscall的方式直接调用内核提供的系调用编号为SYS_write的系统调用,实现的效果也是往标准输出写内容,只不过这里我们将输出内容替换成了其他值。将hook.c编译成动态库:
代码块 |
---|
gcc -fPIC -shared hook.c -o libhook.so |
通过设置 LD_PRELOAD
环境变量,将libhoook.so设置成优先加载,则lib
进行hookPRE_LOAD=“a.so” ./mainreadelf -d a.out
侵入式hook,需要改造代码
...
so设置成优先加载,从面覆盖掉libc中的write函数,如下:
代码块 |
---|
# LD_PRELOAD="./libhook.so" ./a.out
12345 |
这里我们并没有重新编译可执行程序a.out,但是可以看到,write的实现已经替换成了我们自己的实现,究其原因,就是LD_PRELOAD环境变量,它在运行a.out之前,优先把libhook.so加载到了程序的进程空间,使得在a.out运行之前,其全局符号表中就已经有了一个write符号,这样在后续加载libc共享库时,由于全局符号介入机制,libc中的write符号不会再被加入全局符号表,所以全局符号表中的write就变成了我们自己的实现。
第二种方式的hook是侵入式的,需要重新编译代码,实现方式很简单,只需要在编译时将自定义的动态库放在libc之前进行链接就可以了。由于默认情况下gcc总会链接一次libc,并且libc的位置也总在命令行所有参数后面,所以只需要像下面这样操作就可以了:
代码块 |
---|
# gcc main.c -L. -lhook -Wl,-rpath=.
# ./a.out
12345 |
这里显式指定了链接libhook.so(-Wl,-rpath=.
用于指定运行时的动态库搜索路径,避免找不到动态库的问题),由于libhook.so的链接位置靠前,所以运行时会先加载libhook.so,这点也可以通过ldd命令来查看:
代码块 |
---|
# ldd a.out linux-vdso.so.1 (0x00007ffe615f9000) libhook.so => ./libhook.so (0x00007fab4bae3000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so |
...
.6 (0x00007fab4b8e9000)
/lib64/ld-linux-x86-64.so.2 (0x00007fab4baef000) |
sylar hook模块设计
sylar对以下函数进行了hook,并且只对socket fd进行了hook,如果操作的不是socket fd,那会直接调用系统原本的API,而不是hook之后的API:sleepusleepnanosleepsocketconnectacceptreadreadvrecvrecvfromrecvmsgwritewritevsendsendtosendmsgclosefcntlioctlgetsockoptsetsockopt
...