Windows PEB parsing – A binary with no imports

We’re going to see how a program can parse the PEB to recover Kernel32.dll address, and then load any other library. Not a single import is needed !


Writing my PE packer (see tutorial HERE), I managed to drastically reduce the number of imported functions a packed binary needs. I ended up with 5 :

  • LoadLibrary
  • GetProcAddress
  • GetModuleHandle
  • VirtualAlloc
  • VirtualProtect

Now the last 3 can be easily removed from the import table, by retrieving them using LoadLibrary and GetProcAddress. Those 2 functions can be used together to import any single other function from the system. My question is: is it possible to remove even them ? Can we import external functions without using any already imported ones ?

We would need to determine LoadLibrary and GetProcAdress addresses at some point. To do so we would need to know where Kernel32.dll is loaded, but with ASLR, we have no way of knowing any library location.

General solution

The solution lies in an OS structure in memory. When loading the process, the OS maintains 2 structures :

  • The PEB, for “Process Environment Block”, one by process as the names suggests.
  • The TEB, for “Thread Environment Block”, one for each thread.

The PEB contains a list of loaded modules, and their addresses. So we can go through this list, and find the address of Kernel32.dll, which is always loaded by the system, even if not explicitely imported by the binary.
Once we have this address, we will just find LoadLibrary and GetProcAddress addresses, and then we can use them to load anything else we want.

So basically, here is what we need to achieve:

  1. Get the PEB.
  2. Get the address of Kernel32.dll, from the module list in the PEB.
  3. Find GetProcAddress and LoadLibrary in Kernel32.dll.
  4. Use those 2 functions to load anything else.

The code below will be targeting an x86 architecture and compiled with Mingw32. We’ll compile with this command:

gcc.exe noimport.c -o noimport -nostartfiles -nostdlib "-Wl,--entry=__start" -masm=intel

It removes any standard libraries (and so every default imports) and defines the assembly syntax to intel (instead of AT&T).

Parsing the PEB

To begin with, we want to retrive a loaded module address from its name : that would be the GetModuleHandle function. As names are stored in widechar strings in memory, we’ll make a wide string version:

void* myGetModuleHandleW(WCHAR* module_name) {

PEB structures

First thing we are going to need is to get the PEB address. We can’t have it directly, but it is stored in the TEB, which is very easy to get, it’s pointed to by the fs segment register. Here is the TEB structure (source) :

typedef struct _TEB {
  PVOID Reserved1[12];
  PPEB  ProcessEnvironmentBlock;
  PVOID Reserved2[399];
  BYTE  Reserved3[1952];
  PVOID TlsSlots[64];
  BYTE  Reserved4[8];
  PVOID Reserved5[26];
  PVOID ReservedForOle;
  PVOID Reserved6[4];
  PVOID TlsExpansionSlots;

We want the ProcessEnvironmentBlock field, at offset 0x30. Reading this field is a matter of a single inline assembly line (in intel syntax, thanks to the -masm=intel compilation option) :

PEB* PEB_ptr = NULL;
    "mov %[PEB_ptr], fs:[0x30];"
    : [PEB_ptr] "=r" (PEB_ptr)
    : :

Now the PEB structure is the following one (source):

typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;
  //we won't be using anything below here

We are going to use the field Ldr, pointing to a PEB_LDR_DATA struct, as follow (source):

typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList;

There are many more fields to this structure, the MSDN documentation is far from being complete. You can find a more complete description here, but the default Windows headers in MingW are enough for now.
Now this InMemoryOrderModuleList is the field we are looking for, it’s a bi-directional linked list (source):

typedef struct _LIST_ENTRY {
  struct _LIST_ENTRY *Flink;
  struct _LIST_ENTRY *Blink;

Each list element points on a LDR_DATA_TABLE_ENTRY, which contains all the data we’re going to need concerning a loaded module. Now this structure is incomplete in both the documentation on MSDN, and the headers included with most compiler (like Mingw, which I’m using). Here is a more complete one (source):

     LIST_ENTRY InLoadOrderLinks;
     LIST_ENTRY InMemoryOrderLinks;
     LIST_ENTRY InInitializationOrderLinks;
     PVOID DllBase;
     PVOID EntryPoint;
     ULONG SizeOfImage;
     UNICODE_STRING FullDllName;
     UNICODE_STRING BaseDllName;
     ULONG Flags;
     // won't be using anything below this point

We can rename it with _COMPLETED to avoid conflicts with the default headers. Now the important thing to note is that the InMemoryOrderLinks (the linked list elements) points towards one another, we’ll see that again below.

Going through the PEB

So here is the code to go through every LDR_DATA_TABLE_ENTRY structures. We’ll use some include for the types and structures:

#include <windows.h>
#include <winnt.h>
#include <winternl.h>

We’ll start by getting the first linked list item, and define some objects we’ll be using:

PEB_LDR_DATA* peb_ldr_data = PEB_ptr->Ldr;
LIST_ENTRY* list_head = &(peb_ldr_data->InMemoryOrderModuleList);
LIST_ENTRY* list_entry;

Then, we can simply move from item to item in the list. But be aware that it’s circular, so don’t expect a NULL pointer at the “end” : we’ll stop once we come back to the first item.

for(list_entry = list_head->Flink; list_entry != list_head; list_entry = list_entry->Flink){

We actually start by the “second” item (list_head->Flink), as the head is not a LDR_DATA_TABLE_ENTRY, but it points to one.
Now for each element, we’re going to retrieve the corresponding LDR_DATA_TABLE_ENTRY. But we need to account for the fact that the LIST_ENTRY we are using all points toward the InMemoryOrderLinks fields, not the begining of the structure:

ldr_entry = (LDR_DATA_TABLE_ENTRY_COMPLETED*) ((char*)list_entry - sizeof(LIST_ENTRY));

After that, we just have to compare the name of the current module with the name we are looking for and we’re done :

WCHAR* name = ldr_entry->BaseDllName.Buffer;
if(wstring_cmp_i(module_name, name)) {
    return ldr_entry->DllBase;

The function wstring_cmp_i should compare 2 wide strings, and be case insensitive. Here is my implementation for reference:

int wstring_cmp_i(WCHAR* cmp, WCHAR* other) {
    /* Case insensitive Wstring compare, cmp must be lowercase */
    WORD* w_cmp = (WORD*) cmp;
    WORD* w_other = (WORD*) other;
    while(*w_other != 0) {
        WORD lowercase_other = ( (*w_other>='A' && *w_other<='Z')
                                 ? *w_other - 'A' + 'a'
                                 : *w_other);
        if(*w_cmp != lowercase_other) {
            return 0;
        w_cmp ++;
        w_other ++;
    return (*w_cmp == 0);

First step is over, we can call our myGetProcAddress(L"kernel32.dll") to get the address where Kernel32.dll has been loaded.

Parsing the exports

Now we want, given a module address, to find the address of an exported function. That would be GetProcAddress :

void* myGetProcAddress(char* module, char* search_name){

Note that module is a char* here, to simplify pointers arithmetic.
We’re going to parse the module PE header, to get to its export table:

IMAGE_NT_HEADERS* p_NT_HDR = (IMAGE_NT_HEADERS*) (((char*) p_DOS_HDR) + p_DOS_HDR->e_lfanew);

Now the structure of the export table in a PE file is a complex one. There are 3 arrays to consider together:

  • AddressOfNames : contains strings with the function names.
  • AddressOfFunctions : contains the RVA of the exported function.
  • AddressOfNameOrdinals : links the arrays of names with the array of functions RVA by index.

It would be very hard to explain clearly how it’s done, but the code is actually quite simple:

// Get the arrays based on their RVA in the IMAGE_EXPORT_DIRECTORY struct
DWORD* names_RVA_array = (DWORD*) (module + export_directory->AddressOfNames);
DWORD* function_RVA_array = (DWORD*) (module + export_directory->AddressOfFunctions);
WORD* name_ordinals_array = (WORD*) (module + export_directory->AddressOfNameOrdinals);

//Then for each function
for(int i=0; i< export_directory->NumberOfFunctions; ++i) {
    // Get the functions ordinal, name and code RVA
    //DWORD exported_ordinal = name_ordinals_array[i] + export_directory->Base;
    char* funct_name = module + names_RVA_array[i];
    DWORD exported_RVA = function_RVA_array[name_ordinals_array[i]];

    if(string_cmp(search_name, funct_name)){
        return (void*) (module + exported_RVA);

You’ll need to write your own version of strcmp, which I called string_cmp in my code.

Now we are done, given a module address, we can retrieve a function address based on its name. Let’s put everything together.

Final program

We’re going to compile with no standard library, which means no runtime environment, so we’ll call our entrypoint _start. We start by getting Kernel32.dll address, using our own version of GetModuleHandle :

int _start(){
    void* kernel32_dll = myGetModuleHandleW(L"kernel32.dll");

Now we could use our own GetProcAddress all along, but I prefer finding and using the real one (which handles more cases that our version, like import by ordinal for example). We retrieve it using our own version:

void* GetProcAddress_addr = myGetProcAddress(kernel32_dll, "GetProcAddress");
void* (__stdcall *GetProcAddress)(void*,char*) = myGetProcAddress(kernel32_dll, "GetProcAddress");

We need to explicite the calling convention __stdcall, as MingW defaults to __cdecl.
We only miss LoadLibrary, which we’ll get using the real GetProcAddress:

void* (__stdcall *LoadLibraryA)(char*) = GetProcAddress(kernel32_dll, "LoadLibraryA");

And that’s it, we have everything we need. We can load any library and get any function inside. Let’s open a message box for example:

void* User32_dll = LoadLibraryA("user32.dll");
int (__stdcall *MessageBoxA)(void*, char*, char*, int) = GetProcAddress(User32_dll, "MessageBoxA");
MessageBoxA(NULL, "Hello, with no imports", "Hello world", 0);


The final code can be found here:

So we have a message box here, nothing fancy. But let us have a look at our binary headers, with CFF explorer:

Import directory

MingW still produced and import directory entry, and an .idata section, but if we look at the content:

.idata with no imports

This section is completely empty! We could remote it, and set the import directory to zero, it would be the same.

Our goal is achieved, we have a binary with no imports, and it still manages to use external libraries. We could put anything in the import table to appear legitimate. From the outside: nothing dangerous would be loaded, and nothing more could be imported (as LoadLibrary would not be imported). So that would seem pretty safe … But in fact, we can load anything ! This is a simple trick to hide functionalities in a binary.

Leave a Reply

Your email address will not be published.