5. AMSI Bypass
#amsi #amsibypass #golang #maldev #malwaredevelopment
Last updated
Was this helpful?
#amsi #amsibypass #golang #maldev #malwaredevelopment
Last updated
Was this helpful?
is a versatile interface standard that allows your applications and services to integrate with any antimalware product that's present on a machine. AMSI provides enhanced malware protection for your end-users and their data, applications, and workloads.
The AMSI feature is integrated into these components of Windows 10.
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
So how does the above actually look like in real life? PowerShell will let us know when something is flagged as malicious by amsi. So let's send the string "AmsiScanBuffer" that is known to be flagged as malicious.
We then run our bypass code to see what's the effect.
After running the amsi bypass code, we send the "AmsiScanBuffer" string again but the second time instead of getting the same message that our script is malicious we get "CommandNotFoundException". We successfully bypassed AMSI.
Dumping amsi.dll in IDA shows that AmsiScanString is a wrapper function for AmsiScanBuffer. So to completely bypass amsi all we need to do is to patch AmsiScanBuffer.
Let's Have a quick look at the function definition by microsoft
This leaves us with two options, either manipulate AmsiScanBuffer to return a value less than 32768 or manipulate AmsiResultIsMalware to return that our script is not malicious even if the value is higher than 32768.
Having a quick look in the Exported functions of the dll we can see that AmsiResultIsMalware is not exported.
It is then very difficult to find the exact address of this function. We could calculate the offset from the base address of the dll and then hardcode it in our code, but it would most likely work only against our current version of windows only. Any future changes to the dll will break our bypass code.
Let's open windbg to check how we could potentially patch the dll to return a non malicious AMSI_RESULT.
Firstly we set a breakpoint to the entrypoint of the AmsiScanBuffer function using this command: bp amsi!AmsiScanBuffer
We need to find where the result is stored. We can get that from the stack as shown below:
Let's run ipconfig
in powershell and check what is the value of AMSI_RESULT at the function entry and when it returns.
00000001017cea28 is where the result will be stored. When we enter the the AmsiScanBuffer function the value is 0.
Just before the function returns the value is 1.
Let's compare the values when a malicious string is sent.
We can now see that the value 32768 and therefore the value is malicious.
It is clear now that if we return immediately after the function enters the AMSI_RESULT will be 0. This value will be anomalous since the minimum value of AMSI_RESULT is 1. But then again 0 is lower than 32768.
Since AmsiResultIsMalware doesn't validate the minimum value, our patch successfully bypasses amsi.
Since patching could be used to patch other functions such as EtwEventWrite
a function that receives 2 arguments will be written. The first argument will be the destination address and the second a byte slice that will hold the patch.
Line 5: Since the memory address will most likely have RX rights we should first assign write access to that memory using VirtualProtect .
Line 13: We then go ahead and call rtlMoveMemory to write our patch to the destination address.
Line 17: We then restore the memory permissions using VirtualProtect
The wrapper function to patch AmsiScanBuffer in the current process:
Lines 3-4: we get the address of the function
Line 6: We have the return equivalent in hex 0xc3
We call our PatchLocal function to write 0xc3 at the beginning of AmsiScanBuffer
This is useful when a c2 spawns a powershell process and feeds it commands through the c2. The dlls are loaded in the same address in every process on windows. So figuring out the address locally essentially gives us the address we need to patch on the remote process.
Once again we create a function that will receive 3 arguments. First argument will be the PID of the process to patch (in our case powershell) , the destionation address and the patch byte slice.
line 6: We get a handle on the remote process
line 16: We use WriteProcessMemorywin API to write the patch to the destination address. WriteProcessMemory takes care of the permissions so no need to use VirtualAllocEx to assign write permissions.
The AMSI patching wrapper function is shown below:
Microsoft is kind enough to document some of the exposed APIs in "amsi.dll". The page mentions that the AMSI_RESULT is returned by AmsiScanBuffer or AmsiScanString. We therefore turn our focus to these two functions.
What we care about is the last argument where the is stored. In the remarks section it is stated "Any return result equal to or larger than 32768 is considered malware, and the content should be blocked. An app should use to determine if this is the case."
A slightly different approach is used by . I would suggest reading his article on .
The hex equivalent of return is c3
. We will write the address at the entry point of the function. The approach is similar to the one of & .
This is useful when C# code will be running within our golang process for example. See .
The code includes functions to as well.