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