Tutorial Process Environment Block (PEB) and LdrEnumerateLoadedModules

D

Deleted member 65228

Guest
#1

Introduction

I decided to make an example on using LdrEnumerateLoadedModules (NTDLL). It is an undocumented function which is called by LoadLibraryEx, and it will walk through the loaded modules in the process and pass the data to a callback routine - it finds the loaded modules through the Process Environment Block (PEB). Every process has its own Process Environment Block, and this contains data regarding that process.

You can find the Process Environment Block with a routine called NtCurrentTeb().

Code:
PTEB NtCurrentTeb(
   void
);
It returns a PTEB (TEB*) which would be the Thread Environment Block (TEB).

Code:
typedef struct _TEB
{
    NT_TIB NtTib;
    PVOID EnvironmentPointer;
    CLIENT_ID ClientId;
    PVOID ActiveRpcHandle;
    PVOID ThreadLocalStoragePointer;
    PPEB ProcessEnvironmentBlock;
    ULONG LastErrorValue;
    ULONG CountOfOwnedCriticalSections;
    PVOID CsrClientThread;
    PVOID Win32ThreadInfo;
    ULONG User32Reserved[26];
    ULONG UserReserved[5];
    PVOID WOW32Reserved;
    ULONG CurrentLocale;
    ULONG FpSoftwareStatusRegister;
    VOID * SystemReserved1[54];
    LONG ExceptionCode;
    PACTIVATION_CONTEXT_STACK ActivationContextStackPointer;
    UCHAR SpareBytes1[36];
    ULONG TxFsContext;
    GDI_TEB_BATCH GdiTebBatch;
    CLIENT_ID RealClientId;
    PVOID GdiCachedProcessHandle;
    ULONG GdiClientPID;
    ULONG GdiClientTID;
    PVOID GdiThreadLocalInfo;
    ULONG Win32ClientInfo[62];
    VOID * glDispatchTable[233];
    ULONG glReserved1[29];
    PVOID glReserved2;
    PVOID glSectionInfo;
    PVOID glSection;
    PVOID glTable;
    PVOID glCurrentRC;
    PVOID glContext;
    ULONG LastStatusValue;
    UNICODE_STRING StaticUnicodeString;
    WCHAR StaticUnicodeBuffer[261];
    PVOID DeallocationStack;
    VOID * TlsSlots[64];
    LIST_ENTRY TlsLinks;
    PVOID Vdm;
    PVOID ReservedForNtRpc;
    VOID * DbgSsReserved[2];
    ULONG HardErrorMode;
    VOID * Instrumentation[9];
    GUID ActivityId;
    PVOID SubProcessTag;
    PVOID EtwLocalData;
    PVOID EtwTraceData;
    PVOID WinSockData;
    ULONG GdiBatchCount;
    UCHAR SpareBool0;
    UCHAR SpareBool1;
    UCHAR SpareBool2;
    UCHAR IdealProcessor;
    ULONG GuaranteedStackBytes;
    PVOID ReservedForPerf;
    PVOID ReservedForOle;
    ULONG WaitingOnLoaderLock;
    PVOID SavedPriorityState;
    ULONG SoftPatchPtr1;
    PVOID ThreadPoolData;
    VOID * * TlsExpansionSlots;
    ULONG ImpersonationLocale;
    ULONG IsImpersonating;
    PVOID NlsCache;
    PVOID pShimData;
    ULONG HeapVirtualAffinity;
    PVOID CurrentTransactionHandle;
    PTEB_ACTIVE_FRAME ActiveFrame;
    PVOID FlsData;
    PVOID PreferredLanguages;
    PVOID UserPrefLanguages;
    PVOID MergedPrefLanguages;
    ULONG MuiImpersonation;
    WORD CrossTebFlags;
    ULONG SpareCrossTebBits: 16;
    WORD SameTebFlags;
    ULONG DbgSafeThunkCall: 1;
    ULONG DbgInDebugPrint: 1;
    ULONG DbgHasFiberData: 1;
    ULONG DbgSkipThreadAttach: 1;
    ULONG DbgWerInShipAssertCode: 1;
    ULONG DbgRanProcessInit: 1;
    ULONG DbgClonedThread: 1;
    ULONG DbgSuppressDebugMsg: 1;
    ULONG SpareSameTebBits: 8;
    PVOID TxnScopeEnterCallback;
    PVOID TxnScopeExitCallback;
    PVOID TxnScopeContext;
    ULONG LockCount;
    ULONG ProcessRundown;
    UINT64 LastSwitchTime;
    UINT64 TotalSwitchOutTime;
    LARGE_INTEGER WaitReasonBitMap;
} TEB, *PTEB;
(Source: struct TEB - this is outdated and from Windows Vista but perfectly fine for a demonstration).

You may notice the 6th item down on the TEB structure called ProcessEnvironmentBlock (PPEB). That is the Process Environment Block (PEB) of the current process, and it is required to use LdrEnumerateLoadedModules because the data regarding what modules are loaded are contained within the PEB.

The function LdrEnumerateLoadedModules is exported by NTDLL (on modern versions of Windows at-least) and takes in three (3) parameters. Below is an example function prototype.

Code:
typedef NTSTATUS(NTAPI *pLdrEnumerateLoadedModules)(
    BOOLEAN Flags,
    PLDR_ENUM_CALLBACK EnumerateCallback,
    PVOID Context
    );
The second parameter passed into the function is for the callback routine which will receive the data over a period of multiple invocations. The first time the callback is invoked, the module data will be the first during enumeration; as the callback is invoked on and on until the end of the list of modules which was found within the PEB, the entry accessible within the callback routine also increases. Due to this, we can simply print the Buffer of FullDllName (UNICODE_STRING entry under LDR_DATA_TABLE_ENTRY) and each loaded module will be listed, we won't have to loop through any lists or increment values ourselves because the OS handles this for us.

Within the Process Environment Block, by default there is an entry named Ldr (PPEB_LDR_DATA). Within this Ldr (pointer structure to PEB_LDR_DATA) there is an entry named InMemoryOrderModuleList. Each item on the list for this has a data-type of PLDR_DATA_TABLE_ENTRY.

For more information about this, you can check the Microsoft documentation (they do surprisingly have a page about this): PEB_LDR_DATA structure (Windows)


The code example for LdrEnumerateLoadedModules is below.

stdafx.h
Code:
#pragma once
#include <Windows.h>
#include <cstdio>

#include "winternl.h"
#include "ntldr.h"
ntldr.h
Code:
#pragma once
#include "stdafx.h"

BOOL EnumerateProcessModules();

typedef VOID(NTAPI LDR_ENUM_CALLBACK)(
    PLDR_DATA_TABLE_ENTRY ModuleInformation,
    PVOID Parameter,
    BOOLEAN *Stop
    );

typedef LDR_ENUM_CALLBACK* PLDR_ENUM_CALLBACK;
main.cpp
Code:
#include "stdafx.h"
using namespace std;

int main()
{
    BOOL EnumerateStatus = EnumerateProcessModules();

    if (!EnumerateStatus)
    {
        printf("The modules could not be enumerated\n");
    }

    getchar();
    return 0;
}
ntldr.cpp
Code:
#include "stdafx.h"
using namespace std;

typedef NTSTATUS(NTAPI *pLdrEnumerateLoadedModules)(
    BOOLEAN Flags,
    PLDR_ENUM_CALLBACK EnumerateCallback,
    PVOID Context
    );

pLdrEnumerateLoadedModules fLdrEnumerateLoadedModules;

VOID NTAPI EnumerateCallback(
    PLDR_DATA_TABLE_ENTRY DataEntry,
    PVOID Context,
    BOOLEAN *Stop
)
{
    if (DataEntry)
    {
        printf("%ws\n", DataEntry->FullDllName.Buffer);
    }
}

BOOL EnumerateProcessModules()
{
    NTSTATUS NtStatus;
    PPEB ProcessEnvironmentBlock = NtCurrentTeb()->ProcessEnvironmentBlock;
    HMODULE hNtdll = GetModuleHandle("ntdll.dll");

    fLdrEnumerateLoadedModules = (pLdrEnumerateLoadedModules)GetProcAddress(hNtdll,
        "LdrEnumerateLoadedModules");

    if (!fLdrEnumerateLoadedModules ||
        !ProcessEnvironmentBlock)
    {
        return FALSE;
    }

    NtStatus = fLdrEnumerateLoadedModules(0,
        (PLDR_ENUM_CALLBACK)EnumerateCallback,
        ProcessEnvironmentBlock->ImageBaseAddress);

    if (!NT_SUCCESS(NtStatus))
    {
        return FALSE;
    }

    return TRUE;
}
One note: you'll need to include winternl.h unless you already have sufficient definitions for what it has depending on your needs. I copied the contents into another header file and performed modifications for the Process Environment Block structure. The one from Nirsoft works fine, the one by default doesn't provide the ImageBaseAddress entry which is also required for the call (taken from the PPEB received beforehand).


Addition

You can also manually loop through the modules loaded in the process after acquiring the Process Environment Block. Below is some example code.

Code:
    PLIST_ENTRY ModuleList = &ProcessEnvironmentBlock->Ldr->InMemoryOrderModuleList;
    PLIST_ENTRY ForwardLink = ModuleList->Flink;
    PLDR_DATA_TABLE_ENTRY DataEntry = { 0 };

    while (ModuleList != ForwardLink)
    {
        DataEntry = CONTAINING_RECORD(ForwardLink,
            LDR_DATA_TABLE_ENTRY,
            InMemoryOrderLinks);

        if (DataEntry)
        {
            printf("%ws\n", DataEntry->FullDllName.Buffer);
        }

        ForwardLink = ForwardLink->Flink;
    }
There are many ways to obtain the Process Environment Block however one of the most convenient ways is through NtCurrentTeb(), which is extremely simple and quick to use. You can use inline Assembly to locate the Process Environment Block (e.g. targeting FS:[0x30] ), macro's such as __readfsdword (also targeting 0x30) (32-bit) and __readgsqword (targeting 0x60), however I personally think that it is more convenient to just use NtCurrentTeb().

You can use the technique mentioned above (manual list handling) (example above) over LdrEnumerateLoadedModules to make a GetModuleHandle replacement with some changes. You'll just need to find the base address of the module instead of printing the FullDllName Buffer. To find the base address, reference the DllBase entry (data-type of PVOID) instead. Simply type-cast the PVOID to HMODULE and you'll be good to go. ;)

Code:
HMODULE GetModuleHandleAlternate(
    WCHAR *ModuleName
)
{
    PPEB ProcessEnvironmentBlock = NtCurrentTeb()->ProcessEnvironmentBlock;
    PLIST_ENTRY ModuleList = { 0 };
    PLIST_ENTRY ForwardLink = { 0 };
    PLDR_DATA_TABLE_ENTRY DataEntry = { 0 };

    if (ProcessEnvironmentBlock)
    {
        ModuleList = &ProcessEnvironmentBlock->Ldr->InMemoryOrderModuleList;
        ForwardLink = ModuleList->Flink;

        if (ModuleList && ForwardLink)
        {
            while (ModuleList != ForwardLink)
            {
                DataEntry = CONTAINING_RECORD(ForwardLink,
                    LDR_DATA_TABLE_ENTRY,
                    InMemoryOrderLinks);

                if (DataEntry)
                {
                    if (!wcscmp(DataEntry->BaseDllName.Buffer, ModuleName))
                    {
                        return (HMODULE)DataEntry->DllBase;
                    }
                }

                ForwardLink = ForwardLink->Flink;
            }
        }
    }

    return 0;
}
Note that you'll have to add another UNICODE_STRING entry under LDR_DATA_TABLE_ENTRY structure on your own definition for BaseDllName under FullDllName entry. This is done so you can pass "ntdll.dll", "kernel32.dll", "user32.dll" etc, instead of the full image path on disk.

Code:
typedef struct _LDR_DATA_TABLE_ENTRY {
        PVOID Reserved1[2];
        LIST_ENTRY InMemoryOrderLinks;
        PVOID Reserved2[2];
        PVOID DllBase;
        PVOID Reserved3[2];
        UNICODE_STRING FullDllName;
        UNICODE_STRING BaseDllName;
        BYTE Reserved4[8];
        PVOID Reserved5[3];
#pragma warning(push)
#pragma warning(disable: 4201) // we'll always use the Microsoft compiler
        union {
            ULONG CheckSum;
            PVOID Reserved6;
        } DUMMYUNIONNAME;
#pragma warning(pop)
        ULONG TimeDateStamp;
    } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
Thanks for reading.
 
D

Deleted member 65228

Guest
#2
One quick update. You should call LdrLoadDll if the module cannot be found so it can be loaded (this would be a replacement for LoadLibrary as well).

For anyone wondering, you can hand-make the structures using WinDbg. Exg:



Attach to process and then check for ntdll.dll. You can do this for exported structures in ntdll.dll. You can also see the correct calculations for orders for the OS version. They jumble around sometimes depending on OS update and sometimes entries are removed or added depending on OS version/SP update.
 
D

Deleted member 65228

Guest
#3
Use _wcsicmp so you don't have to worry about upper/lower-case. Otherwise you cannot do "kernel32.dll" like with GetModuleHandle, you'd have to use "KERNEL32.DLL".

Updated example (small commented)

Code:
HMODULE GetModuleHandleAlternate(
    WCHAR *ModuleName
)
{
    PPEB ProcessEnvironmentBlock = NtCurrentTeb()->ProcessEnvironmentBlock; // find the PEB
    PLIST_ENTRY ModuleList = { 0 };
    PLIST_ENTRY ForwardLink = { 0 };
    PLDR_DATA_TABLE_ENTRY DataEntry = { 0 };

    //
    //    check if the PEB could be found
    //

    if (ProcessEnvironmentBlock)
    {

        //
        //  we need to find the list of modules & also keep track of the Forward Link
        //

        ModuleList = &ProcessEnvironmentBlock->Ldr->InMemoryOrderModuleList;
        ForwardLink = ModuleList->Flink;

        if (ModuleList && ForwardLink)
        {

            //
            //    go through the list of loaded modules
            //

            while (ModuleList != ForwardLink)
            {

                //
                //    retrieve the info for the current module on the list
                //

                DataEntry = CONTAINING_RECORD(ForwardLink,
                    LDR_DATA_TABLE_ENTRY,
                    InMemoryOrderLinks);

                if (DataEntry)
                {
                    if (!_wcsicmp(DataEntry->BaseDllName.Buffer, ModuleName))
                    {

                        //
                        //    we found the module we are looking for so we can retrieve the base address and return it in HMODULE form
                        //

                        return (HMODULE)DataEntry->DllBase;
                    }
                }

                ForwardLink = ForwardLink->Flink;
            }
        }
    }

    return 0;
}
 
D

Deleted member 65228

Guest
#4
You can define NtCurrentPeb() manually so you don't have to keep referencing the TEB (Thread Environment Block).

Code:
#define NtCurrentPeb() (NtCurrentTeb()->ProcessEnvironmentBlock)

...

PPEB ProcessEnvironmentBlock = NtCurrentPeb();
If you'd like to do it manually, you can use __readfsdword macro for 32-bit compilations and __readgsqword for 64-bit compilations (use a definition check to determine at run-time because you cannot compile it normally with both of them in use without one, the compiler will refuse __readgsqword for 32-bit compilations and vice versa).

32-bit example.
Code:
PPEB ProcessEnvironmentBlock = (PPEB)__readfsdword(0x30);
64-bit example.
Code:
PPEB ProcessEnvironmentBlock = (PPEB)__readgsqword(0x60);
We use 0x30 for __readfsdword because that is where the Process Environment Block is located (e.g. you can use inline Assembly with FS:[0x30] instead). For 64-bit, the address is 2* the 32-bit address (thus being 0x60).

Code:
    __asm {
        mov eax, dword ptr fs:[00000030h]
        mov dword ptr[ProcessEnvironmentBlock], eax
    }
In the above snippet we move the address of the Process Environment Block into the EAX register and then we assign the value of ProcessEnvironmentBlock (PPEB - pointer to PEB structure) to the value stored under the EAX register, which would be the address of the Process Environment Block.

I still think it is a lot more convenient to just use NtCurrentTeb()->ProcessEnvironmentBlock, especially when you can manually define NtCurrentPeb to do it for you.
 

Similar Threads

Similar Threads