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


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

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
    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 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 ]
    success = ReadProcessMemory(-1, ByVal (PEBLdrAddress + 12), VarPtr(ReadBytes), Len(ReadBytes), size)
    InLoadOrderLinksStart = ReadBytes
    'Debug.Print "Start=" & Hex(InLoadOrderLinksStart)
    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
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.


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

Leave a Reply

Your email address will not be published. Required fields are marked *