PEB Parsing & API Hashing

Malware technique

Published on


Introduction

In this article, we will explore how API hashing and PEB parsing techniques and how to bypass them. This pair of techniques is used to dynamically resolve the address of a function. It is commonly employed in malicious programs with the aim of evading antivirus program analysis and slowing down malicious code analysts. These techniques make the code significantly more challenging to reverse engineer.

From a static analysis perspective, by observing the loaded libraries during execution, we can identify the functions used by the program. In the screenshot below, we are dealing with a program that does not employ the PEB parsing technique:
Image
Now, when we observe the same program, we notice that the functions VirtualAlloc, VirtualAllocEx, and WriteProcessMemory are missing. This absence is due to the use of the PEB parsing technique:
Image
We will notice a difference in the functions loaded in the KERNEL32.DLL library. Indeed, VirtualAlloc, VirtualAllocEx, and WriteProcessMemory functions are no longer statically visible. Antivirus programs and analysts will pay particular attention to programs with unclear origins and those that use a chain of functions for memory injection.

As a reminder, the VirtualAlloc function is used to allocate a block of memory of a specified size during its call. In the context of shellcode injection, we ensure to specify the size of the shellcode when allocating memory. The WriteProcessMemory function is responsible for writing the shellcode (stored in a variable) into the previously allocated memory block by VirtualAlloc.


Explanation

To enhance our understanding of the PEB parsing technique, it is crucial to explore concepts such as the definition of TEB (Thread Environment Block) and PEB (Process Environment Block) structures, as well as how Windows programs manage resources.


TEB / PEB structure

The TEB (Thread Environment Block) and the PEB (Process Environment Block) are data structures related to the Windows operating system. These structures enable the proper functioning of a process and a thread, containing a set of information. For example, in the PEB, we find execution parameters, environment variables, handles, loaded modules, etc. As for the TEB, it holds information related to a specific thread of the currently executing process, such as its state, exception handling information, etc.

The PEB is created when the process starts and is stored in the process's memory. The TEB is created for each running thread and is stored within the memory of the process to which the thread belongs.
Image
Our focus is on the modules loaded into memory when a process is executed. By default, the program will load at least these modules in the following order: NTDLL.DLL, KERNEL32.DLL and KERNELBASE.DLL


Modules loaded in memory

The goal is to retrieve loaded modules in memory by navigating through various data structures. We access TEB to get PEB's address, then locate Ldr (a pointer to _PEB_LDR_DATA). Inside _PEB_LDR_DATA, InInitializationOrderModuleList points to _LIST_ENTRY (a double-linked list) for traversing modules. Data from this structure helps display module information via _LDR_DATA_TABLE_ENTRY.

Here is a summary diagram of the path to follow:
Image
To provide further clarity to the explanation above, we will repeat the steps one by one using the example of the notepad.exe program. First, we will open WinDBG and then attach it to the notepad.exe process to execute our first command dt _TEB @$teb, where the variable @$teb holds the address of the TEB:
Image
Next, we will display the structure of the PEB by specifying its address (we could have directly used the command "dt _PEB @$peb," but we will maintain this logic to apply it in the code):
Image
Next, we will retrieve the _PEB_LDR_DATA structure, which contains the InInitializationOrderModuleList structure at offset + 0x1c:
Image
The InInitializationOrderModuleList structure is a double-linked list that connects all modules together. This data structuring method allows for the ordered loading of modules. Here is what the state of the structure looks like for the first module:
Image
If we want to know the loaded module, we simply need to access the _LDR_DATA_TABLE_ENTRY structure with the correct alignment (to achieve this, we need to subtract 0x10):
Image
If we want to know the second loaded module, we proceed as follows:
Image
When we want to retrieve the parameter containing the module name, we need to be aware that when we display the _LDR_DATA_TABLE_ENTRY structure we have an alignment problem. To remedy this, we need to remove 0x10, allowing us to display the structure with the correct data. Here's a diagram illustrating our position:
Image

  • The module's name is contained in the BaseDllName parameter, which is located at [ADDR + 0x20]
  • The base address of the module is contained in the DllBase parameter, which is located at [ADDR + 0x08]

Regarding the BaseDllName parameter, the value we would retrieve corresponds to the one located at offset 0x30 of the _LDR_DATA_TABLE_ENTRY structure, which is at [_LDR_DATA_TABLE_ENTRY+0x30]. In the image above, we were misled by the BaseDllName parameter indicating its value at position [_LDR_DATA_TABLE_ENTRY+0x2c]. However, if we look at the structure to which it points, we will realize that we need to add +0x4.
Image
To verify, here is the value contained at [ADDR + 0x20]:
Image
We can define the data structuring method as the diagram below:
Image
After displaying the elements of the _LDR_DATA_TABLE_ENTRY structure, we can retrieve the symbols of a module using DllBase. What we should note is that each element of a module is located at a specific offset relative to it, which we can refer to as a relative virtual address. The data structuring within modules remains consistent for all modules.

The distinguishing factor between modules lies in the value of DllBase. By adding DllBase and the relative virtual address, we obtain the real virtual address of the desired data.

Below is the diagram of the structures allowing us to identify the loaded symbols within the modules:
Image
For example, let's take the module KERNEL32.DLL. We will proceed step by step from WinDBG. Let's start by retrieving the signature at offset 0x3c:
Image
Next, we retrieve the value contained at this address and add it to the value of DllBase. Then, we add an offset of 0x78 to reach the relative virtual address (RVA) pointing to the IMAGE_EXPORT_DIRECTORY structure. To access the structure for the KERNEL32.DLL module, we only need to add the value of DllBase to obtain the actual virtual address (VA):
Image
Now that we have the virtual address of the export table, we can retrieve two values:

  • The number of symbols
  • The pointer to the relative address of the symbol list

WinDBG output:
Image
To obtain the addresses containing the names of the symbols, we will perform the following calculation: [VA symbol list + symbol number - i * 4] + DllBase:
Image
Finally, obtaining the addresses of the symbols will be a two-step process. First, we need to revisit the IMAGE_EXPORT_DIRECTORY structure where we will point to the Ordinal Table RVA parameter. This parameter is essential as it acts as an index to obtain the addresses of the symbols:
Image
Continuing within the IMAGE_EXPORT_DIRECTORY structure, we will then point to the Address Table RVA parameter. This parameter, in turn, points to the EXPORT Address Table, which contains the addresses of the symbols:
Image
Here is the relationship between EXPORT Name Pointer Table, EXPORT Ordinal Table, and EXPORT Address Table in diagram form:
Image
Here the pseudocode adapted to this relationship:

eax = eax + ((ecx - 0x1) * 0x4) // EXPORT Name Pointer Table
eax = eax + ((ecx - 0x1) * 0x2) // EXPORT Ordinal Table
edx = edx + (eax * 0x4)         // EXPORT Address Table

Hash correspondence

Now, we know how modules are loaded into a process's memory and how to retrieve the addresses of the symbols we are interested in. The goal of the PEB parsing is to dynamically load modules and their symbols stealthily, so it is not desirable to include character strings in our program to test for equality. However, as a first step, we can calculate the hash of a character string, specifically the desired symbol. In a second step, we will calculate the hash of the symbol names that will be enumerated. This is the API Hashing technique.

Here is the function that allows us to calculate the hash of the current symbol (the esi register holds the name of the current symbol being processed, the ebx register contains the value of DllBase, and the edi register contains the IMAGE_EXPORT_DIRECTORY structure):

hashing:               
    lodsb                        ; Load the next byte from esi into al
    test al, al                  ; Check for NULL terminator
    jz compare                   ; Jump into the compare function once the string is read
    ror edx, 0x0d                ; Rotate edx 13 bits to the right
    add edx, eax                 ; Add the new byte into the accumulator
    jmp hashing                  ; Next iteration

compare:          
    cmp edx, [esp+0x24]          ; Compare the calculated hash with the hash provided
    jnz enumeration              ; If it doesn't match go back to enumeration function
    mov edx, [edi+0x24]          ; AddressOfNameOrdinals RVA
    add edx, ebx                 ; AddressOfNameOrdinals VMA
    mov cx, [edx+2*ecx]          ; Extrapolate the function's ordinal
    mov edx, [edi+0x1c]          ; AddressOfFunctions RVA
    add edx, ebx                 ; AddressOfFunctions VMA
    mov eax, [edx+4*ecx]         ; Get the function RVA
    add eax, ebx                 ; Get the function VMA
    ret

 
This approach allows us to reference only the hashes of the functions we want to invoke. Let's take a closer look at a malicious program using this technique.

In its first function, the program will call the function sub_4014C0 twice, passing the hexadecimal values 0x48317727 and 0x13B56D18 as arguments, respectively. We can assume that these hexadecimal values refer to the hashes of the requested functions.
Here is the code of the first function of the program:
Image
In the function sub_4014C0, we can recognize the symbol enumeration and a call to the function sub_401460. This latter function is responsible for hashing the character string passed as an argument:
Image
At this point, one might naively assume that we can access the hashing function and retrieve the byte used for the ROR operation. It is noted here that the value 0x0d is used for this operation. However, even after modifying our Python hashing program to test all the symbols of multiple modules, we still cannot find the function that is attempting to be loaded:
Image
Returning to the previous function, we notice a XOR operation with the hexadecimal value 0xd99ebd73, followed by a conditional jump:
Image
The XOR operation is performed with the return value of the hashing function, and if the result is equal to the hexadecimal value sent as an argument to the function call, the program will retrieve the address of the function. We can define the pseudocode as follows:

for ( DWORD i = 0; i < (DWORD)NumberOfNames; i++ ) {
    unsigned int hash = apihashing(pFunctionName);
    if ( hash == ( hash ^ 0xd99ebd73 ) ) {
        PEsignature = BaseAddr + 0x3c;

        IMAGE_EXPORT_DIRECTORY_RVA = BaseAddr + PEsignature + 0x78;
        IMAGE_EXPORT_DIRECTORY_VMA = BaseAddr + IMAGE_EXPORT_DIRECTORY_RVA;

        OrdinalTable_RVA = IMAGE_EXPORT_DIRECTORY_VMA + 0x24;
        OrdinalTable_VMA = BaseAddr + OrdinalTable_RVA;

        index = OrdinalTable_VMA + 2 * i;

        FunctionTable_RVA = IMAGE_EXPORT_DIRECTORY_VMA + 0x1c;
        FunctionTable_VMA = BaseAddr + FunctionTable_RVA;
        
        FunctionAddr_RVA = FunctionTable_VMA + 4 * index;
        FunctionAddr_VMA = BaseAddr + FunctionAddr_RVA;
    }
}

 
The second hexadecimal value acts as a key. We notice that this key is hardcoded, which means it is also present during its second call in the first function of the malware. Therefore, we can assume that: [First hexadecimal value pushed in argument] ^ [hexadecimal key 0xd99ebd73] = real hash. Upon verification, we observe that the first function attempting to be called is VirtualAlloc:
Image
We can easily verify by letting the program run until the next 'call eax' instruction is executed:
Image
Finally, here is a summary of the steps to follow in order to thwart this technique:

  • - Identify the moment when the PEB is retrieved via the fs segment at offset +0x30.
  • - Identify the moment when the ordinal table (offset +0x24) and the address table (offset +0x1c) are used, this is the point where the desired function is found.
  • - Identity the next instruction call [register] (in our case it was call eax).

Conclusion

PEB Parsing and API Hashing are techniques frequently employed by malicious programs, but their implementation is not as straightforward as demonstrated in this article. Additionally, in certain programs, we could observe the presence of anti-debugging and anti-VM mechanisms associated with these techniques. Therefore, it is crucial to analyze the program step by step in order to understand its meaning.