Linux下预加载型动态库后门
0x00 前言
在Linux中,要隐藏指定的文件或进程,常用的技术手段是使用Rootkit。Rootkit可以通过两种方式实现:一种是在内核层面,通过加载内核驱动模块;另一种是在用户层面,通过注入共享动态库。具体到后者,它通过LD_PRELOAD机制预加载一个自定义的共享库,这样当应用程序尝试调用某些标准库函数时,系统会优先使用预加载的共享库中的同名函数,从而实现对原始函数的劫持。通过这种劫持机制,可以操控系统行为,以隐藏特定的文件或进程。
0x01 原理
我们可以通过ldd来查看某个程序运行时加载的动态库:
1.动态链接器(Dynamic Linker):当一个程序启动时,动态链接器负责加载程序所需的所有动态库,并解析程序中未定义的符号(如函数和变量)到这些库中定义的符号。
2.环境变量LD_PRELOAD:在Linux系统中,LD_PRELOAD环境变量用于指定在程序启动时优先加载的共享库列表。如果在这个列表中指定了一个自定义的SO库,那么这个库将在程序的其他动态库之前被加载。
3.符号解析(Symbol Resolution):当动态链接器加载库时,它会按照一定的顺序解析符号。如果在LD_PRELOAD指定的库中找到了程序未定义的符号,那么这个符号将被绑定到该库中的实现。
4.函数劫持(Function Interception):通过创建一个包含与被劫持函数同名的函数的SO库,并使用LD_PRELOAD来确保这个库被优先加载,可以实现对原始函数的劫持。当程序尝试调用原始函数时,实际上会调用预加载库中的函数。
通过使用strace来跟踪ps aux的执行过程,我们可以看到ps会执行read系统调用,逐一读取/proc路径下的相关文件,以获取相应的进程信息。然后,它会执行write系统调用,将这些信息输出到屏幕上:
/proc主要用于内核与用户空间的交互,包含了大量的目录,每个目录名作为进程的pid,目录下的文件保存着这个进程的各种信息,例如:
/proc/[pid]/status文件包含了进程的状态信息
/proc/[pid]/cmdline文件包含了进程的命令行参数信息
/proc/[pid]/stat文件包含了进程的详细状态信息
而在一系列read和write前,ps会执行getdents64系统调用,获取读取到的目录:
实际上,getdents64 是一个系统调用,它不直接对应于 C 标准库中的函数。在用户空间,通常使用 readdir 或 readdir_r 这样的库函数来读取目录内容,这些函数内部会使用 getdents64(或者 getdents,取决于系统和文件系统的类型)来实现目录项的读取。
readdir()是C语言libc库中提供的用于读取目录内容的函数的一个函数,在Linux中用于读取某个目录下的内容,它返回的值如下:
- d_ino:文件的 inode 号。
- d_off:目录文件中到下一个 dirent 结构的偏移。
- d_reclen:当前 dirent 结构的长度。
- d_type:文件类型。这个字段不是所有的文件系统都支持,可能需要通过其他方式来确定文件类型。
- d_name:文件名,以 null 结尾的字符串。
所以,我们可以劫持原本的readdir(),先一步获取返回结果,通过当前进程的/proc/[pid]/cmdline内容,判断是否需要将这个进程的显示过滤掉。
例如下列c代码定义了新的readdir()函数,屏蔽系统中正在运行的frpc进程
1 |
|
将上述代码编译为动态库hide_frp.so:
1 | gcc -shared -fPIC hide_frp.c -o hide_frp.so -ldl |
-shared: 生成的是共享库文件
-fPIC: 生成位置无关代码,使其可以在内容中任意位置执行,创建共享动态库的必要条件
-ldl: 链接动态链接库,提供运行时动态加载和调用共享库的功能
在目标主机上,通过修改环境变量的方式来加载hide_frp.so,劫持原本的readdir函数,实现从ps命令的执行结果中隐藏掉frp进程。
加载动态库后,frpc进程被隐藏:
0x02 持久化
此时的隐藏只适用于当前shell会话及其派生的子环境,因为我们修改的环境变量只会在当前shell生效,其他用户登录后,并不会加载hide_frp.so,也并不会实现隐藏进程。为了使每个登录的用户都去加载hide_frp.so,我们可以修改一些配置文件,例如.bashrc、.bash_profile,或者修改/etc/ld.so.preload等动态链接库配置文件。
/etc/ld.so.preload文件是 Linux 中的一个动态链接器配置文件。它的主要作用是保存程序运行之前需要预先加载的库。当一个程序启动时,动态链接器会查看/etc/ld.so.preload,然后加载文件中列出的所有库。
动态库被加载的过程:
在/etc/ld.so.preload中写入需要预加载的so库后,登录新的shell环境,也无法查看到frp进程:
现在进程虽然已经隐藏了,但是有一定经验的溯源人员还是会查看/etc/ld.so.preload,所以还需要一些手法对/etc/ld.so.preload和hide_frp.so文件进行进一步隐藏,使其通过常规的cat、ls、rm等命令无法发现。
例如,通过strace追踪rm命令执行过程可以发现,rm命令实际调用的是unlinkat()去执行删除文件的操作:
因此,我们可以尝试劫持unlinkat(),使rm无法删除hide_frp.so:
1 | int unlinkat(int dirfd, const char *pathname, int flags) { |
实现效果:
通过劫持系统函数open(),避免cat、vi等命令直接查看到文件的内容:
1 | int open(const char *pathname, int flags, mode_t mode) { |
实现效果:
逐步优化hide_frp.so,可以实现更加完美的隐藏,不过,这种方式只适用于动态编译的二进制,对于静态编译的程序,运行时并不会加载动态库,这种方式也无法生效了,所以,我们可以利用替换静态编译的二进制来进行溯源。
0x03 溯源
例如,下载静态编译的busybox,查看/etc/ld.so.preload,并将hide_frp.so删除,删除后,系统会因为无法加载hide_frp.so产生报错:
frp进程随之暴露: