Use LD_PRELOAD to hook libc Library Functions
/南京大学蒋炎岩老师在可执行文件和加载中讲到可以使用环境变量LD_PRELOAD
对libc的函数进行"override"。Linux Manual中对LD_PRELOAD
的介绍如下:
LD_PRELOAD
A list of additional, user-specified, ELF shared objects to be loaded before all others. This feature can be used to selectively override functions in other shared objects.
The items of the list can be separated by spaces or colons, and there is no support for escaping either separator. The objects are searched for using the rules given under DESCRIPTION. Objects are searched for and added to the link map in the left-to-right order specified in the list.
In secure-execution mode, preload pathnames containing slashes are ignored. Furthermore, shared objects are preloaded only from the standard search directories and only if they have set-user-ID mode bit enabled (which is not typical).
Within the names specified in the LD_PRELOAD list, the dynamic linker understands the tokens
$ORIGIN
,$LIB
, and$PLATFORM
(or the versions using curly braces around the names) as described above in Rpathtoken expansion. (See also the discussion of quoting under the description of LD_LIBRARY_PATH.)There are various methods of specifying libraries to be preloaded, and these are handled in the following order:
The LD_PRELOAD environment variable.
The --preload command-line option when invoking the dynamic linker directly.
The /etc/ld.so.preload file (described below).
利用LD_PRELOAD
这个环境变量,我们可以将一些我们感到不满意的库函数替换成我们自己实现的函数,或者是对库函数进行插装,来获取程序对库函数的调用信息。下面这个程序演示了如何对库函数进行插桩:
malloc_override.c
1 | // By GPT-4 |
编译该程序为动态链接库:
1 | gcc -shared -fPIC -o malloc_override.so malloc_override.c -ldl |
执行命令
1 | LD_PRELOAD=./malloc_override.so ls |
可以看到命令行的输出:
1 | malloc(0x563533a0a2d0) |
hooking实现的原理比较简单,我们使用LD_PRELOAD
预先加载了我们自己编译的动态库,malloc和free这两个符号的地址就已经确定下来了,之后/lib64/ld-linux-x86-64.so.2
加载libc时,libc中的malloc和free不会再覆盖之前我们已经确定的符号。此后ls程序调用malloc和free,就是调用我们自己的动态库中的malloc和free了。
因为我们的目的是对malloc和free进行插桩,所以我们只是在上面做了一层包装,最终还是需要调用libc中的malloc和free,但是这该如何实现呢?使用函数指针即可。real_malloc
和real_free
指向的是libc中malloc和free的位置,这两个函数指针的地址是通过dlsym
来获取的。需要注意的是,dlsym
的参数中有一个是RTLD_NEXT
,表示下一个符号的位置。因为我们使用LD_PRELOAD
提前加载了我们的动态库,因此libc中的malloc和free在我们的动态库符号的下一级,也就是第二个符号。
利用同样的方法,可以对其他的libc库函数进行插桩。
在研究过程中,我发现一个比较有趣的问题,如果将fprintf
的参数从stderr
换成stdout
之后,再使用相同的命令,会出现segmentation fault:
1 | ❯ LD_PRELOAD=./malloc_override.so ls |
将fprintf(stdout, ...)
换成printf
之后也是同样的情况。我百思不得其解,我用了下面这样一个测试程序来测试动态库:
prac.c
1 |
|
执行以下命令:
1 | ❯ gcc prac.c |
我的程序中只出现了一次malloc,但是在hooking中却出现了两次malloc,那一定是printf
这个函数调用了malloc。这也与我们的常识符合,stdout是有一个buffer的,每次的输出都会先输出到buffer中,等到出现换行符时,再将输入打印到终端。
如果禁用stdout的buffer呢?会出现什么结果?修改后的malloc_override.c
如下:
malloc_override.c
1 | // ... |
再执行同样的命令,发现不再出现segmentation fault,可以正常运行。
所以最终来看,是stdout buffer的问题。
为了探究出现segmentation fault的原因,将malloc_override.c
中setbuf(stdout, NULL)
删除,并使用fprintf(stderr, ...)
输出函数调用信息:
malloc_override.c
1 | //... |
编译动态库之后重新执行命令:
1 | ❯ gcc -shared -fPIC -g -o malloc_override.so malloc_override.c -ldl |
命令行输出如下:
1 | call malloc(77) |
能看出是stack overflow导致segmentation fault,那么即可推断原因:使用fprintf
或者printf
向 stdout输出内容时调用malloc申请buffer,此后malloc又再次调用 fprintf 向stdout输出内容,循环递归导致stack overflow,最终segmentation fault。
但是将libc替换为musl,重新编译prac.c,程序却能够正常运行:
1 | ❯ musl-gcc prac.c |
查看musl的内部实现:
stdio.h
1 |
stdout.c
1 | static unsigned char buf[BUFSIZ+UNGET]; |
可以看到,在musl中,stdout的buffer是在静态数据区,调用printf
或者fprintf(stdout, ...)
时不会调用malloc,因此也不会出现上述stack overflow的情况。