Tradecraft Improvement 3 - AMSI Evasions 1 - Reverse Engineering AMSI
Pre-requisites
- Basic understanding of Windows internals
- Some knowledge about PowerShell
- Basic knowledge about malwares
- Basic C/C++ knowledge
Introduction to AMSI
There are already a number of blogs and sources dedicated to the evasion of Microsoft’s Antimalware Scan Interface
aka AMSI
. But I wanted to write a blog which would encompass certain important tips and tricks that would help you improve your own bypasses and would give you some knowledge and understanding of AMSI to look for more bypasses as well. I will divide this blog into 2 or more parts so that it helps you later on during your pentesting / Red Team engagements. I will also post my code and scripts for your reference in my GitHub
Coming back to AMSI, it was introduced by Microsoft to protect against the malicious usage of scripting languages like PowerShell, VBScript or JScript. This can also be used by AV engines, Microsoft Defender or even other developers to examine various .NET scripts that are executed.
However, with recent developments to AMSI
, it is not just limited to scripting languages, but it can also be used to examine languages that are dynamically executed. This also includes applications written in C# since they use dynamic code loading or may even interact with scripting languages.
Internal Workings of AMSI
Example of AMSI working
In order to understand how AMSI
bypasses work, we need to understand the internal workings of AMSI
first. This will allow us to later understand where we are going to look for more AMSI
bypasses. Therefore, we can make short changes to our loader scripts whenever we get flagged and keep on loading our malwares.
So, AMSI
works based on signature based detection. This means that when a script is executed, it will look for certain characters or words which are already considered malicious.
As an example we can consider the following example
- If we write the string
invoke-mimikatz
in a PowerShell session, we will get a message from AMSI flagging our string.
This is because the string invoke-mimikatz
is already stored as malicious. Now let’s we try to execute something similar which is already flagged, it will be immediately blocked like this by AMSI. Similar thing will happen if we use Write-Host invoke-mimikatz
to print out invoke-mimikatz
on the output.
- Now if we use something common like a simple base64 encoding as obfuscation, we can bypass this.
function Decode-Base64String {
param (
[string]$base64String
)
try {
$decodedBytes = [Convert]::FromBase64String($base64String)
$decodedText = [System.Text.Encoding]::UTF8.GetString($decodedBytes)
return $decodedText
} catch {
Write-Error "An error occurred: $_"
return $null
}
}
# Example usage
$base64Encoded = "aW52b2tlLW1pbWlrYXR6Cg=="
$decodedText = Decode-Base64String -base64String $base64Encoded
Write-Host "Decoded Text: $decodedText"
- Here we have base64 encoded the string
invoke-mimiatz
beforehand. We will use this base64 encoded string in our PowerShell script to print out the stringinvoke-mimikatz
- But before execution, we need to set the
Execution
policy toRemote-Signed
orUnrestricted
for the sake of simplicity. There are also way to bypass this but I will discuss them later in another blog.
- But before execution, we need to set the
Run the following command in an Administrator PowerShell session
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
As we can see that our script has successfully printed the invoke-mimikatz
string without AMSI printing out any alert.
Taking a look at AMSI Internals
For reference we can take a look at the above picture taken from https://learn.microsoft.com/en-us/windows/win32/amsi/how-amsi-helps
- It shows that there is an
AMSI
provider which is actually a COM (Component Object Model) which opens up aCOM
interface calledIAntimalwareProvider
- This provider can be used by any
AV/EDR
vendor or any program or developer to look for malicious signatures in various scripts or programs.- They can make use of the various functions within the provider such as
Scan()
function
- They can make use of the various functions within the provider such as
- In order to implement AMSI, the AV vendor first needs to registry the
COM
object by creating an associatedCLSID
entry under theHKLM\CLSID
registry key. - Then they need to registry the same
CLSID
underComputer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AMSI\Providers\
We can find the provider is already registered.
More references for developers about AMSI can be found here -> https://learn.microsoft.com/en-us/windows/win32/amsi/dev-audience
This might be help to someone who is looking to learn more about the defensive capabilities of AMSI and how they are being used by a certain AV or defensive program.
The amsi.dll
As shown in the reference image for AMSI, we will find that there is a library called amsi.dll
is loaded in a process. This DLL can be used to directly work with AMSI.
If we take a look at the loaded modules in a PowerShell process, we will find the amsi.dll
- This will usually be our main target when trying to create bypasses for AMSI.
- This is because the function within it can be patched, hooked and manipulated pretty much under all circumstances
- We can see these functions, if we open up
amsi.dll
under PE Bear. Theamsi.dll
can be found in the folderC:\Windows\System32\amsi.dll
Here we can see all the functions that are exported by amsi.dll
. Now we need to figure out which functions are mainly use by PowerShell. Since this will enable us to write bypasses for our PowerShell based loaders
Checking function calls in amsi.dll
- We will refer to the Export Table that we viewed in PE Bear and then inspect the disassembly of those functions in x64 Dbg
- Then we will attach to a PowerShell session and examine the flow of those function calls
NOTE: While testing on Windows 11, Windows Defender was flagging my x64 dbg when I tried attaching to PowerShell process. Probably because the binary wasn’t signed. For testing purposes I kept Real-time protection turned off.
- First lets attach to a PowerShell process using x64 Dbg
- Then under the
amsi.dll
module look for theAmsiScanString
API
Once you attach to the PowerShell process you can view the loaded modules using the Alt+E
shortcut and then search for the required modules and functions in the search bar.
- Clicking on the AmsiScanString API we find that after initializing some variables, it finally calls the AmsiScanBuffer API.
Therefore, we can put a breakpoint in the AmsiScanBuffer
API call and enter some string in the PowerShell session.
Here I have added 2 breakpoints. One at the AmsiScanBuffer
call and another at the first line of the AmsiScanBuffer
function.
Also, added a breakpoint at the starting of AmsiScanString
. This will help us understand if it is actually AmsiScanString
that calls AmsiScanBuffer
when we enter a string in the PowerShell session or not.
- Now enter any command in the PowerShell prompt
Upon pressing enter we will notice that instead of hitting the start of AmsiScanString
or even the call from AmsiScanString
we directly enter AmsiScanBuffer
start
Taking a quick look at the disassembly of AmsiScanBuffer
we will find our string being passed to it and some checks are run on it. We will later on go into details using IDA while trying to find more info about exploitable patterns.
However, we still need to find out which function is actually calling AmsiScanBuffer
. For this we will place a breakpoint on some more functions in amsi.dll
Here you can see that I have added breakpoints to some common AMSI APIs. We will again start execution and check which API is hit first
As soon as we start execution, we will see that the AmsiOpenSession is called first. According to MSDN it opens up a session within which multiple scan requests can be correlated.. Looking at its definition
HRESULT AmsiOpenSession(
[in] HAMSICONTEXT amsiContext,
[out] HAMSISESSION *amsiSession
);
We will find that it takes a structure HAMSICONTEXT
as an input and gives out a structure of type HAMSISESSION
as an output. These structures are undocumented.
This means before any other function is called AmsiOpenSession
definitely needs to get called. Continuing with the execution of the code we will find that the next function executed is AmsiScanBuffer
As we can see in the above picture our string will also be passed to AmsiScanBuffer
API. Continuing through the execution, we will find that at this mainly 2 functions are being AmsiOpenSession
and AmsiScanBuffer
which are important to us.
Another function that we can notice is when starting the PowerShell process. Till now we were just attaching to the powershell.exe
process, now we will start a powershell.exe
process from the debugger itself. In that it will be debugged from the start only.
This can be done by selecting the powershell.exe
file from File
option from x64Dbg
- Right now the process has just be loaded and is stuck at the default breakpoint at the entry
Therefore, there are no extra modules like amsi.dll
loaded at this moment.
- After continuing through the program for sometime, we will notice that
clr.dll
is also loaded. This is the DLL where all the core components of .NET Framework are loaded. This will be important later on
- Now as soon as
amsi.dll
shows up in the loaded modules check the breakpoints
- We will find that it has hit the AmsiInitialize function. It has the following definition
HRESULT AmsiInitialize(
[in] LPCWSTR appName,
[out] HAMSICONTEXT *amsiContext
);
appName
-> is the first argument that it takes and contains the identity of the application that is calling the AMSI APIamsiContext
-> of typeHAMSICONTEXT
is a variable is passed to all subsequent API calls and we see this being passed toAmsiInitialize
API as well. Therefore, it is also an important data.
Continuing through the execution we will find that the next API being called just after is AmsiOpenSession
And subsequently it will call AmsiScanBuffer
until the PowerShell session is initialized.
Testing the API call Flow with API monitor
Now we will again test our theory with API Monitor . For this we will first run the API monitor program as an Administrator and then load the symbols for the external DLL amsi.dll
Once we have added all these APIs, we will start a new PowerShell process
- As soon as we launch a new PowerShell process we will find the APIs that are used to initialize AMSI
- Just like we previously analyzed,
AmsiInitialize
is being called first followed byAmsiOpenSession
- If we enter a malicious string like
invoke-mimikatz
we will find the APIs that are being called for scanning that
- We will also find our string being passed to
AmsiScanBuffer
call
Finally it will exit out with AmsiCloseSession
Final API flow for AMSI in a PowerShell session
So the final flow of AMSI functions that will take place when a PowerShell session is loaded will look something as follows:
AmsiInitialize
-> First theAmsiInitialize
API is called to create a commonamsiContext
of typeHAMSICONTEXT
for the particular AMSI API caller. This context will be used by the subsequent API calls likeAmsiOpenSession
AmsiOpenSession
-> As already mentioned next API that is called will beAmsiOpenSession
. This will take theamsiContext
and create a newamsiSesssion
which will then be used for a particular set of scanAmsiScanBuffer
-> This API takes 6 arguments and the definition looks as follows
HRESULT AmsiScanBuffer(
[in] HAMSICONTEXT amsiContext,
[in] PVOID buffer,
[in] ULONG length,
[in] LPCWSTR contentName,
[in, optional] HAMSISESSION amsiSession,
[out] AMSI_RESULT *result
);
- First the
AmsiScanBuffer
will take theamsiContext
which was created byAmsiInitialize
- Then it takes the pointer to the buffer containing our required input
length
parameter holds the length of the buffer that we want to entercontentName
will hold the name of the any file / script / URL that you want AMSI to scan.amsiSession
-> In this case its an optional parameters but is used in most of the cases. This was created by theAmsiOpenSession
API.result
-> Finally we have the result which is of type AMSI_RESULT enum. This helps AMSI determine whether or not the content in the buffer should be blocked.
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
} ;
AmsiCloseSession
-> Lastly we will have this API to close the current AMSI scanning session. It requires both theamsiContext
and theamsiSession
.
Conclusion
Now that we have a clear idea about the main AMSI APIs being used in a PowerShell session and the order in which they are called, we can dive into how they work and their internals. Once we have a good understanding of their internals we will be able to start devising bypasses according to our requirements.
I will be posting the analysis of AMSI APIs and writing basic evasions in the next part since this part has already become very long.