用户态隐藏

进程名伪装 (prctl结合argv[0])

在网上看到一个 prctl 函数可以修改当前的进程名,使用方法则在当前逻辑添加代码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/prctl.h>

int main(int argc, char* argv[], char *envp[])
{
const char *new_title = "prctl_new_name";
prctl(PR_SET_NAME, new_title, NULL, NULL, NULL);

while (true) {
sleep(2);
}

return 0;
}

image-20250613105255966

但是好像不起作用

首先我们需要知道的是,pstop的底层逻辑是读取并遍历/proc目录下的文件来展示信息

我们可以通过进程号来看看prctl的一些相关信息

/proc/3080/cmdline

1
./prctl

/proc/3080/status /proc/3080/comm

image-20250613105546586

因此ps命令应该是读取/proc/3080/cmdline的结果,以上方法对制定明确进程号(ps -a ps -p 进程ID)和top有效

但我们可以通过修改argv[0](第一个元素就是程序的进程名)来改变/proc/[pid]/cmdline的内容,并结合prctl这里要注意防止内存溢出

新名称 ≤ argv[0] 原长度,则我们可以直接覆盖 argv[0] 内容,并将剩余部分填 \0。因为此时已分配的内存空间是够的

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/prctl.h>

int main(int argc, char **argv) {
strcpy(argv[0],"Qu43ter");
prctl(PR_SET_NAME, "stomped");
pause();
return 0;
}

image-20250613114651183

新名称 > argv[0] 原长度,直接扩展 argv[0] 会覆盖后续的 argv[1...argc-1] (所有非程序名的参数)或环境变量,导致崩溃或数据损坏

正确做法是申请新内存将argv[1...argc-1] 复制到新区域,并重新设置 argv[0]

https://github.com/smaugx/setproctitle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/prctl.h>

#include "../setproctitle.h"

char **smaug_os_argv;

int main(int argc, char* argv[], char *envp[])
//argc:参数个数
//argv:参数数组
//envp:环境变量数组
{
smaug_os_argv = argv;

//const char *new_title = "hidden_main_new";
const char *new_title = "";
if (smaug_init_setproctitle() == SMAUG_PROCTITLE_OK) {
smaug_setproctitle(new_title);
}

prctl(PR_SET_NAME, new_title, NULL, NULL, NULL);

while (true) {
sleep(1);
}
return 0;
}

image-20250613162823377

这种隐藏方式其实还是会有纰漏,但是如果遇到不仔细的管理员也有可乘之机,但是我们可以伪造成已有相似的进程名来混淆

文件系统隔离

我们之前提及到ps实际上是遍历/proc目录下的文件,但如果将进程目录文件挂载到其它目录就可以“隐身”了

1
2
mkdir hide_proc
sudo mount -o bind ./hide_proc /proc/183335

image-20250613171303676

我们可以cat /proc/mounts来检测是否有文件系统被挂载到了进程目录下

image-20250613172306338

1
2
3
4
5
6
- 设备路径 :挂载的设备,例如 /dev/sda1
- 挂载点 :设备被挂载到的目录,例如 /proc/183335
- 文件系统类型 :例如 ext4
- 挂载选项 :例如 rw,relatime,errors=remount-ro
- dump标志 :用于备份,通常为 0
- fsck顺序 :文件系统检查顺序,通常为 0

通过umount /proc/183335卸载挂载点

动态库劫持(LD_PRELOAD)

对于查看系统进程命令ps top,本质上是通过readdir去读/proc,那如果我们能够劫持readdir等函数调用改变其逻辑那就可以忽略掉我们想要隐藏的进程,实现方式就是跟换掉动态链接库,调用我们自己的逻辑方式,从而到达替换效果 https://github.com/gianlucaborello/libprocesshider

而在Linux中的一个环境变量LD_PRELOAD允许你定义在程序运行前优先加载的动态链接库(绕过Disable Functions也是这个原理之一)

https://pyer.dev/post/some-record-for-module-and-so-in-linux-738817ac

LD_PRELOAD

系统还存在另外一个环境变量叫做 LD_PRELOAD,这个文件中我们可以指定预先装载的一些共享库或者目标文件。在 LD_PRELOAD 里面指定的文件会在动态链接器按照固定规则搜索共享库之前装载,它比 LD_LIBRARY_PATH 里面所指定的目录中的共享库还要优先。无论程序是否依赖于它们,LD_LIBRARY_PATH 里面指定的共享库或目标文件都会被装载。

由于全局符号介入这个机制的存在,LD_PRELOAD 里面指定的共享库或目标文件中的全局符号就会覆盖后面加载的同名全局符号,这使得我们可以很方便地做到改写标准 C 库中的某个或某几个函数而不影响其他函数,对于程序的调试或测试非常有用。

/etc/ld.so.preload)等效于这个环境变量。

image-20250614191239303

选择需要隐藏的进程后,开始编译

1
2
3
make
sudo mv libprocesshider.so /usr/local/lib/
sudo echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload

但是修改/etc/ld.so.preload的前提是root用户或者sudo权限

image-20250614193038538

image-20250614193021386

若想要找到这种恶意文件的话就需要查找异常的网络连接,或者直接检测/etc/ld.so.preload,要么就老老实实去/proc中一个个找

内核态隐藏

Rootkit技术(LKM模块)

eBPF隐藏

插个眼,以后找个时间单独学习,涉及到Linux内核的确实一时半会不好搞

参考文章:

Linux上隐藏进程名(初级版)

隐藏在显眼的地方:在类 UNIX 系统中修改进程名称

如何隐藏一个 Linux 进程