64位Windows中使用PEB来获取进程加载的模块
由于项目相关,我需要使用PEB来获取进程已加载的模块,这里做一下记录。
定位到PEB
这里通过TEB定位PEB。使用gs:[0]定位到当前TEB,加上0x60偏移得到PEB。

_PEB_LDR_DATA结构体
在PEB中0x18处,定位到_PEB_LDR_DATA类型的Ldr成员。

通过wrk的源代码和windbg手动解析,得到该结构体定义如下:


简单地说,_PEB_LDR_DATA 是 Windows 进程中一个至关重要的模块管理控制中心。由操作系统在进程启动时创建和维护,专门负责记录和管理这个进程加载了哪些可执行模块。
它维护了三个关键的双向链表,记录了所有已加载到进程地址空间的模块信息(EXE本身和所有DLL)。
这三个链表指向同样的模块信息(LDR_DATA_TABLE_ENTRY),只是排序方式不同:
InLoadOrderModuleList
- 按加载顺序排列。链表头就是进程的主可执行模块(.exe文件),然后是后续按加载顺序加入的DLL。
InMemoryOrderModuleList
- 按在内存中的地址顺序排列。链表节点是基于模块在内存中映像的基地址链接起来的。
- 这种结构在某些需要快速进行地址查找的内核操作中效率更高。
InInitializationOrderModuleList
- 按初始化顺序排列。这个链表反映了模块的入口点函数(DllMain)被调用的顺序。
- 它对于理解模块的初始化和卸载依赖关系很重要。
一个模块,三个视图。每个模块(LDR_DATA_TABLE_ENTRY)都有三个“链接指针”,分别将自己挂入这三个链表中。
插叙:说说新增的两个成员
可以看到,较新版本的Windows内核中_PEB_LDR_DATA增加了两个新的成员:
- ShutdownInProgress
- ShutdownThreadId
它们主要用于进程退出时的同步与安全控制,防止在卸载模块(DLL)的过程中发生竞态条件。
这两个字段的具体作用如下:
1. ShutdownInProgress (布尔值)
- 功能:这是一个标志位(Flag),用于指示当前进程是否正处于“关闭进行中”的状态。
- 机制:当进程开始执行退出流程(如调用了 ExitProcess)时,加载器(Loader)会先将此标志置为 TRUE。
- 用途:主要用于同步。例如,在 DLL 卸载过程中,如果有其他线程试图并发地加载 DLL,加载器检测到这个标志后可以快速失败或等待,避免在模块链表处于不一致状态时进行操作,从而防止内核崩溃。
2. ShutdownThreadId (线程ID)
- 功能:明确记录是哪个线程触发了进程关闭。
- 机制:它存储了发起关闭操作(如调用 ExitProcess 或处理 WM_CLOSE 的主线程)的线程 ID。
- 用途:
- 所有权追踪:在复杂的多线程退出场景中,明确知道“谁”发起了关闭,有助于进行更精细的资源清理决策。
- 死锁预防:如果关闭线程在等待其他线程结束,而其他线程又卡在加载器锁中,内核可以利用此信息检测潜在的死锁环路,并可能采取更激进的超时或终止策略。
InLoadOrderModuleList成员
_PEB_LDR_DATA中的InLoadOrderModuleList成员,类型为内核中的双链表类型:_LIST_ENTRY。

该成员内嵌于_LDR_DATA_TABLE_ENTRY结构之中,为_LDR_DATA_TABLE_ENTRY结构第一个字段:

**我们主要利用它来获取我们需要的模块。**该成员为双链表的头节点,这个节点应该不包含任何内容,仅作为头节点使用:
0:000> dt _LDR_DATA_TABLE_ENTRY 0x7ffba53128f0
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x000002d1`a40b5e50 – 0x000002d1`a40bc5d0 ]
+0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x000002d1`a40b5e60 – 0x000002d1`a40bc5e0 ]
+0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x000002d1`a40b5bc0 – 0x000002d1`a40b6450 ]
+0x030 DllBase : (null)
+0x038 EntryPoint : (null)
+0x040 SizeOfImage : 0
+0x048 FullDllName : _UNICODE_STRING ""
+0x058 BaseDllName : _UNICODE_STRING ""
+0x068 FlagGroup : [4] "???"
+0x068 Flags : 0xc05200e8
+0x068 PackagedBinary : 0y0
+0x068 MarkedForRemoval : 0y0
+0x068 ImageDll : 0y0
+0x068 LoadNotificationsSent : 0y1
+0x068 TelemetryEntryProcessed : 0y0
+0x068 ProcessStaticImport : 0y1
+0x068 InLegacyLists : 0y1
+0x068 InIndexes : 0y1
+0x068 ShimDll : 0y0
+0x068 InExceptionTable : 0y0
+0x068 VerifierProvider : 0y0
+0x068 ShimEngineCalloutSent : 0y0
+0x068 LoadInProgress : 0y0
+0x068 LoadConfigProcessed : 0y0
+0x068 EntryProcessed : 0y0
+0x068 ProtectDelayLoad : 0y0
+0x068 AuxIatCopyPrivate : 0y0
+0x068 ReservedFlags3 : 0y1
+0x068 DontCallForThreads : 0y0
+0x068 ProcessAttachCalled : 0y0
+0x068 ProcessAttachFailed : 0y1
+0x068 ScpInExceptionTable : 0y0
+0x068 CorImage : 0y1
+0x068 DontRelocate : 0y0
+0x068 CorILOnly : 0y0
+0x068 ChpeImage : 0y0
+0x068 ChpeEmulatorImage : 0y0
+0x068 ReservedFlags5 : 0y0
+0x068 Redirected : 0y0
+0x068 ReservedFlags6 : 0y10
+0x068 CompatDatabaseProcessed : 0y1
+0x06c ObsoleteLoadCount : 0x7ff6
+0x06e TlsIndex : 0
+0x070 HashLinks : _LIST_ENTRY [ 0x00000000`00000000 – 0x000000fd`dbbbf770 ]
+0x080 TimeDateStamp : 4
+0x088 EntryPointActivationContext : (null)
+0x090 Lock : (null)
+0x098 DdagNode : 0x00000000`00000002 _LDR_DDAG_NODE
+0x0a0 NodeModuleLink : _LIST_ENTRY [ 0x000002d1`a40b62c0 – 0x000002d1`a40b62c0 ]
+0x0b0 LoadContext : (null)
+0x0b8 ParentDllBase : (null)
+0x0c0 SwitchBackContext : 0x00000000`00000008 Void
+0x0c8 BaseAddressIndexNode : _RTL_BALANCED_NODE
+0x0e0 MappingInfoIndexNode : _RTL_BALANCED_NODE
+0x0f8 OriginalBase : 1
+0x100 LoadTime : _LARGE_INTEGER 0x620026
+0x108 BaseNameHashValue : 0xa40b5b00
+0x10c LoadReason : 0x2d1 (No matching name)
+0x110 ImplicitPathOptions : 0
+0x114 ReferenceCount : 0
+0x118 DependentLoadFlags : 0
+0x11c SigningLevel : 0 ''
+0x120 CheckSum : 0
+0x128 ActivePatchImageBase : (null)
+0x130 HotPatchState : 0 ( LdrHotPatchBaseImage )
可以看到大部分字段都是空的,我们利用InLoadOrderModuleList的Flink字段得到第一个有效的_LDR_DATA_TABLE_ENTRY结构:
0:000> dq 000002d1`a40b5e50
000002d1`a40b5e50 000002d1`a40b5ba0 00007ffb`a53128f0
000002d1`a40b5e60 000002d1`a40b5bb0 00007ffb`a5312900
0:000> dt _LDR_DATA_TABLE_ENTRY 000002d1`a40b5e50
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x000002d1`a40b5ba0 – 0x00007ffb`a53128f0 ]
+0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x000002d1`a40b5bb0 – 0x00007ffb`a5312900 ]
+0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000000`00000000 – 0x00000000`00000000 ]
+0x030 DllBase : 0x00007ff6`c0520000 Void
+0x038 EntryPoint : 0x00007ff6`c05312b2 Void
+0x040 SizeOfImage : 0x28000
+0x048 FullDllName : _UNICODE_STRING "D:\\P\\R\\PE\\P\\W.exe"
+0x058 BaseDllName : _UNICODE_STRING "W.exe"
+0x068 FlagGroup : [4] "???"
+0x068 Flags : 0x22cc
+0x068 PackagedBinary : 0y0
+0x068 MarkedForRemoval : 0y0
+0x068 ImageDll : 0y1
+0x068 LoadNotificationsSent : 0y1
+0x068 TelemetryEntryProcessed : 0y0
+0x068 ProcessStaticImport : 0y0
+0x068 InLegacyLists : 0y1
+0x068 InIndexes : 0y1
+0x068 ShimDll : 0y0
+0x068 InExceptionTable : 0y1
+0x068 VerifierProvider : 0y0
+0x068 ShimEngineCalloutSent : 0y0
+0x068 LoadInProgress : 0y0
+0x068 LoadConfigProcessed : 0y1
+0x068 EntryProcessed : 0y0
+0x068 ProtectDelayLoad : 0y0
+0x068 AuxIatCopyPrivate : 0y0
+0x068 ReservedFlags3 : 0y0
+0x068 DontCallForThreads : 0y0
+0x068 ProcessAttachCalled : 0y0
+0x068 ProcessAttachFailed : 0y0
+0x068 ScpInExceptionTable : 0y0
+0x068 CorImage : 0y0
+0x068 DontRelocate : 0y0
+0x068 CorILOnly : 0y0
+0x068 ChpeImage : 0y0
+0x068 ChpeEmulatorImage : 0y0
+0x068 ReservedFlags5 : 0y0
+0x068 Redirected : 0y0
+0x068 ReservedFlags6 : 0y00
+0x068 CompatDatabaseProcessed : 0y0
+0x06c ObsoleteLoadCount : 0xffff
+0x06e TlsIndex : 0
+0x070 HashLinks : _LIST_ENTRY [ 0x00007ffb`a53132c0 – 0x00007ffb`a53132c0 ]
+0x080 TimeDateStamp : 0x6719d48a
+0x088 EntryPointActivationContext : (null)
+0x090 Lock : (null)
+0x098 DdagNode : 0x000002d1`a40b5fd0 _LDR_DDAG_NODE
+0x0a0 NodeModuleLink : _LIST_ENTRY [ 0x000002d1`a40b5fd0 – 0x000002d1`a40b5fd0 ]
+0x0b0 LoadContext : 0x000000fd`dbbbf530 _LDRP_LOAD_CONTEXT
+0x0b8 ParentDllBase : (null)
+0x0c0 SwitchBackContext : 0x00007ffb`a52b3014 Void
+0x0c8 BaseAddressIndexNode : _RTL_BALANCED_NODE
+0x0e0 MappingInfoIndexNode : _RTL_BALANCED_NODE
+0x0f8 OriginalBase : 0x00007ff6`c0520000
+0x100 LoadTime : _LARGE_INTEGER 0x01dcdfcd`700cf026
+0x108 BaseNameHashValue : 0x7132fddc
+0x10c LoadReason : 4 ( LoadReasonDynamicLoad )
+0x110 ImplicitPathOptions : 0
+0x114 ReferenceCount : 2
+0x118 DependentLoadFlags : 0
+0x11c SigningLevel : 0 ''
+0x120 CheckSum : 0
+0x128 ActivePatchImageBase : (null)
+0x130 HotPatchState : 0 ( LdrHotPatchBaseImage )
这样,就得到了进程加载的第一个模块的信息。以此类推,遍历这个双链表,便可以得到所有已加载模块的信息。
总结
有兴趣的读者,不妨多观察InLoadOrderModuleList链表中模块的顺序。并且InLoadOrderModuleList中模块的顺序,便一定是实际的加载顺序吗?



