我们有这样一段简单的代码,用来输出 10 个[0, 100)的随机数:

 1// random.c
 2#include <stdio.h>
 3#include <stdlib.h>
 4#include <time.h>
 5
 6int main() {
 7    srand(time(NULL));
 8    int i = 10;
 9    while (i--) printf("%d\n", rand() % 100);
10    return 0;
11}

编译运行:

 1liubang@venux-dev:~$ gcc random.c -o random
 2liubang@venux-dev:~$ ./random
 344
 446
 597
 651
 762
 876
 992
1076
1138
1210

这个程序每次运行的结果都是不一样的,现在我们希望我们能够在不修改源码的情况下,控制程序的输出结果,例如我希望这段程序运行的结果是 每次都能输出 10 个 10。由于不能修改源码,或者我们根本没有源码,面对这样一个编译后的可执行二进制文件,想要修改程序的运行结果,可能显 得有些困难。然而,如果能够善用动态连接器的话,这都不是问题。

下面我们来创建一段程序:

1// unrandom.c
2int rand() {
3    return 10;
4}

然后将它编译成动态链接库:

1liubang@venux-dev:~$ gcc -shared -fPIC unrandom.c -o libunrandom.so

这样我们就得到了一个名为libunrandom.so的动态链接库,然后执行:

 1liubang@venux-dev:~$ LD_PRELOAD=./libunrandom.so ./random
 210
 310
 410
 510
 610
 710
 810
 910
1010
1110

是不是发现原来程序的结果变成了 10 个 10,即使执行多次,结果都一样。为什么会这样呢?LD_PRELOAD到底做了什么事? 下面我们来进一步了解其中的原理。

我们使用 ldd 命令查看原始程序用到的链接库:

1liubang@venux-dev:~$ ldd random
2	linux-vdso.so.1 (0x00007ffebcba5000)
3	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f21ad8d7000)
4	/lib64/ld-linux-x86-64.so.2 (0x00007f21adeca000)

这些链接库是在程序编译时期就确定的,而且被编译到最终的可执行文件中了。众所周知,libc.so提供了标准 c 函数库,所以所有的 c 程序都会使用到这个链接库。 我们可以使用下面的命令查看libc.so都提供了哪些标准函数:

1liubang@venux-dev:~$ nm -D /lib/x86_64-linux-gnu/libc.so.6
2...
3...
400000000000443a0 T rand
5...

在输出的结果中,我们确实能够看到rand这个标准函数。

下面我们再来看看,使用LD_PRELOAD后到底发生了什么。

1liubang@venux-dev:~$ LD_PRELOAD=./libunrandom.so ldd ./random
2	linux-vdso.so.1 (0x00007ffe48bed000)
3	./libunrandom.so (0x00007f419497d000)
4	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f419458c000)
5	/lib64/ld-linux-x86-64.so.2 (0x00007f4194d81000)

很显然,我们的程序“被强制”使用了libunrandom.so这个链接库。从而导致 random 程序在调用rand()函数的时候使用了我们自己实现的,而不是真正的rand()函数。

这个技巧仅仅使用来 hack 程序,修改程序的运行结果吗?其实不然,在实际的应用中,通常会使用这个技巧来做一些类似于 Java 中的’AOP’,对程序进行切面,并注入一些有用的代码,以实现一些功能。

下面我们来看另一个例子:我们想要知道某个二进制程序中每次调用 malloc 时的参数,并统计调用了多少次。注意这里仅仅是想要获取调用标准函数 malloc 的参数,而不会改变原始程序的行为。

 1// malloc_trace.c
 2#include <dlfcn.h>
 3#include <unistd.h>
 4#include <stddef.h>
 5#include <stdio.h>
 6#include <string.h>
 7
 8static int c = 0;
 9
10void *malloc(size_t size) {
11    char buf[128];
12    static void *(*real_malloc)(size_t) = NULL;
13    if (real_malloc == NULL) {
14        real_malloc = dlsym(RTLD_NEXT, "malloc");
15    }
16    c++;
17    sprintf(buf, "malloc called, size = %zu, count = %d\n", size, c);
18    write(2, buf, strlen(buf));
19    return real_malloc(size);
20}

编译成动态链接库:

1liubang@venux-dev:~$ gcc -D_GNU_SOURCE -shared -ldl -fPIC -o libmalloc_trace.so malloc_trace.c

然后我们来统计一下ls这个命令中 malloc 调用的参数和次数:

 1liubang@venux-dev:~$ LD_PRELOAD=./libmalloc_trace.so ls
 2malloc called, size = 552, count = 1
 3malloc called, size = 120, count = 2
 4malloc called, size = 1024, count = 3
 5malloc called, size = 5, count = 4
 6......
 7malloc called, size = 5928, count = 174
 8malloc called, size = 208, count = 175
 9malloc called, size = 208, count = 176
10malloc called, size = 1024, count = 177
11 bin   btree.c   Desktop   Documents   Downloads   go   libmalloc_trace.so   libunrandom.so   malloc_trace.c   merge_sort.c   Pictures   Public   random   random.c   rsync.sh   unrandom.c  'VirtualBox VMs'   workspace   模板

大功告成,是不是很简单。