Linux进程伪装:动态修改/proc/self/exe
字数 1253 2025-08-24 07:48:23
Linux进程伪装:动态修改/proc/self/exe 技术详解
1. 背景知识
在Linux系统中,/proc/pid目录下记录了进程的许多重要信息:
/proc/pid/cmdline:进程启动时的命令行参数/proc/pid/exe:指向进程可执行ELF文件的软链接/proc/pid/cwd:指向进程运行当前路径的软链接
安全监控软件通常会记录这些信息来检测恶意进程。因此,攻击者需要掌握修改这些信息的技术来隐藏自身。
2. 进程信息修改技术
2.1 修改进程名(cmdline)
通过修改argv[0]可以改变进程名,示例代码:
extern char **environ;
static char **g_main_Argv = NULL; // pointer to argument vector
static char *g_main_LastArgv = NULL; // end of argv
void setproctitle_init(int argc, char **argv, char **envp) {
int i;
for (i = 0; envp[i] != NULL; i++) // calc envp num
continue;
environ = (char **)malloc(sizeof(char *) * (i + 1)); // malloc envp pointer
for (i = 0; envp[i] != NULL; i++) {
environ[i] = malloc(sizeof(char) * strlen(envp[i]));
strcpy(environ[i], envp[i]);
}
environ[i] = NULL;
g_main_Argv = argv;
if (i > 0)
g_main_LastArgv = envp[i - 1] + strlen(envp[i - 1]);
else
g_main_LastArgv = argv[argc - 1] + strlen(argv[argc - 1]);
}
void setproctitle(const char *fmt, ...) {
char *p;
int i;
char buf[MAXLINE];
extern char **g_main_Argv;
extern char *g_main_LastArgv;
va_list ap;
p = buf;
va_start(ap, fmt);
vsprintf(p, fmt, ap);
va_end(ap);
i = strlen(buf);
if (i > g_main_LastArgv - g_main_Argv[0] - 2) {
i = g_main_LastArgv - g_main_Argv[0] - 2;
buf[i] = '\0';
}
// 修改argv[0]
(void)strcpy(g_main_Argv[0], buf);
p = &g_main_Argv[0][i];
while (p < g_main_LastArgv)
*p++ = '\0';
g_main_Argv[1] = NULL;
// 调用prctl设置进程名
prctl(PR_SET_NAME, buf);
}
使用示例:
int main(int argc, char *argv[]) {
setproctitle_init(argc, argv, environ);
setproctitle("[sshd]"); // 将进程名修改为[sshd]
// ...
}
2.2 修改进程ID
通过fork()可以改变进程ID:
// fork返回值为0表示是子进程;大于0表示父进程;小于0表示错误
if (fork() > 0)
exit(0); // 父进程退出,子进程继续
3. 修改/proc/self/exe的技术
3.1 使用execve系列函数
execve会在保持进程ID不变的情况下替换整个进程内容,包括/proc/self/exe。
示例代码:
int main() {
printf("PID=%d\n", getpid());
execve("/bin/sh", 0, 0); // 替换为/bin/sh
return 0;
}
高级用法:使用内存ELF文件结合execveat系统调用
static unsigned char elf_file_data[] = {
// ELF文件内容
};
#define PAGE_SZ 0x1000
#define PAGE_ALIGN(size) (size % PAGE_SZ == 0 ? size : ((size / PAGE_SZ + 1)*PAGE_SZ))
int main() {
int fd = memfd_create("", 1);
const int len = sizeof(elf_file_data);
ftruncate(fd, len);
char *shm = mmap(NULL, PAGE_ALIGN(len), 3, 1, fd, 0);
if (shm == MAP_FAILED) {
perror("mmap");
return -1;
}
memcpy(shm, elf_file_data, len);
munmap(shm, PAGE_ALIGN(len));
execveat(fd, "", 0, 0, 0x1000);
return 0;
}
执行后/proc/pid/exe会变为'/memfd: (deleted)'
3.2 使用prctl(PR_SET_MM, PR_SET_MM_EXE_FILE, ...)
prctl系统调用提供了直接修改/proc/pid/exe的能力。
重要前提:需要先unmap所有现有的可执行内存区域(包括内核创建的,如ELF的.text段)。
示例代码:
int prctl_routine(char *name) {
errno = 0;
int fd = open(name, O_RDONLY);
if (fd < 0) {
perror("open");
return EXIT_FAILURE;
}
int ret = prctl(PR_SET_MM, PR_SET_MM_EXE_FILE, fd, 0, 0);
if (ret < 0) {
perror("prctl");
}
close(fd);
return 0;
}
int main() {
char exe[256] = {0};
readlink("/proc/self/exe", exe, 255);
printf("Before %s\n", exe);
prctl_routine("/bin/sh");
memset(exe, 0, sizeof(exe));
readlink("/proc/self/exe", exe, 255);
printf("After %s\n", exe);
return 0;
}
注意事项:
- 普通编译的程序直接运行会报错:"Device or resource busy"
- 需要使用UPX等加壳工具先处理程序,因为加壳过程会unmap原有内存映射
操作步骤:
gcc a.c -o a -static
upx a -o a_upx # 使用UPX加壳
./a_upx # 运行加壳后的程序
成功执行后/proc/self/exe会被修改为/bin/dash
4. 技术要点总结
-
进程伪装三要素:
- 修改进程名(cmdline)
- 修改进程ID
- 修改可执行文件路径(/proc/self/exe)
-
/proc/self/exe修改方法对比:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| execve | 简单直接 | 会替换整个进程 | 需要完全替换进程时 |
| execveat+memfd | 隐蔽性强 | 需要准备内存ELF | 高级隐蔽需求 |
| prctl | 不替换进程 | 需要先unmap内存 | 需要保持进程状态时 |
- 安全注意事项:
- prctl方法在Linux 4.9及之前版本只能使用一次
- 从Linux 4.10开始取消了这一限制
- 使用prctl前必须确保所有可执行内存区域已被unmap
5. 防御建议
对于安全防护软件,检测此类伪装技术需要注意:
- 不要仅依赖/proc信息进行进程识别
- 结合多种检测手段,如内存特征分析、行为监控等
- 特别注意使用memfd_create和execveat组合的隐蔽技术
- 监控prctl系统调用的使用情况