Skip to main content

Tradecraft Improvement 3 - AMSI Evasions 1 - Reverse Engineering AMSI

Pre-requisites

  1. Basic understanding of Windows internals
  2. Some knowledge about PowerShell
  3. Basic knowledge about malwares
  4. 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 string invoke-mimikatz
    • But before execution, we need to set the Execution policy to Remote-Signed or Unrestricted for the sake of simplicity. There are also way to bypass this but I will discuss them later in another blog.

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 a COM interface called IAntimalwareProvider
  • 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
  • In order to implement AMSI, the AV vendor first needs to registry the COM object by creating an associated CLSID entry under the HKLM\CLSID registry key.
  • Then they need to registry the same CLSID under Computer\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. The amsi.dll can be found in the folder C:\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 the AmsiScanString 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.

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 API
  • amsiContext -> of type HAMSICONTEXT is a variable is passed to all subsequent API calls and we see this being passed to AmsiInitialize 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 by AmsiOpenSession
  • 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:

  1. AmsiInitialize -> First the AmsiInitialize API is called to create a common amsiContext of type HAMSICONTEXT for the particular AMSI API caller. This context will be used by the subsequent API calls like AmsiOpenSession
  2. AmsiOpenSession -> As already mentioned next API that is called will be AmsiOpenSession. This will take the amsiContext and create a new amsiSesssion which will then be used for a particular set of scan
  3. AmsiScanBuffer -> 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 the amsiContext which was created by AmsiInitialize
  • Then it takes the pointer to the buffer containing our required input
  • length parameter holds the length of the buffer that we want to enter
  • contentName 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 the AmsiOpenSession 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
} ;
  1. AmsiCloseSession -> Lastly we will have this API to close the current AMSI scanning session. It requires both the amsiContext and the amsiSession.

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.