简介

在红蓝对抗中,进程注入是攻击者维持访问、绕过防御的经典手段。本文通过一个完整的实验——从向 explorer.exe 注入 shellcode 获取反向 Shell,到使用 PowerShell 脚本与 WinDBG 进行交叉检测——逐层揭开线程注入的攻防细节,并深入解析检测工具背后的内存取证逻辑。

注入

首先,我们使用课程中编写的注入器程序,将一段用于反弹 Shell 的 shellcode 注入到 notepad.exe 或 exploer.exe 进程中。选择 explorer.exe 是因为它通常是用户态下长期运行且拥有合法网络连接权限的进程,注入后不易被察觉。

注入器内部会执行以下关键步骤:

  1. 打开目标进程:通过 OpenProcess 获取 notepad.exe 的句柄,申请必要的访问权限(如 PROCESS_VM_WRITEPROCESS_CREATE_THREAD)。

  2. 分配内存:在目标进程虚拟地址空间内调用VirtualAllocEx,申请一块可读、可写、可执行(RWX)的内存区域,大小与 shellcode 匹配。

  3. 写入 Shellcode:使用WriteProcessMemory将准备好的反弹 Shell 字节码拷贝到新分配的内存中。

  4. 启动远程线程:调用CreateRemoteThreadRtlCreateUserThread等 API,让目标进程执行这片内存中的代码。

执行注入器后,我们的受害者主机会立即显示弹窗信息,证明 shellcode 已在 notepad.exe 的上下文中成功运行。

注入检测

攻击发生后,我们切换到蓝队视角,使用开源 PowerShell 脚本 Get-InjectedThreads.ps1(来自 NetSPI)对系统内所有进程的线程进行扫描。

下载地址:

https://gist.github.com/jaredcatkinson/23905d34537ce4b5b1818c3e6405c1d2

$a = Get-InjectedThread; $a

脚本遍历当前系统中的每一个进程,并检查其内部的所有线程。很快,我们捕捉到一个异常项:

从输出可以明确看到,notepad 进程中存在一个线程 ID 为 5778 的活动线程,其起始地址指向一块私有、已提交的内存(MEM_PRIVATE | MEM_COMMIT),而正常线程通常从磁盘上加载的模块(MEM_IMAGE)开始执行。

这立刻触发了“已注入线程”的红旗。

为了进一步确认该异常线程是否确实携带了我们的恶意 shellcode,我们提取了线程内容字节。

在 PowerShell 中,$a.Bytes 字段存储了线程起始地址附近的内存数据,我们将其转换为熟悉的 \x 形式:

($a.Bytes | ForEach-Object ToString x2) -join "\x"

输出的十六进制字符串如下(示例):

与此同时,我们查看注入器源码中硬编码的原始 shellcode,两者进行严格比对。结果完全吻合——该线程正在执行我们的恶意负载,确认为注入后产生。

这种字节级的比对,在事件响应中可以形成可靠的技术证据,避免误报,也为后续的内存取证提供了精确的 IoC(入侵指标)。

WinDBG解剖

为了从调试器角度直观查看注入线程的细节,我们需要先定位具体是哪个线程。Process Explorer 等系统工具可以帮助我们快速找到 notepad.exe 下新增的线程。

如果存在注入,这里会变成一个孤零零的、很“突兀”的内存地址,比如 0x03730000,而不是像图中带有dll名称的符号(这里我们使用课程中讲解的进程注入编写,规避了显示)。

同时,Get-InjectedThread 的输出也直接给出了 ThreadId 和十进制形式的起始地址。

我们可以看到ThreadId为7548,也就是新建的线程,将其转为16进制为1d7c。

在WinDbg主界面,通过菜单或命令附加到目标进程。

点击 File → Attach to a Process,在弹出的列表中找到 explorer.exe,点击OK。

附加成功后,目标进程会被暂停,WinDbg的命令输入栏会显示调试提示符(例如 0:000>),等待指令。

使用~命令显示进程中的所有线程,确认可疑线程是否在列表中。

这个命令会列出所有线程的ID和状态。WinDbg使用~加上线程编号(如0, 1, 2…)来表示一个线程,注意这个编号是WinDbg内部的编号,并非系统线程ID。可以通过查看输出中的线程ID(十六进制显示)来定位我们之前发现的那个可疑注入线程。

接着,首先,使用~<线程编号> s命令切换到我们找到的可疑线程的上下文环境中。切换成功后,使用~.命令查看当前线程的详细信息。在输出中找到 StartAddressStart 字段,这个地址就是注入代码的入口点。

根据分析,Get-InjectedThreads.ps1 的检测核心在于判断线程起始地址的内存类型是否为 MEM_IMAGE。如果不是,则判定为可疑注入。我们可以通过以下命令在WinDbg中手动验证这一逻辑。

使用 !address 命令查询 StartAddressStart 所在内存区域的详细属性:

在输出结果中,查找 Type 和 State 字段:

  • 对于正常线程,类型应为MEM_IMAGE。

  • 对于我们分析的注入线程,Type 很可能为 MEM_PRIVATE(如果是通过 VirtualAllocEx 分配的内存)或 MEM_MAPPED。

  • 如果State是MEM_COMMIT,且Type不是MEM_IMAGE,则高度印证了注入行为。

对于正常的主线程或从 kernel32.dll 等模块启动的线程,起始地址必然位于一个 MEM_IMAGE 类型的内存区域,代码是由系统加载器从 PE 文件映射过来的。当攻击者使用 VirtualAllocEx 目标进程中动态分配内存并直接写入 shellcode 时,这块内存就是 MEM_PRIVATE 类型。因此,只要线程的起始地址分配 MEM_PRIVATE 内存中,就几乎可以将其巧克力该线程正在执行未映射到进程的匿名代码——这就是注入的标志。

当然,更巧妙的合法注入技术(如通过映射 DLL 后再篡改其.text节)可能导致内存类型仍为 MEM_IMAGE,但 Get-InjectedThreads 同时配合其他提示式检查(例如模块加载列表比对、线程初始化与已知模块导出函数的匹配)来增强检测率。虽然页面仅显示了最基础的“非内存中的线程”检测,但它已经足以对付大多数入门级到中级的注入方法。