一、先搞懂核心:什么是“进程隐藏”?普通用户为啥能做到?
咱们先抛开复杂术语,用大白话讲明白:进程隐藏不是让进程“消失”,而是让它“伪装”起来——明明在后台运行着你想执行的程序(比如终端、监听工具),但用ps、top这些常用命令查看时,显示的却是另一个人畜无害的程序名称(比如apache、系统工作线程),让人一眼看过去不会起疑心。
很多人会有疑问:Linux里普通用户权限很低,连修改系统文件都要输密码,怎么能隐藏进程?其实关键在于“找对突破口”——我们不修改系统内核,不搞特殊权限,只“欺骗”那些查看进程的工具,而这些工具的核心信息来源,都是Linux系统里的/proc文件系统。
简单说:所有查看进程的命令(ps、top、lsof),都不是直接和内核沟通,而是去读/proc目录下的文件(比如/proc/进程ID/cmdline、/proc/进程ID/stat),这些文件里存着进程的名称、命令行参数、运行状态。我们只要修改这些文件对应的进程内部结构,就能让工具“看走眼”,达到隐藏效果——而修改自己启动的进程的内部结构,普通用户完全有权限做到,不需要root,也不需要特殊工具。
补充一个关键知识点:Linux中“进程所有者”对自己启动的进程有完全控制权,包括修改进程的名称、环境变量等内部状态,这是普通用户能实现进程隐藏的核心前提,也是这种隐藏方式的合法性基础(仅用于合法场景,比如渗透测试、合法隐蔽监控)。
二、核心原理:不碰内核,只“欺骗”/proc文件系统
整个隐藏逻辑的核心,就围绕一句话:让目标进程的实际运行内容和/proc目录中显示的内容不一致。具体来说,就是让进程实际执行我们需要的命令(比如终端、监听程序),但让/proc目录中显示它是另一个普通程序(比如apache),这样别人用工具查看时,只会看到伪装后的程序,看不到真实运行的内容。 这里要区分两个关键概念,避免混淆:
- 真实进程:我们实际要运行的程序(比如/bin/bash、监听工具),负责执行核心功能;
- 伪装进程:我们让工具显示的程序(比如apache、kworker),仅用于“迷惑”查看者,不执行实际功能。 而实现这一点的核心技术,依赖3个Linux底层知识点,咱们用大白话逐个解释:
1. fork与execve:进程的“分身术”
fork就是“创建分身”——一个进程(父进程)创建出另一个完全一样的进程(子进程);execve就是“换灵魂”——让子进程放弃原来的身份,去执行另一个程序。这是Linux中启动所有程序的基础,也是我们实现隐藏的第一步:父进程创建子进程,子进程通过execve执行真实要运行的程序,但在执行前,先“伪装”自己的身份。
2. ptrace:进程的“远程操控器”
ptrace是Linux提供的一个系统调用,简单说就是“让一个进程能操控另一个进程”——父进程可以用ptrace“附着”(监控)子进程,查看子进程的内存、寄存器状态,甚至给子进程注入命令,让它执行特定操作。这是实现隐藏的核心工具:父进程通过ptrace操控子进程,修改子进程的内部状态,让它在/proc中显示为伪装的程序。 这里要注意:普通用户只能用ptrace操控自己创建的子进程,不能操控别人的进程,这也符合Linux的权限规则,不会越权。
3. prctl:进程的“改名工具”
prctl也是一个系统调用,其中有一个功能是“修改进程名称”(PR_SET_NAME)——通过这个调用,我们可以给进程设置一个新的名称,这个名称会显示在ps、top等命令的输出中,也会写入/proc/进程ID/stat文件。这是伪装的关键一步:通过prctl给子进程设置伪装名称,让工具看到的是伪装后的名字。
三、设计思路:一步步拆解,普通用户也能看懂的实现逻辑
整个进程隐藏的设计思路,就像“拍电影”:父进程是“导演”,子进程是“演员”,ptrace是“化妆师”,prctl是“改名道具”,/proc是“观众看到的屏幕”。导演让演员(子进程)扮演一个“路人甲”(伪装程序),但实际让演员做的是“主角”(真实程序)的工作,观众(查看进程的人)只能看到屏幕上的路人甲,看不到演员的真实身份。 具体设计步骤(对应代码核心逻辑,避开复杂语法,只讲流程),咱们用“步骤+大白话”拆解,再搭配简易流程图,一看就懂:
第一步:解析用户指令,准备“伪装道具”
用户运行工具时,会指定两个关键信息:一是“真实要运行的程序”(比如/bin/bash),二是“要伪装成的程序”(比如apache)。工具先解析这两个信息,同时准备好伪装需要的环境变量——比如把真实程序的环境变量复制一份,再修改其中的关键变量(比如“_=”变量,这个变量会显示进程的启动路径),让环境变量也符合伪装程序的特征,避免露出马脚。
第二步:创建子进程,让子进程“准备伪装”
父进程通过fork创建子进程,子进程启动后,先调用ptrace(PTRACE_TRACEME),意思是“告诉父进程,我允许你操控我”,然后调用execve执行真实程序,但此时execve传入的“命令行参数”是伪装程序的参数(比如apache的“-k start”)——这样一来,子进程实际执行的是真实程序,但内核会根据execve的参数,在/proc中暂时记录伪装程序的信息,为后续伪装打基础。
第三步:父进程“操控”子进程,完成伪装
子进程执行execve后,会暂停等待父进程的操控(这是ptrace的特性),此时父进程开始“化妆”:
- 父进程通过ptrace查看子进程的内存和寄存器状态,找到子进程的栈结构(栈里存着进程的命令行参数、环境变量等关键信息);
- 父进程调用prctl给子进程设置伪装名称(比如“apache2 -k start”),这个名称会立即更新到/proc中;
- 父进程继续通过ptrace单步执行子进程,直到找到子进程的main函数入口——这是因为子进程执行真实程序时,main函数是程序的核心入口,只有找到这里,才能修改进程的内部状态,让它既能正常执行真实程序,又能保持伪装;
- 父进程在子进程的内存中,创建一个新的“执行头”(包含真实程序的命令行参数、环境变量),然后修改子进程的寄存器,让子进程从这个新的“执行头”开始执行——这样一来,子进程实际执行的是真实程序,但/proc中显示的却是伪装程序的信息,实现“表里不一”。
第四步:清理现场,父进程“功成身退”
伪装完成后,父进程会断开和子进程的ptrace连接,清理掉自己的临时数据,然后根据用户需求,要么后台运行,要么等待子进程结束——此时子进程已经完全伪装完成,后台执行真实程序,而任何查看进程的工具,都只会看到伪装后的程序名称和参数,看不到真实内容。 简易流程原理图(文字版,不用复杂图表,大白话看懂) 用户输入指令 → 工具解析真实程序+伪装程序 → 父进程fork创建子进程 → 子进程调用ptrace允许操控 → 子进程execve(真实程序+伪装参数) → 父进程ptrace附着子进程 → 父进程prctl修改子进程名称 → 父进程找到子进程main函数 → 父进程创建新执行头,修改寄存器 → 父进程断开连接,子进程后台执行(真实程序+伪装显示)
四、代码核心逻辑解读:避开复杂语法,只看关键操作
...
intmain(int argc, char **argv, char **envp){
...
if(argv){
i = 0;
while(argv[i]){
if(argv[i][0] == SOH_CHAR){
self_exec = 1;
argv[i][0] = NULL_CHAR;
argv[i] = NULL;
break;
}
i++;
}
}
if(self_exec){
memset(&argc, i, 1);
i++;
if((retval = prctl(PR_SET_NAME, argv[i], NULL, NULL, NULL)) == -1){
fprintf(stderr, "prctl(PR_SET_NAME, %lx, NULL, NULL, NULL): %s\n", \
(unsignedlong) argv[i + 1], strerror(errno));
exit(-1);
}
j = 0;
while(argv[i][j]){
argv[i][j] = NULL_CHAR;
j++;
}
wait(NULL);
exit(0);
}
tmp_size = get_vector_byte_count(envp);
tmp_size += PATH_MAX + 3;
opterr = 0;
while ((opt = getopt(argc, argv, "e:m:wa:bdqvh")) != -1) {
switch (opt) {
case'a':
tmp_size += strlen(optarg) + 1;
}
}
if((mimic_envp = (char **) calloc(tmp_size, sizeof(char))) == NULL){
fprintf(stderr, "calloc(%d, %d): %s\n", tmp_size, (int) sizeof(char), strerror(errno));
exit(-1);
}
optind = 1;
opterr = 1;
while ((opt = getopt(argc, argv, "e:m:wa:bdrqvh")) != -1) {
switch (opt) {
case'e':
execute_string = optarg;
break;
case'm':
mimic_string = optarg;
break;
case'b':
background = 1;
break;
case'd':
daemonize = 1;
break;
case'a':
if(!key_match(mimic_envp, optarg)){
mimic_envp[mimic_envc++] = optarg;
}
break;
case'r':
raw_mimic = 1;
break;
case'q':
quiet = 1;
break;
case'v':
quiet = 0;
break;
case'h':
default:
usage();
}
}
if((argc - optind) || !execute_string){
usage();
}
// Do initialization of basic data structures for the given input.
if(!mimic_string){
if(!getuid()){
mimic_string = DEFAULT_ROOT_MIMIC;
}else{
mimic_string = DEFAULT_MIMIC;
}
}
if(wordexp(execute_string, &execute_wordexp_t, 0)){
error(-1, errno, "wordexp(%s, %lx, 0)", execute_string, (unsignedlong) &execute_wordexp_t);
}
if((execute_argv = wordexp_t_to_vector(&execute_wordexp_t)) == NULL){
error(-1, errno, "wordexp_t_to_vector(%lx)", (unsignedlong) &execute_wordexp_t);
}
execute_argc = execute_wordexp_t.we_wordc;
if(raw_mimic){
if((mimic_argv = calloc(2, sizeof(char *))) == NULL){
error(-1, errno, "calloc(2, %d)", (int) sizeof(char *));
}
tmp_size = strlen(mimic_string);
if((mimic_argv[0] = calloc(tmp_size + 1, sizeof(char))) == NULL){
error(-1, errno, "calloc(%d, %d)", tmp_size + 1, (int) sizeof(char));
}
memcpy(mimic_argv[0], mimic_string, tmp_size);
mimic_argc = 1;
}else{
if(wordexp(mimic_string, &mimic_wordexp_t, 0)){
error(-1, errno, "wordexp(%s, %lx, 0)", mimic_string, (unsignedlong) &mimic_wordexp_t);
}
if((mimic_argv = wordexp_t_to_vector(&mimic_wordexp_t)) == NULL){
error(-1, errno, "wordexp_t_to_vector(%lx)", (unsignedlong) &mimic_wordexp_t);
}
mimic_argc = mimic_wordexp_t.we_wordc;
}
if(raw_mimic){
tmp_size = strlen(mimic_argv[0]);
if((mimic_short_name = (char *) calloc(tmp_size + 1, sizeof(char))) == NULL){
error(-1, errno, "calloc(%d, %d)", tmp_size + 1, (int) sizeof(char));
}
memcpy(mimic_short_name, mimic_argv[0], tmp_size);
}else{
if((mimic_argv[0][0] == '[') && (mimic_argv[0][strlen(mimic_argv[0]) - 1] == ']')){
if((mimic_short_name = (char *) calloc(strlen(mimic_argv[0]) - 2 + 1, sizeof(char))) == NULL){
error(-1, errno, "calloc(%d, %d)", (int) (strlen(mimic_argv[0]) - 2 + 1), (int) sizeof(char));
}
memcpy(mimic_short_name, mimic_argv[0] + 1, strlen(mimic_argv[0]) - 2);
}else{
if((mimic_short_name = strrchr(mimic_argv[0], '/')) == NULL){
mimic_short_name = mimic_argv[0];
}else{
mimic_short_name++;
}
}
}
if(envp){
i = 0;
while(envp[i]){
if(!key_match(mimic_envp, envp[i])){
if(!strncmp(envp[i], "_=", 2)){
tmp_string_ptr = (char *) calloc(strlen(mimic_argv[0]) + 3, sizeof(char));
tmp_string_ptr[0] = '_';
tmp_string_ptr[1] = '=';
memcpy(tmp_string_ptr + 2, mimic_argv[0], strlen(mimic_argv[0]));
mimic_envp[mimic_envc++] = tmp_string_ptr;
}else{
mimic_envp[mimic_envc++] = envp[i];
}
}
i++;
}
}
if(daemonize){
if(!quiet){
printf("Disconnecting from the tty and becoming a daemon now.\n");
fflush(stdout);
quiet = 1;
}
retval = fork();
if(retval == -1){
error(-1, errno, "daemon fork()");
}elseif(retval){
exit(0);
}
if(chdir("/") == -1){
error(-1, errno, "chdir(\"/\")");
}
if(setsid() == -1){
error(-1, errno, "setsid()");
}
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
}
if(!quiet){
printf("Launching child...");
fflush(stdout);
}
retval = fork();
if(retval == -1){
error(-1, errno, "fork()");
}elseif(!retval){
// Child.
// Request to be attached.
if((retval = ptrace(PTRACE_TRACEME, 0, NULL, NULL)) == -1){
error(-1, errno, "ptrace(PTRACE_TRACEME, 0, NULL, NULL)");
}
// Go, go, go!
execve(execute_argv[0], mimic_argv, mimic_envp);
error(-1, errno, "execve(%s, %lx, NULL)", execute_argv[0], (unsignedlong) mimic_argv);
}else{
child_pid = retval;
if(!quiet){
printf("\t\t\tSuccess!\n");
}
if(!quiet){
printf("Waiting for child to attach...");
fflush(stdout);
}
wait(NULL);
if(!quiet){
printf("\t\tSuccess!\n");
}
if(!quiet){
printf("Initializing ptrace_do...");
fflush(stdout);
}
if((child = ptrace_do_init(child_pid)) == NULL){
fprintf(stderr, "ptrace_do_init(%d): %s\n", child_pid, strerror(errno));
exit(-1);
}
if(!quiet){
printf("\t\tSuccess!\n");
}
if(!quiet){
printf("Determining stack state...");
fflush(stdout);
}
errno = 0;
peektext = ptrace(PTRACE_PEEKTEXT, child->pid, child->saved_regs.rsp, NULL);
if(errno){
fprintf(stderr, "ptrace(PTRACE_PEEKTEXT, %d, %lx, NULL): %s\n", \
child->pid, (unsignedlong) child->saved_regs.rsp, \
strerror(errno));
goto CLEAN_UP;
}
argc_stack_val = peektext;
argv_stack_val = child->saved_regs.rsp + 0x8;
envp_stack_val = argv_stack_val + ((argc_stack_val + 1) * 0x8);
if(!quiet){
printf("\t\tSuccess!\n");
}
if(!quiet){
printf("Politely requesting name change...");
fflush(stdout);
}
if((local_buffer = ptrace_do_malloc(child, strlen(mimic_short_name) + 1)) == NULL){
fprintf(stderr, "ptrace_do_malloc(%lx, %d): %s\n", \
(unsignedlong) child, (int) (strlen(mimic_short_name) + 1), \
strerror(errno));
goto CLEAN_UP;
}
memset(local_buffer, 0, strlen(mimic_short_name) + 1);
memcpy(local_buffer, mimic_short_name, strlen(mimic_short_name));
if((remote_buffer = ptrace_do_push_mem(child, local_buffer)) == 0){
fprintf(stderr, "ptrace_do_push_mem(%lx, %lx): %s\n", \
(unsignedlong) child, (unsignedlong) local_buffer, \
strerror(errno));
goto CLEAN_UP;
}
errno = 0;
ret_long = ptrace_do_syscall(child, __NR_prctl, PR_SET_NAME, (unsignedlong) remote_buffer, 0, 0, 0, 0);
if(errno){
fprintf(stderr, "ptrace_do_syscall(%lx, __NR_prctl, PR_SET_NAME, %lx, 0, 0, 0, 0): %s\n", \
(unsignedlong) child, (unsignedlong) remote_buffer, \
strerror(errno));
goto CLEAN_UP;
}
if(ret_long < 0){
fprintf(stderr, "remote prctl(PR_SET_NAME, %lx, 0, 0, 0): %s\n", \
(unsignedlong) child->saved_regs.rsp + 0x8, \
strerror(-ret_long));
goto CLEAN_UP;
}
if(!quiet){
printf("\tSuccess!\n");
}
if(!quiet){
printf("Searching for main()...");
fflush(stdout);
}
memcpy(&test_regs, &(child->saved_regs), sizeof(struct user_regs_struct));
while( ! \
( \
(test_regs.rdi == argc_stack_val) && \
(test_regs.rsi == argv_stack_val) && \
(test_regs.rdx == envp_stack_val) && \
(test_regs.rip > child->map_head->start_address) && \
(test_regs.rip < child->map_head->end_address) && \
(test_regs.rax == test_regs.rip)
)){
if((ret_long = ptrace(PTRACE_SINGLESTEP, child->pid, NULL, NULL)) == -1){
fprintf(stderr, "ptrace(PTRACE_SINGLESTEP, %d, NULL, NULL): %s\n", \
child->pid, \
strerror(errno));
break;
}
wait(&status);
if(!WIFSTOPPED(status)){
fprintf(stderr, "!WIFSTOPPED()\n");
ret_long = -1;
break;
}
if((ret_long = ptrace(PTRACE_GETREGS, child->pid, NULL, &test_regs)) == -1){
fprintf(stderr, "ptrace(PTRACE_GETREGS, %d, NULL, %lx): %s\n", \
child->pid, (unsignedlong) &test_regs, \
strerror(errno));
break;
}
}
if(ret_long == -1){
fprintf(stderr, "Error: libc register setup not detected. Aborting!\n");
goto CLEAN_UP;
}
if(!quiet){
printf("\t\t\tSuccess!\n");
}
if(!quiet){
printf("Building execution headers...");
fflush(stdout);
}
memcpy(&(child->saved_regs), &test_regs, sizeof(struct user_regs_struct));
execution_header_size = get_vector_byte_count(argv);
execution_header_size += get_vector_byte_count(envp);
execution_header_size += 2 * sizeof(unsignedlong);
if((execution_header_local = (char **) ptrace_do_malloc(child, execution_header_size)) == NULL){
fprintf(stderr, "ptrace_do_malloc(%lx, %d): %s\n", \
(unsignedlong) child, (int) ((execute_argc * sizeof(char **)) + 1), \
strerror(errno));
goto CLEAN_UP;
}
memset(execution_header_local, 0, execution_header_size);
if((execution_header_remote = ptrace_do_get_remote_addr(child, (void *) execution_header_local)) == NULL){
fprintf(stderr, "ptrace_do_get_remote_addr(%lx, %lx): %s\n", \
(unsignedlong) child, (unsignedlong) execution_header_local, \
strerror(errno));
goto CLEAN_UP;
}
build_execution_headers(execution_header_local, execution_header_remote, execute_argv, envp);
if((execution_header_remote = ptrace_do_push_mem(child, execution_header_local)) == 0){
fprintf(stderr, "ptrace_do_push_mem(%lx, %lx): %s\n", \
(unsignedlong) child, (unsignedlong) execution_header_remote, \
strerror(errno));
goto CLEAN_UP;
}
ptrace_do_free(child, execution_header_local, FREE_LOCAL);
if(!quiet){
printf("\t\tSuccess!\n");
}
if(!quiet){
printf("Setting up final state...");
fflush(stdout);
}
child->saved_regs.rdi = execute_argc;
child->saved_regs.rsi = (unsignedlong) execution_header_remote;
child->saved_regs.rdx = (unsignedlong) execution_header_remote + ((execute_argc + 1) * sizeof(char **));
if(!quiet){
printf("\t\tSuccess!\n");
}
if(!quiet){
printf("\n\tGood-bye and have a good luck! :)\n\n");
}
CLEAN_UP:
ptrace_do_cleanup(child);
if(!background){
soh_string[0] = SOH_CHAR;
soh_string[1] = NULL_CHAR;
if((foreground_mimic_argv = (char **) calloc(mimic_argc + 3, sizeof(char **))) == NULL){
fprintf(stderr, "calloc(%d, %d): %s\n", \
mimic_argc + 3, (int) sizeof(char), strerror(errno));
exit(-1);
}
i = 0;
while(mimic_argv[i]){
foreground_mimic_argv[i] = mimic_argv[i];
i++;
}
foreground_mimic_argv[i++] = soh_string;
foreground_mimic_argv[i] = mimic_short_name;
execve(argv[0], foreground_mimic_argv, mimic_envp);
}
return(0);
}
return(-1);
}
intkey_match(char **vector, char *key_value){
int i;
char *ptr;
i = 0;
while(vector[i]){
if((ptr = strchr(key_value, '=')) != NULL){
if(!strncmp(vector[i], key_value, (int) (ptr - key_value) + 1)){
return(1);
}
}
i++;
}
return(0);
}
voidbuild_execution_headers(void *local_buffer, void *base_addr, char **argv, char **envp){
...
argc = 0;
if(argv){
while(argv[argc++]){}
}
envc = 0;
if(envp){
while(envp[envc++]){}
}
tmp_len = (argc * sizeof(char *)) + (envc * sizeof(char *)) + (2 * sizeof(char *));
pointer_ptr = (char **) local_buffer;
tmp_ptr_local = local_buffer + tmp_len;
tmp_ptr_remote = base_addr + tmp_len;
if(argv){
i = 0;
while(argv[i]){
*(pointer_ptr++) = tmp_ptr_remote;
tmp_len = strlen(argv[i]);
memcpy(tmp_ptr_local, argv[i], tmp_len);
tmp_ptr_local += tmp_len + 1;
tmp_ptr_remote += tmp_len + 1;
i++;
}
pointer_ptr++;
}
if(envp){
i = 0;
while(envp[i]){
*(pointer_ptr++) = tmp_ptr_remote;
tmp_len = strlen(envp[i]);
memcpy(tmp_ptr_local, envp[i], tmp_len);
tmp_ptr_local += tmp_len + 1;
tmp_ptr_remote += tmp_len + 1;
i++;
}
}
}
intget_vector_byte_count(char **argv){
int i = 0;
int total_strlen = 0;
while(argv[i]){
total_strlen += strlen(argv[i]);
i++;
}
return( ((i + 1) * sizeof(void *)) + total_strlen + (i * sizeof(char)) );
}
If you need the complete source code, please add the WeChat number (c17865354792)
咱们不用逐行看代码(大部分是错误处理、参数解析,没必要),只挑代码中实现上述步骤的关键部分,用大白话解读,结合设计思路,一看就懂:
1. 参数解析
代码中用getopt函数解析用户输入的参数(比如“-e”指定真实程序,“-m”指定伪装程序),如果用户没指定伪装程序,会自动设置默认值:普通用户默认伪装成apache,root用户默认伪装成系统工作线程(kworker)——这是因为这两个程序在Linux中很常见,不容易引起怀疑。 同时,代码会初始化伪装用的环境变量(mimic_envp),复制真实程序的环境变量,再修改“_=”变量,让它指向伪装程序的路径,避免环境变量露出马脚。
2. 子进程创建与准备
代码中用fork()创建子进程,子进程中执行两个关键操作:
- ptrace(PTRACE_TRACEME, 0, NULL, NULL):告诉父进程“可以操控我”;
- execve(execute_argv[0], mimic_argv, mimic_envp):execute_argv是真实程序的路径,mimic_argv是伪装程序的参数——这样子进程实际执行真实程序,但内核会暂时记录伪装参数。
3. 父进程操控子进程
这是代码的核心部分,主要做了3件事:
- 等待子进程暂停:用wait(NULL)等待子进程执行execve后暂停,准备接受操控;
- 修改子进程名称:通过ptrace给子进程注入prctl系统调用,设置伪装名称(比如apache);
- 修改子进程执行状态:通过ptrace单步执行子进程,找到main函数入口,然后创建新的执行头(包含真实程序的参数),修改寄存器,让子进程执行真实程序。
4. 清理与退出
代码中用ptrace_do_cleanup()清理ptrace的连接资源,然后父进程根据用户是否指定“后台运行”(-b参数),决定是等待子进程结束,还是自己退出,让子进程后台继续运行。
五、相关领域知识点总结:不止于“隐藏”,看懂背后的Linux底层逻辑
这个进程隐藏工具的设计,本质上是对Linux进程管理、系统调用、文件系统的综合运用,掌握这些知识点,不仅能看懂这个工具,还能理解Linux进程的运行本质,咱们总结几个核心知识点,都是面试、学习中常用的:
1. Linux进程管理核心:fork与execve的配合
Linux中所有程序的启动,都是“fork+execve”的组合:fork创建子进程,execve让子进程执行新程序。这是进程创建的基础,也是很多进程操控工具(比如调试工具gdb)的核心原理——gdb就是通过ptrace操控子进程,实现断点、调试功能。
2. /proc文件系统:进程信息的“展示窗口”
/proc是Linux的虚拟文件系统,不是真实的磁盘文件,而是内核在内存中创建的“临时文件”,每个进程对应一个/proc/进程ID目录,里面的cmdline、stat、environ等文件,存储着进程的关键信息。所有查看进程的工具,都是通过读取这些文件来获取进程信息的——这也是“伪装”的核心突破口:修改这些文件对应的进程内部状态,就能欺骗工具。
3. 系统调用:用户态与内核态的“桥梁”
ptrace、prctl、fork、execve都是Linux的系统调用——用户态程序(比如我们的工具)通过系统调用,向内核请求服务(比如创建进程、修改进程名称)。普通用户能调用这些系统调用,但权限有限(比如只能操控自己的子进程),这也是Linux权限管理的核心:最小权限原则。
4. 进程内存结构:栈与寄存器的作用
进程运行时,栈中存储着命令行参数、环境变量、函数调用信息,寄存器存储着当前执行的指令地址、函数参数等。工具通过修改栈中的参数和寄存器的状态,让进程既能执行真实程序,又能在/proc中显示伪装信息——这背后是对进程内存布局的深刻理解。
六、测试运行
编译成功后,目录下会生成 ./mimic 可执行文件。按从易到难的方式测试:
测试 1:最基础的伪装
./mimic -e /bin/bash
这时候你会发现终端"卡住"了——实际上它已经启动了一个 bash,但你在另一个终端里执行:
ps aux | grep apache
你会看到类似这样的输出:
youruser 12345 0.0 0.0 16648 1324 pts/0 S+ 12:00 0:00 /usr/sbin/apache2 -k start
这就是伪装成功了。ps 看到的名字是 apache2,但实际上这个进程的真实身份是 /bin/bash。
怎么确认它是假的?
# 1. 找到那个进程的 PID(比如 12345)
# 2. 查看它实际执行的是哪个文件
ls -l /proc/12345/exe
# 输出会是:/proc/12345/exe -> /bin/bash
# 3. 再看它的真实命令行
cat /proc/12345/cmdline | tr '\0'' '
# 输出会是:/usr/sbin/apache2 -k start
# 4. 但看进程短名(stat 里的 comm)
cat /proc/12345/stat | awk '{print $2}'
# 输出会是:(apache2)
测试 2:后台运行
前台运行会占用你的终端,实际用起来一般是后台:
./mimic -b -e "/bin/bash -c 'sleep 300'"
然后立刻查进程:
ps aux | grep apache
你会看到一个"apache2"进程静静躺在那里,实际上它在执行 sleep 300。
测试 3:自定义伪装身份
./mimic -b -e "/bin/bash -c 'sleep 300'" -m "/usr/sbin/cron -f"
这时候 ps aux 看到的就是 cron 而不是默认的 apache2。
测试 4:伪装成内核线程
如果你用 root 跑:
sudo ./mimic -b -e "/bin/bash -c 'sleep 300'" -m "[kworker/0:0]"
然后看 ps aux | grep kworker,它会混在一堆真正的内核线程里,非常难分辨。
总结
其实这种普通用户就能实现的进程隐藏,本质上不是“高科技”,而是对Linux底层逻辑的深刻理解——找到/proc文件系统这个“薄弱点”,利用fork、ptrace、prctl这些系统调用,让进程“表里不一”,从而达到隐藏效果。
而背后涉及的进程管理、系统调用、/proc文件系统、进程内存结构等知识点,不仅是理解这个工具的关键,也是Linux学习的核心——掌握这些知识点,你不仅能看懂进程隐藏的原理,还能举一反三,理解更多Linux工具(比如调试工具、进程管理工具)的工作逻辑。
Welcome to follow WeChat official account【程序猿编码】