1.1、LD_PRELOAD HOOK 原理
LD_PRELOAD
是一个环境变量,用于在运行 Linux 程序时指定一个或多个共享库,这些库会在其他库之前加载。这使得开发者可以替换或增强现有库的功能。以下是对 LD_PRELOAD
的详细解释:
1. 用途
- 函数重载:通过提供自定义实现,重载某些库函数。例如,可以替换标准库函数以添加日志、监控或调试功能。
- 调试和分析:用于调试程序,允许开发者在不修改源代码的情况下跟踪函数调用。
- 安全性:可以用来实现安全检查,例如在访问敏感资源时进行权限验证。
2. 如何使用
要使用 LD_PRELOAD
,可以在终端中设置这个环境变量,然后运行程序。例如:
export LD_PRELOAD=/path/to/your/library.so
./your_program
3. 工作原理
- 当程序启动时,动态链接器(
ld.so
)会检查LD_PRELOAD
环境变量,并加载指定的共享库。 - 这些库的函数实现会优先于系统库的相应函数,允许开发者插入自定义代码。
当程序启动时,动态链接器(如 ld.so)负责解析和加载程序所需的共享库。
LD_PRELOAD 环境变量指定的共享库会被动态链接器优先加载,无论是程序自身的共享库
还是系统共享库。
1.2、实践
思路:我们先写一个关于字符串对比的myfile.c文件,然后我们hook掉strcmp库函数,使得无论我们输入的密码是否正确,我们使其输出'longlongNB'
myfile.c源码:
#include <stdio.h>
#include <string.h>
int main() {
char *name = "user";
char *passwd = "longlong";
char input[100]; // 用于存储用户输入的密码
// 提示用户输入密码
printf("Enter your password: ");
fgets(input, sizeof(input), stdin);
// 移除输入末尾的换行符
input[strcspn(input, "\n")] = '\0';
// 比较输入的密码与预定义密码
if (strcmp(input, passwd) == 0)
{
printf("Passwords match!\n");
printf("Welcome %s!\n", name);
printf("longlongNB")
}
else
{
printf("Passwords do not match.\n");
}
return 0;
}
上代码的大概意思是,先定义用户的账号密码,然后和通过函数strcmp对比输入的密码,如果正确就输出:
Passwords match!\n
Welcome user!\n
longlongNB
如果密码错误就输出:
Passwords do not match.
然后编译文件
gcc myfile.c -o myfile //x86
我们先运行一下这个代码,分别输入正确的密码和错误的密码进行验证我们程序是否可以正确运行。
我们可以看到我们程序可以正确运行,接下来我们编写我们hook文件,去hook掉这个strcmp函数,使程序不管密码输入的是否正确,都会输出密码正确是所输出的内容。
myhook.c源码:
#include <stdio.h>
#include <string.h>
int strcmp(const char *s1, const char *s2)
{
if(getenv("LD_PRELOAD") != NULL){
unsetenv("LD_PRELOAD"); // 必须清除LD_PRELOAD环境变量,否则会陷入hook的死循环。
}
printf("This is the hook file for longlong.\n");
return 0;
}
这个代码就只是对strcmp函数的重写,一旦我们hook成功,那么就会输出:
This is the hook file for longlong.
if(getenv("LD_PRELOAD") != NULL){
unsetenv("LD_PRELOAD"); // 必须清除LD_PRELOAD环境变量,否则会陷入hook的死循环。
}
这段代码需要着重讲解一下,这个代码的意思清除这个环境变量 ,以防止后面再有调用这个函数的时候也会被hook掉。(后面1.3、细节的补充章节,详细讲解一下)
编译myhook.c文件
gcc -shared -fPIC myhook.c -o myhook.so
然后我们加载我们的myhook.so文件,再运行myfile文件
LD_PRELOAD=./myhook.so ./myfile
1.3、细节的补充
关于上面讲到的代码:
if(getenv("LD_PRELOAD") != NULL)
{
unsetenv("LD_PRELOAD"); // 必须清除LD_PRELOAD环境变量,否则会陷入hook的死循环。
}
myfile.c源码:
#include <stdio.h>
#include <string.h>
int main() {
char *name = "user";
char *passwd = "longlong";
char input[100]; // 用于存储用户输入的密码
// 提示用户输入密码
printf("Enter your password: ");
fgets(input, sizeof(input), stdin);
// 移除输入末尾的换行符
input[strcspn(input, "\n")] = '\0';
// 比较输入的密码与预定义密码
if (strcmp(input, passwd) == 0)
{
printf("Passwords match!\n");
printf("Welcome %s!\n", name);
printf("longlongNB\n");
}
else
{
printf("Passwords do not match.\n");
}
//第二次比较
printf("//第二次比较\n");
if (strcmp(input, passwd) == 0)
{
printf("Passwords match!\n");
printf("Welcome %s!\n", name);
printf("longlongNB\n");
}
else
{
printf("Passwords do not match.\n");
}
return 0;
}
我们对myhook.c文件也进行需修改
myhook.c源代码:
#define _GNU_SOURCE // 启用 GNU 扩展特性,以访问额外的函数和符号
#include <stdio.h> // 引入标准输入输出库
#include <stdlib.h> // 引入标准库,包含动态内存管理和环境变量处理
#include <string.h> // 引入字符串处理函数库
#include <unistd.h> // 引入对 POSIX 操作系统 API 的访问
#include <dlfcn.h> // 引入动态链接库相关的函数
#include <sys/types.h> // 引入基本数据类型定义
#include <sys/stat.h> // 引入文件状态信息
// 声明一个指向原始 strcmp 函数的函数指针,初始值为 NULL
static int (*original_strcmp)(const char *s1, const char *s2) = NULL;
// 自定义 strcmp 函数实现
int strcmp(const char *s1, const char *s2)
{
// 检查环境变量 LD_PRELOAD 是否被设置
if (getenv("LD_PRELOAD") != NULL)
{
// 如果设置了 LD_PRELOAD,清除该环境变量
unsetenv("LD_PRELOAD");
printf("清除LD_PRELOAD"); // 输出提示信息
return 0; // 返回 0,表示字符串相等(自定义行为)
}
else
{
// 如果 original_strcmp 还未初始化
if (original_strcmp == NULL) {
// 使用 dlsym 获取原始 strcmp 函数的地址
original_strcmp = dlsym(RTLD_NEXT, "strcmp");
}
// 调用原始 strcmp 函数并返回结果
return original_strcmp(s1, s2);
}
}
1.3.1、arm编译:
编译:
arm-linux-gnueabi-gcc -o myfile-arm myfile.c
arm-linux-gnueabi-gcc -fPIE -shared -o myhook-test.so myhook-test.c -ldl
运行:
qemu-arm -L /usr/arm-linux-gnueabi/ -E LD_PRELOAD=./myhook-test.so ./myfile-arm
我们发现两次的调用第一次被hook掉,第二次是正常的进行了比较,显示密码错误。
这和我们所想要的结果是一致的。
1.3.2、其他补充
思路的补充解释:
我们上面的所讲的源码和hook的思路就是,通过hook库函数,来简单的学习hook的思路,我们上述通过简单的字符串比较来使其输出不同的结果,从而直观的验证我们思路。
我们上面的之所以写两个strcmp就是想直观的理解一下LD_PRELOAD这个全局变量。
上面我们所演示的hook源码,就是通过 #include <dlfcn.h>头文件中的dlsym()函数来拿到原本的strcmp函数的地址,从而实现我们第一次strcmp的比较是调用的我们自己的hook函数,而第二次则是调用的原本的库函数strcmp。
头文件的使用方法
#include <dlfcn.h>头文件需要我们定义一个宏定义#define _GNU_SOURCE,目的是启用 GNU 扩展特性,以访问额外的函数和符号,否则我们在编译的时候会遇到下图所示的问题。
在编译我们hook文件时,我们需要加上‘-ldl’,因为-ldl 是一个编译选项,用于链接动态链接库(dynamic linking library)。具体来说,它指示编译器链接 libdl 库,该库提供了与动态链接相关的功能,比如 dlsym、dlopen 和 dlclose 等函数。
这些函数主要用于在运行时加载共享库和查找符号,使得程序能够在执行时动态地加载和使用库中的功能。使用 -ldl 是编译涉及这些功能的程序时必不可少的步骤。
否则就会遇到下图的问题。
关于hook技术在IOT领域的应用场景:
当我们拿到一个设备的固件时,我们没有实体设备,但是又想验证我们的漏洞思路,所以只能通过qemu模拟的方法来手动模拟固件。但是在固件启动时,可能会存在从设备上获取设备码或者需要从内存获取某些特定值的情况存在,会导致我们模拟固件失败,而我们没有这个实体设备,所以我们就可以通过hook的方法来hook掉其中的某些函数,使得这个固件启动时以为你成功获取到了某些值,从而使得固件成功模拟。