C++使用LD_PRELOAD劫持(Hook)库函数

在调试C++程序的时候,我们并不是每次都能拿到源代码,很多时候我们只能得到一个动态库so,调试时这个动态库就是一个黑匣子,没办法查看修改或者在里面加日志,那么我们是不是就没有任何办法对我们感兴趣的函数和参数进行监控和跟踪了呢?

对于这种情况,我们一般会挂上gdb,然后在我们感兴趣的地方打上断点,然后查看堆栈里的变量的值。但这个过程时比较繁琐的,尤其是在需要了解程序执行的大量中间过程时是非常让人抓狂的。下面我们将介绍一种使用钩子函数的方法,来修改目标函数的运行时的行为,来达到我们跟踪函数运行的目的。

钩子函数可以在运行时劫持预先存在的函数,我们可以在钩子函数里对预先的函数做一些包装,使得函数保持原来的功能的前提下做一些额外的操作。在本文中我们主要的是linux系统的动态加载API,动态加载允许在运行时加载并运行动态链接库里的函数,所以我们可以把钩子函数打包成动态链接库,以实现对现有函数的劫持。实现这个功能需要用到LD_PRELOAD环境变量,使用LD_PRELOAD加载的动态库会最先被加载,这就使得我们有机会可以在钩子函数里运用动态加载技术将原先的函数绑定到钩子函数中,从而达到监控及跟踪的效果。

下面就以最简单的hello world的例子来讲解这一切是怎么做到的。
首先main函数,helloworld.c

1
2
3
4
5
6
#include <stdio.h>
#include <unistd.h>
int main() {
puts("Hello world!n");
return 0;
}

再是钩子函数,example.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
int puts(const char *message) {
int (*new_puts)(const char *message);
int result;
new_puts = dlsym(RTLD_NEXT, "puts");
if(strcmp(message, "Hello world!n") == 0) {
result = new_puts("Goodbye, cruel world!n");
} else {
result = new_puts(message);
}
return result;
}

在这个样例里,我们劫持了标准库里的puts函数。简单看一下代码。
第四行我们定义了一个和标准库里有一模一样签名的puts函数,这是能劫持puts函数的关键,钩子函数的签名一定要和原函数一模一样。
第五行我们又声明了一个和原函数参数一样的函数指针,这个指针用来后面指向原函数。
第九行利用dlsym函数获取原函数的指针并赋给上面声明的指针,RTLD_NEXT这个枚举变量告诉动态加载器加载第二个参数(在本例中为puts)相关联的函数的下一个实例也就是原函数里的puts
后面的if判断里我们比较传进来的参数,然后输出不同的结果,从而在运行时改变程序的行为。

编译运行看一下

1
2
kingway@ubuntu:~/code$ gcc helloworld.c -o helloworld
kingway@ubuntu:~/code$ gcc example.c -o libexample.so -fPIC -shared -ldl -D_GNU_SOURCE

helloworld.c正常编译成可执行文件就行,example.c我们需要编译成位置无关的动态链接库,-ldl -D_GNU_SOURCE是必须的,这样我们才能使用动态加载API,并且访问RTLD_NEXT枚举变量。

看一下运行的结果

1
2
3
kingway@ubuntu:~/code$ export LD_PRELOAD="/home/kingway/code/libexample.so"
kingway@ubuntu:~/code$ ./helloworld
Goodbye, cruel world!

可以看到Hello world成功被劫持,并输出了钩子函数里的结果。这只是一个简单的例子,但通过这种方法,我们可以轻松的劫持黑匣子动态链接库的函数,并在必要时进行一些修改,达到调试的目的。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

小小鼓励一下~

支付宝
微信