Geoff Chappell, Software Analyst
The LDR_DATA_TABLE_ENTRY structure is NTDLL’s record of how a DLL is loaded into a process.
Each process has its own list of loaded modules. In some sense, it has three lists since although there is only the one LDR_DATA_TABLE_ENTRY structure for each module, each is linked in three different orders. The way to find the list is well known, including to malware. The Ldr member of the process’s PEB points to the process’s PEB_LDR_DATA which contains the list heads as InLoadOrderModuleList, InMemoryOrderModuleList and InInitializationOrderModuleList. Less well known—or less well respected in real-world practice, even by programmers who aren’t writing malware—is that the links in these lists are not safe to follow while modules might be loaded and unloaded. That this can’t happen at the time can be hard enough to ensure even for the current process.
In an ideal world, the LDR_DATA_TABLE_ENTRY might be opaque outside NTDLL. But various high-level modules supplied with Windows over the years have used at least one member of the LDR_DATA_TABLE_ENTRY, which eventually had to be disclosed. A new header, named WINTERNL.H, for previously internal APIs was added to the Software Development Kit (SDK) apparently in 2002, and remains to this day. Starting with the SDK for Windows 7, WINTERNL.H presents a modified LDR_DATA_TABLE_ENTRY that has just the InMemoryOrderLinks, DllBase, FullDllName, CheckSum and TimeDateStamp members, plus padding that gets these members to the same offsets as in the true structure. It seems unlikely that Microsoft will change the LDR_DATA_TABLE_ENTRY in any way that moves these members.
Indeed, given that LDR_DATA_TABLE_ENTRY at least started as an undocumented structure for NTDLL’s internal use, it is surprisingly stable across Windows versions. Until a significant reworking for Windows 8, the structure grew only by extension and many of the original members—which happen to be the most useful in practice—keep their same positions through the whole history. The following table shows the changing sizes:
Version | Size (x86) | Size (x64) |
---|---|---|
3.10 to 3.51 | 0x44 | |
4.0 to 5.0 | 0x48 | |
5.1 before Windows XP SP2 | 0x4C | |
5.1 from Windows XP SP2 to 5.2 | 0x50 | 0x98 |
6.0 | 0x68 | 0xC8 |
6.1 | 0x78 | 0xE0 |
6.2 | 0x98 | 0x0110 |
6.3 to 1511 | 0xA0 | 0x0118 |
1607 and higher | 0xA8 | 0x0120 |
These sizes, and the offsets, types and names in the tables that follow, are from Microsoft’s symbol files for the kernel starting with Windows 2000 SP3 and for NTDLL starting with Windows XP. Symbol files for earlier versions do not contain type information for the LDR_DATA_TABLE_ENTRY, but inspection confirms that all but one member that was in use by then had near enough the same usage as far back as Windows NT 3.10.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x00 | 0x00 |
LIST_ENTRY InLoadOrderLinks; |
3.10 and higher |
0x08 | 0x10 |
LIST_ENTRY InMemoryOrderLinks; |
3.10 and higher |
0x10 | 0x20 |
LIST_ENTRY InInitializationOrderLinks; |
3.10 to 6.1 |
union { LIST_ENTRY InInitializationOrderLinks; LIST_ENTRY InProgressLinks; }; |
6.2 and higher | ||
0x18 | 0x30 |
PVOID DllBase; |
3.10 and higher |
0x1C | 0x38 |
PVOID EntryPoint; |
3.10 and higher |
0x20 | 0x40 |
ULONG SizeOfImage; |
3.10 and higher |
0x24 | 0x48 |
UNICODE_STRING FullDllName; |
3.10 and higher |
0x2C | 0x58 |
UNICODE_STRING BaseDllName; |
3.10 and higher |
0x34 | 0x68 |
ULONG Flags; |
3.10 to 6.1 |
union { UCHAR FlagGroup [4]; ULONG Flags; struct { /* bit fields, see below */ }; }; |
6.2 and higher | ||
0x38 | 0x6C |
USHORT LoadCount; |
3.10 to 6.1 |
USHORT ObsoleteLoadCount; |
6.2 and higher | ||
0x3A | 0x6E |
USHORT TlsIndex; |
all |
0x3C | 0x70 |
union { LIST_ENTRY HashLinks; struct { PVOID SectionPointer; ULONG CheckSum; }; }; |
3.10 to 6.1 |
LIST_ENTRY HashLinks; |
6.2 and higher |
A practical reason to know of this structure is for the debugging exercise of finding why a DLL did not get unloaded when expected or did get unloaded but by surprise. Both are questions of DLL reference counting. Before Windows 8, the LoadCount member of this structure is the reference count. The LDR_DATA_TABLE_ENTRY for the DLL in question is most easily found when the DLL has just loaded. A program’s loading and unloading of the DLL can then be tracked easily by setting a write-memory breakpoint on the LoadCount member. At each break to the debugger, look at what the count has changed to and look at a stack dump to see who made the change.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x44 | 0x80 |
union { ULONG TimeDateStamp; PVOID LoadedImports; }; |
4.0 to 6.1 |
ULONG TimeDateStamp; |
6.2 and higher |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x48 | 0x88 |
PVOID EntryPointActivationContext; |
5.1 and higher |
0x4C | 0x90 |
PVOID PatchInformation; |
5.1 from Windows XP SP2 to 6.2 |
PVOID Spare; |
6.3 only | ||
PVOID Lock; |
10.0 and higher |
Insertion of the LDR_DATA_TABLE_ENTRY into three more lists for Windows Vista soon enough got undone when Windows 8 greatly reworked the tracking of DLLs as they get loaded and unloaded. These members’ positions have an entirely different use in Windows 8 and higher.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x50 (6.0 to 6.1) | 0x98 (6.0 to 6.1) |
LIST_ENTRY ForwarderLinks; |
6.0 to 6.1 |
0x58 (6.0 to 6.1) | 0xA8 (6.0 to 6.1) |
LIST_ENTRY ServiceTagLinks; |
6.0 to 6.1 |
0x60 (6.0 to 6.1) | 0xB8 (6.0 to 6.1) |
LIST_ENTRY StaticLinks; |
6.0 to 6.1 |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x50 | 0x98 |
LDR_DDAG_NODE *DdagNode; |
6.2 and higher |
0x54 | 0xA0 |
LIST_ENTRY NodeModuleLink; |
6.2 and higher |
0x5C | 0xB0 |
LDRP_DLL_SNAP_CONTEXT *SnapContext; |
6.2 to 6.3 |
LDRP_LOAD_CONTEXT *LoadContext; |
10.0 and higher | ||
0x60 | 0xB8 |
PVOID ParentDllBase; |
6.2 and higher |
0x64 | 0xC0 |
PVOID SwitchBackContext; |
6.2 and higher |
0x68 | 0xC8 |
RTL_BALANCED_NODE BaseAddressIndexNode; |
6.2 and higher |
0x74 | 0xE0 |
RTL_BALANCED_NODE MappingInfoIndexNode; |
6.2 and higher |
One addition for Windows 7 also got caught up in the reorganisation for Windows 8. Others are retained but shifted.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x68 (6.1) | 0xC8 (6.1) |
PVOID ContextInformation; |
6.1 only |
0x6C (6.1); 0x80 |
0xD0 (6.1); 0xF8 |
ULONG_PTR OriginalBase; |
6.1 and higher |
0x70 (6.1); 0x88 |
0xD8 (6.1); 0x0100 |
LARGE_INTEGER LoadTime; |
6.1 and higher |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x90 | 0x0108 |
ULONG BaseNameHashValue; |
6.2 and higher |
0x94 | 0x010C |
LDR_DLL_LOAD_REASON LoadReason; |
6.2 and higher |
If only for now, it seems the LDR_DLL_LOAD_REASON isn’t held elsewhere and may as well be enumerated here:
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x98 | 0x0110 |
ULONG ImplicitPathOptions; |
6.3 and higher |
When Windows 8 extended the LoadCount from its old 16 bits, it defined a ReferenceCount, distinct from the LoadCount, but placed it in the LDR_DDAG_NODE with the new LoadCount. Windows 10 moves it here.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x9C | 0x0114 |
ULONG ReferenceCount; |
10.0 and higher |
0xA0 | 0x0118 |
ULONG DependentLoadFlags; |
1607 and higher |
0xA4 | 0x011C |
UCHAR SigningLevel; |
1703 and higher |
Starting with version 6.2, what had just been a ULONG for Flags is elaborated formally as bit fields.
Mask | Definition | Versions |
---|---|---|
0x00000001 |
ULONG PackagedBinary : 1; |
6.2 and higher |
0x00000002 |
ULONG MarkedForRemoval : 1; |
6.2 and higher |
0x00000004 |
ULONG ImageDll : 1; |
6.2 and higher |
0x00000008 |
ULONG LoadNotificationsSent : 1; |
6.2 and higher |
0x00000010 |
ULONG TelemetryEntryProcessed : 1; |
6.2 and higher |
0x00000020 |
ULONG ProcessStaticImport : 1; |
6.2 and higher |
0x00000040 |
ULONG InLegacyLists : 1; |
6.2 and higher |
0x00000080 |
ULONG InIndexes : 1; |
6.2 and higher |
0x00000100 |
ULONG ShimDll : 1; |
6.2 and higher |
0x00000200 |
ULONG InExceptionTable : 1; |
6.2 and higher |
ULONG ReservedFlags1 : 2; |
6.2 and higher | |
0x00001000 |
ULONG LoadInProgress : 1; |
6.2 and higher |
0x00002000 |
ULONG ReservedFlags2 : 1; |
6.2 to 6.3 |
ULONG LoadConfigProcessed : 1; |
10.0 and higher | |
0x00004000 |
ULONG EntryProcessed : 1; |
6.2 and higher |
0x00008000 |
ULONG ProtectDelayLoad : 1; |
10.0 and higher |
ULONG ReservedFlags3 : 3; |
6.2 to 6.3 | |
ULONG ReservedFlags3 : 2; |
10.0 and higher | |
0x00040000 |
ULONG DontCallForThreads : 1; |
6.2 and higher |
0x00080000 |
ULONG ProcessAttachCalled : 1; |
6.2 and higher |
0x00100000 |
ULONG ProcessAttachFailed : 1; |
6.2 and higher |
0x00200000 |
ULONG CorDeferredValidate : 1; |
6.2 and higher |
0x00400000 |
ULONG CorImage : 1; |
6.2 and higher |
0x00800000 |
ULONG DontRelocate : 1; |
6.2 and higher |
0x01000000 |
ULONG CorILOnly : 1; |
6.2 and higher |
0x02000000 |
ULONG ChpeImage : 1; |
1803 and higher |
ULONG ReservedFlags5 : 3; |
6.2 to 1709 | |
ULONG ReservedFlags5 : 2; |
1803 and higher | |
0x10000000 |
ULONG Redirected : 1; |
6.2 and higher |
ULONG ReservedFlags6 : 2; |
6.2 and higher | |
0x80000000 |
ULONG CompatDatabaseProcessed : 1; |
6.2 and higher |
In earlier versions, the Flags bits are presumably defined by macros. Names and values for some are known from the !dlls command as implemented in debugger extensions (KDEXTX86.DLL in versions 3.51 and 4.0, but EXTS.DLL for Windows XP and higher):
Mask | Symbolic Name | Versions |
---|---|---|
0x00000002 | LDRP_STATIC_LINK | 3.51 to 6.1 |
LDRP_MARKED_FOR_REMOVAL | 6.2 and higher | |
0x00000004 | LDRP_IMAGE_DLL | 3.51 and higher |
0x00000008 | LDRP_SHIMENG_ENTRY_PROCESSED | 5.1 to 6.1 |
LDRP_LOAD_NOTIFICATIONS_SENT | 6.2 and higher | |
0x00000010 | LDRP_TELEMETRY_ENTRY_PROCESSED | 5.1 and higher |
0x00001000 | LDRP_LOAD_IN_PROGRESS | 3.51 and higher |
0x00002000 | LDRP_UNLOAD_IN_PROGRESS | 3.51 to 6.1 |
0x00004000 | LDRP_ENTRY_PROCESSED | 3.51 and higher |
0x00008000 | LDRP_ENTRY_INSERTED | 3.51 to 4.0 |
0x00010000 | LDRP_CURRENT_LOAD | 3.51 to 4.0 |
0x00020000 | LDRP_FAILED_BUILTIN_LOAD | 3.51 to 4.0 |
0x00040000 | LDRP_DONT_CALL_FOR_THREADS | 3.51 and higher |
0x00080000 | LDRP_PROCESS_ATTACH_CALLED | 3.51 and higher |
0x00100000 | LDRP_DEBUG_SYMBOLS_LOADED | 3.51 to 4.0 |
0x00400000 | LDRP_COR_IMAGE | 5.1 and higher |
0x00800000 | LDRP_COR_OWNS_UNMAP | 5.1 to 6.1 |
LDRP_DONT_RELOCATE | 6.2 and higher | |
0x01000000 | LDRP_COR_IL_ONLY | 5.1 and higher |
0x10000000 | LDRP_REDIRECTED | 5.1 and higher |