简介
LD_PRELOAD工具,更合理的称呼应该是LD_PRELOAD机制。它是Linux程序加载的一套机制:简单来说,就是可以将动态编译的程序(ldd有显示动态链接库的,都是动态编译的程序,且gcc编译时,如果未加-static参数,则就是使用动态编译程序),在加载其下所有需要的动态链接库前,先加载你所指定的动态链接库。并且LD_PRELOAD机制是每个Linux上所有动态编译的程序必走的流程,所以在Linux上,动态链接库的加载顺序应该是:
通过该机制,就可以实现动态链接库函数的劫持(即动态的实现hook函数)。该机制大大加快了排查问题的效率,原先需要修改内核,在系统调用处加入打印日志的排查方式,基本都可以用该机制平替。
使用LD_PRELOAD环境变量
一般使用LD_PRELOAD环境变量跑LD_PRELOAD有两种方式,其中一种是直接设置环境变量,如下所示:
export LD_PRELOAD=/xxx/libxxx.so#绝对路径
但此种方式意义不大,因为如果是想要截获某些系统命令,若该命令是bash中的内置命令(help命令可以列出bash所有内置命令,或使用type xxx(命令)查看是否是bash内置命令),由于bash启动时,无法设置该环境变量(环境变量的设置,总是要在bash启动后,才可设置),所以此种方式无法截获bash内置命令。
但此方式可用于简单测试自己编写的动态链接库中,需要替换的函数符号,是否和原动态链接库(一般是glic库)的函数符号相同(一定要编译出的函数符号相同,才可成功劫持)
下面以kill命令为例,演示下使用这个环境变量的效果:
1.查看kill命令用了哪个库函数(一般bash内置命令和非内置命令,所使用的库函数都是相同的)
2.重新定义该库函数,并将其编译成一个动态链接库
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<dlfcn.h>
//在glibc中查看kill原本的定义为int kill (pid_t pid, int sig)
//但其实只要定义的函数名称,也就是函数符号相同,则可被成功劫持
//所有这就不管参数了
int kill ()
{
printf("******lz*******\n");
return 0;
}
再将以上程序编译成动态链接库:
gcc -shared -fPIC hook_kill.c -o libhook_kill.so
3.在需要执行/bin/kill命令的bash中,设置环境变量
export LD_PRELOAD=/xxx/libhook_kill.so
4.执行/bin/kill命令,结果如下图所示,说明已被成功劫持
另:如果不想设置环境变量,还可通过以下方式执行命令,控制影响范围
LD_PRELOAD=`pwd`/libhook_kill.so /bin/kill `pidof top`
使用配置文件
由以上可知,如果想bash中的内置命令也生效,则至少需要在bash启动时,就能识别到所替换的动态链接库,LD_PRELOAD机制则提供了/etc/ld.so.preload配置文件
1.将想要在程序启动时,就加载的动态链接库,写入该配置文件
echo “/home/lz/Desktop/ld_preload/libhook_kill.so.0” > /etc/ld.so.preload
2.新开一个bash,再直接执行kill命令(bash内置),可以发现已成功被拦截
注意:
/etc/ld.so.preload配置文件中的动态链接库的加载,是在程序启动的时候就已经加载完毕了(静态加载,而非懒加载),也就是说,在程序运行过程中,如果修改了/etc/ld.so.preload文件中的内容,也并不会影响当前已经启动了的bash内置程序的执行结果(因为修改前的函数符号及其指令,在该bash启动时就已经被识别到,并加载到内存中了),只会影响到在其修改操作之后,启动的应用程序或命令的执行结果
其他
●hook_kill模板(摘抄自网络),学习该模板中的主要流程,即可编写出其他功能的hook工具:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <dlfcn.h>
typedef int(*KILL)(pid_t pid, int sig);
#define TMP_BUF_SIZE 256
/* 获取进程命令行参数 */
void get_cmd_by_pid(pid_t pid, char *cmd)
{
char buf[TMP_BUF_SIZE];
int i = 0;
snprintf(buf, TMP_BUF_SIZE, "/proc/%d/cmdline", pid);
FILE* fp = fopen(buf, "r");
if(fp == NULL)
{
return;
}
memset(buf, 0, TMP_BUF_SIZE);
size_t ret = fread(cmd, 1, TMP_BUF_SIZE - 1, fp);
/*
*需要下面for循环的原因是
*man手册资料
*This holds the complete command line for the process, unless the process is a zombie.
*In the latter case,there is nothing in this file: that is, a read on this file will return 0
*characters. The command-line arguments appear in this file as a set of strings separated by
*null bytes ('\0'), with a further null byte after the last string.
*/
for (i = 0; ret != 0 && i < ret - 1; i++)
{
if (cmd[i] == '\0')
{
cmd[i] = ' ';
}
}
fclose(fp);
cmd[TMP_BUF_SIZE - 1] = '\0';
}
//重新定义kill函数
int kill(pid_t pid, int sig)
{
static KILL orign_kill = NULL;
//接收kill命令的进程信息
char buf_des[TMP_BUF_SIZE] = {0};
get_cmd_by_pid(pid, buf_des);
//获取当前进程信息
char buf_org[TMP_BUF_SIZE] = {0};
get_cmd_by_pid(getpid(), buf_org);
//获取父进程信息
char buf_porg[TMP_BUF_SIZE] = {0};
get_cmd_by_pid(getppid(), buf_porg);
printf("hook kill(sig:%d): [%s(%d) -> %s(%d)] -> [%s(%d)]\n",
sig, buf_porg, getppid(), buf_org, getpid(), buf_des, pid);
out:
if(!orign_kill){
//获取下一个kill的函数指针
//这里如果下一次加载的是glibc中的kill
//则就可获取到原本的kill函数指针
orign_kill = (KILL)dlsym(RTLD_NEXT, "kill");
}
//调用原本的kill方法
return orign_kill(pid, sig);
}
生成动态链接库:
#程序中有使用dlopen、dlsym、dlclose、dlerror,显示加载动态库时,
#需要加入-ldl编译选项,才能在启动时,为动态库分配内存空间
gcc -shared -fPIC -o hook_kill.so hook_kill.c -ldl
●想要更多的了解linux动态链接器,可参考其源码以及如下文章:
1.动态链接库的装载过程(动态链接器的工作流程):
https://blog.csdn.net/xiaofei0859/article/details/50588387?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-5.pc_relevant_paycolumn_v3&spm=1001.2101.3001.4242.4&utm_relevant_index=7