The Windows Antimalware Scan Interface (AMSI) 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
Example
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.
Area of focus
Microsoft is kind enough to document some of the exposed APIs in "amsi.dll". The AmsiResultIsMalware page mentions that the AMSI_RESULT is returned by AmsiScanBuffer or AmsiScanString. We therefore turn our focus to these two functions.
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.
What we care about is the last argument where the AMSI_RESULT 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 AmsiResultIsMalware to determine if this is the case."
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.
How do we get AmsiScanBuffer to return a value lower than 32768?
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.
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.
A slightly different approach is used by rastamouse. I would suggest reading his article on how to patch amsi.
Writing the Patch
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 process injection & shellcode runner.
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.
We call our PatchLocal function to write 0xc3 at the beginning of AmsiScanBuffer
Patching remote process
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.
// Write a patch on a remote processfuncPatchRemote(pid uint32, address uintptr, patch []byte) error {// Get handle on remote process pHandle, err := windows.OpenProcess( windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION,false, pid)if err !=nil {return fmt.Errorf("[ERROR] Unable to get a handle on process %d, %v", pid, err) }// Write to process memoryvar numberOfBytesWritten uintptr err = windows.WriteProcessMemory( pHandle, address,&patch[0],uintptr(len(patch)),&numberOfBytesWritten)if err !=nil {return fmt.Errorf("[ERROR] WriteProcessMemory failed, %v", err) } fmt.Printf("[+] Wrote patch at destination address 0x%x\n", address)returnnil}
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: