admin管理员组文章数量:1605045
1. 引言
在诸多的场景中(例如软件测试,软件安全研究等领域)经常需要分析在目标进程中具体加载了哪些模块(DLL),以及所加载的模块的信息(如模块基地址,映射文件大小等)。获取这windows进程加载的模块信息,曾经有一个行之有效又很便捷的方法,使用windows提供PSAPI(psapi.dll,windows进程状态信息接口)提供的相关的接口就可以快捷的获取进程及进程加载的模块信息。有关PSAPI接口可以参考psapi.h或者微软的官方文档Psapi.h header - Win32 apps | Microsoft Docs。在这里笔者说一句与本文主题无关的题外话,研究windows内核尽量参考一手的windows官方文档,笔者有一个习惯,在对内核相关的研读时都是尽可能的阅读英文版的windows官方文档和使用windows的原版的工具(例如windbg),孜孜不倦,唯求其真。
图1-PSAPI微软官方文档
然而,在这个win64操作系统已经普及,32位程序尚未没落,依然大行其道的时代里,使用PSAPI获取进程中的模块信息已经不再那么有效。在win64的wow64环境中运行的32位程序获取64位进程中模块信息,就无比尴尬,因为windows的PSAPI是不支持的。
我们可以在微软的官方文档中关于EnumProcessModules函数的描述(EnumProcessModules function (psapi.h) - Win32 apps | Microsoft Docs)一探究竟,如图-2和图-3官方文档中所描述。其大意是说在64位的程序中枚举其它进程中的模块信息可以调用EnumProcessModuleEx接口。在wow64环境运行的32位程序中获取64位进程中的模块信息会返回错误代码299(GetLastError),其对应的错误信息为“仅完成部分的 ReadProcessMemory 或 WriteProcessMemory 请求。”。
图-2 64位程序使用EnumProcessModulesEx接口
图-3 EnumProcessModules不支持32位程序中枚举64位程序模块
既然问题摆在我们面前,那么是否有破解之道呢?这正是笔者写这篇文章的目的,在本文中,会详细阐述一种更底层的,放之四海而皆准的,获取windows进程中模块的方法。该方法适用32为程序和64位程序枚举其它进程(包括32位和64位)中的模块。“授之于鱼不如授之于渔”,然而,笔者写本文的目的不仅仅是分享如何获取进程中的模块信息,而是尽量描述清楚解决该类问题的思路,希望能起到抛砖引玉的作用,能启发读者朋友解决类似的问题。由于笔者技术水平有限,如若有错误或不当之处请指正。
熟悉windows内核的朋友都知道,PEB(Process Environment Block,进程环境块)是一个重要的内核数据结构,windows的每个运行的进程中都维护一个PEB数据块其中记录着进程相关的各种信息。在PEB有一个PEB_LDR_DATA的数据,该数据记录着进程已经加载的模块信息。其实windows的PSAPI中的EnumProcessModules的底层实现也是通过PEB_LDE_DATA来遍历进程加载的模块的。“山穷水尽疑无路,柳岸花明又一村”,那么,问题的解决方案似乎近在咫尺了。
2. PEB数据结构
2.1 MSDN文档中的PEB数据结构
PEB的数据结构会随着操作系统的版本而有差异,可以在微软官方文档(https://docs.microsoft/en-us/windows/win32/api/winternl)查看PEB极其相关的数据结构定义。
图4-PEB及相关的数据结构
如上图所示的数据结构是在微软官方文档上摘录的,在PEB,PEB_LDR_DATA及LDR_DATA_TABLE_ENTRY结构体中有诸多的Reserved(保留)的数据项,表明这些数据项的定义可能会随着数据版本的变化而有差异,这些数据项尽量不要使用,如果在迫不得已的情况下使用,要做好操作系统不同版本间的兼容处理。
在PEB结构体中的偏移0x0C处指向进程已加载的模块结构体PEB_LDR_DATA指针,关于PEB_LDR_DATA结构体在微软的官宣文档上是如此介绍的”Contains information about the loaded modules for the process.”。关于PEB结构体中其它数据项笔者在此不再赘述,有兴趣的朋友可自行查阅微软官宣文档,或者联系笔者做进一步的沟通交流。
在PEB_LDR_DATA结构体中,InMemoryOrderModuleList是一个双向链表节点(LIST_ENTRY结构体),其FLink和Blink均指向已加载的模块信息的结构体LDR_DATA_TABLE_ENTRY的头部。在微软的官方文档中关于InMemoryOrderModuleList是这样描述的“The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure. For more information, see Remarks.”。
在LDR_DATA_TABLE_ENTRY结构体中的ImMemoryOrderModuleList同样指向进程中已记载的相邻(上一个和下一个)模块的LDR_DATA_TABLE_ENTRY结构体的头部。这样就可以通过PEB—>LDR遍历所有的模块了。
图5-进程已加载模块双向链表示意图
如图-5所示,笔者画出了LDR_DATA_TABLE_ENTRY双向链表示意图,双向链条的最后一个节点是尾节点,其是无效的模块(并非真是的模块),这一点需要特别注意。在上图中从PEB数据块出发,沿着绿色的箭头(FLink)遍历,即可枚举windows进程已加载的模块。
然而,微软的官方文档或许是处于版本兼容的考虑(真实的意图不得而知),对PEB结构的介绍中使用Reserved(保留)隐藏了一些细节。如果要对这些细节进行尽一步得探究,我们可以借助微软的调试工具windbg去进行深入的分析。
2.2 真实的PEB结构
其实真的PEB_LDR_DATA和LDR_DATA_TABLE_ENTRY结构体中有三个LINK_ENTRY节点分别是InLoadOrderModuleList,InMemoryOrderModuleList,InInitializationOrderModuleList。分别是按照加载次序,在内存中的映像地址(Image)顺序,初始化顺序进行组织的。我们可以使用windbg附加某个32位的进程,分别使用“dt _PEB_LDR_DATA”和”dt _LDR_DATA_TABLE_ENTRY”指令查看PEB_LDR_DATA和LDR_DATA_TABLE_ENTRY结构中的数据项。
有关PEB相关的其它数据结构有兴趣的朋友可以使用windbg自行分析,笔者不再赘述,如有必要也可以和笔者进行进一步的沟通交流。
图-6 32位程序PEB_LDR_DATA结构体
图-7 32位程序LDR_DATA_TABLE_ENTRY结构体
2.3 32位程序和64位程序PEB结构差异
PEB结构体在32位程序和64位程序中差异比较大,最显著的差异是指针在32位程序中是32为的,然而在64位程序中是64位的,要有其它的一些细节上的差异笔者也不再一一列举,可以通过windbg查看相应的结构体在32位程序和64位程序中的差异。具体方法使用windbg分别附加32位和64位程序使用”dt _PEB”命令查看PEB结构体(也可以查看其它结构体),如下图所示(截图只显示了部分的结构体)。
在实际的应用中,我们一定要注意区分操作系统的版本,以及程序是32位程序还是程序,以做特定的兼容处理。
图-8 32位程序和64位程序PEB结构体差异
3. 通过PEB遍历已加载模块的方法
通过以上章节的叙述,是否觉得通过PEB结构体遍历程序已加载的块,已经豁然开朗,近在咫尺了?然而,笔者很遗憾的说”NO”,正所谓“道高一尺,魔高一丈“,我们面临的依然会有很多的障碍,不经风雨怎见得彩虹。这所有的一切,且听笔者娓娓道来。
3.1 读取PEB结构体的数据的方法
如何才能读取到PEB结构体的数据?在笔者所知的有两种方法,其一是通过TEB结构体读取PEB指针,另外一种方法,是使用Windows提供的API接口读取。下面笔者就这两种方法逐一介绍。
3.1.1 通过TEB读取PEB结构体数据
TEB(Thread Environment Block)是线程环境块,该结构中包含了系统频繁使用的与线程相关的数据。进程中的每个线程都有一个自己的TEB块。在TEB中有一个指向进程PEB结构体的指针,在32位程序中其偏移量为0x30,在64为程序中其偏移量为0x60。同样我们在windbg中使用“!teb”和”dt _TEB”命令可以分别查看TEB块数据和TEB数据结构,如下图所示。
图9-TEB数据结构
根据TEB结构的描述,那么我们可知,只要能获取TEB结构体数据,我们就可以顺利成章的读取道PEB结构的数据。熟悉系统内核的朋友都知道,fs寄存器其实就指向了当前先线程的TEB数据块。那么我们可以通过汇编指令mov eac, fs[0x30]或mov eax, fs[0x60]来获进程的PEB结构体地址。
当然,我们也可以通过Windows API NtCurrentTeb 来获取当前线程的TEB结构体指针,然而不幸的是该API仅在Win7及以后的版本支持,在微软的官方文档上是如此进行说明的“Minimum supported client Available in Windows 7 and later versions of Windows.“。
然而,无论通过汇编指令还是通过NtCurrentTeb接口来获取TEB的地址,都是有局限的。因为,通过这种方法只能获取到当前进程的PEB,在实际的应用场景中遍历本进程的加载的模块意义不大,大多场景是分析其它第三方的应用(进程)所加载的模块。或许,有的朋友会说,可以通过注入的方式,达到目的。注入固然是可以的,但是注入太重,笔者不建议使用。
那么是否有更“轻量级“的方法,来遍历其它第三方应用所加载模块的方法呢?答案是肯定的,下面笔者就介绍使用windows api来读取第三方应用的PEB的方法。
3.1.2 通过NtQueryInformationProcess读取PEB数据
在NtDll.dll的API接口NtQueryInformationProcess支持查询进程的PEB数据,调用该方法的流程是首先使用OpenProcess打开已经存在的进程对象,并返回目标进程对象的句柄。分配PEB结构的内存,调用NtQueryInformationProcess传入目标进程对象句柄,已分配的PEB结构体内存指针,使用ProcessBasicInformation(0)作为ProcessInformationClass参数的值调用NtQueryInformationProcess来读取目标进程的PEB结构体的数据。关于NtQueryInformationProcess说明请参考NtQueryInformationProcess function (winternl.h) - Win32 apps | Microsoft Docs。
图-10 NtQueryInformationProcess官方文档说明
然而,到此是否意味着大功告成呢?答案否也,因为如果在32位程序中调用NtQueryInformationProcess来获取64位程序的PEB是不可以的。在32位程序中,获取64位程序PEB结构体的数据,需要调用在微软官方文档上尚未公开的API接口NtWow64QueryInformationProcess64,因为在官宣文档中没有公开,若要使用就需要对该接口进行逆向分析,该接口的声明与NtQueryInformationProcess类似。同样在32位程序中读取64位进程中的数据调用ReadProcessMemory接口也是不可以的,需要调用尚未公开的NtWow64ReadVirtualMemory64接口,该接口的声明ReadProcessMemory类似,差别就在于与指针,数据长度相关的参数类型为64位的。
3.2 示例程序
通过以上章节的描述,想必朋友们对于如何遍历第三方进程中已加载的模块,已经胸有成竹了。下面笔者对于整个流程进行梳理,并使用曾经风靡一时,时至今日已经没落,但尚未完全退出历史舞台的delphi语言进行实现。其实使用什么语言并不重要,对于程序员来时内核原理,基础算法,编码艺术等才是极为重要的硬实力。
3.2.1 遍历进程已加载模块流程
如下图所示在32位程序中遍历目标进程已加载的模块的流程如下图所示。
图-11 遍历目标进程已加载模块流程
3.2.2 示例程序核心代码片段
使用delphi编写的示例代码的核心代码段如下,关于结构体的声明及Windows API的声明可以参考以上章节的叙述,笔者不再赘述。3.2.3 进程模块查看器
function getModuleList(HProcess: THandle; var sParamete: string): TModuleList;
var bRet: Boolean;
dwNtState: NTSTATUS;
bWinIs64, bProcWow64: Boolean;
cMemLen, cWritenLen, cReadLen, cUSBufLen: Cardinal;
pBasicInfo32: PPROCESS_BASIC_INFORMATION32;
pBasicInfo64: PPROCESS_BASIC_INFORMATION64;
pPEBData32: PPEB32;
pPEBData64: PPEB64;
pLDR32: PPEB_LDR_DATA32;
pLDR64: PPEB_LDR_DATA64;
pLDREntry32: PLDR_DATA_TABLE_ENTRY32;
pLDREntry64: PLDR_DATA_TABLE_ENTRY64;
pNextLDREntry32, pUSBuf: Pointer;
pNextLDREntry64: UInt64;
cReadLen64: Int64;
pParam32: PRTL_USER_PROCESS_PARAMETERS32;
pParam64: PRTL_USER_PROCESS_PARAMETERS64;
pModule: PModuleInfo;
sModuleFile: string;
begin
EnableDebugPriv;
Result := TModuleList.Create;
bWinIs64 := IsWin64;
bProcWow64 := IsWow64Process(HProcess);
pBasicInfo32 := nil;
pBasicInfo64 := nil;
pPEBData32 := nil;
pPEBData64 := nil;
pLDR32 := nil;
pLDR64 := nil;
pLDREntry32 := nil;
pLDREntry64 := nil;
pParam32 := nil;
pParam64 := nil;
try
if bWinIs64 and (not bProcWow64) then
begin
//目标程序位64位程序
//1. 查询目标进程基信息
cMemLen := SizeOf(PROCESS_BASIC_INFORMATION64);
pBasicInfo64 := GetMemory(cMemLen);
FillChar(pBasicInfo64^, cMemLen, #0);
dwNtState := NtWow64QueryInformationProcess64(HProcess, ProcessBasicInformation,
pBasicInfo64, cMemLen, cWritenLen);
if dwNtState < 0 then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
//2. 读取PEB数据
cMemLen := SizeOf(PEB64);
pPEBData64 := GetMemory(cMemLen);
FillChar(pPEBData64^, cMemLen, #0);
dwNtState := NtWow64ReadVirtualMemory64(HProcess, pBasicInfo64^.PebBaseAddress,
pPEBData64, cMemLen, cReadLen64);
if dwNtState < 0 then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
//3. 读取Parameter
cMemLen := SizeOf(RTL_USER_PROCESS_PARAMETERS64);
pParam64 := GetMemory(cMemLen);
FillChar(pParam64^, cMemLen, #0);
sParamete := '';
dwNtState := NtWow64ReadVirtualMemory64(HProcess, pPEBData64^.ProcessParameters,
pParam64, cMemLen, cReadLen64);
if dwNtState >= 0 then
begin
cUSBufLen := pParam64^.ComandLine.Length + 2;
pUSBuf := GetMemory(cUSBufLen);
FillChar(pUSBuf^, cUSBufLen, #0);
dwNtState := NtWow64ReadVirtualMemory64(HProcess, pParam64^.ComandLine.Buffer,
pUSBuf, pParam64^.ComandLine.Length, cReadLen64);
if dwNtState >= 0 then
sParamete := WideCharToString(PWideChar(pUSBuf));
FreeMemory(pUSBuf);
end;
//4. 读取LDR数据
cMemLen := SizeOf(PEB_LDR_DATA64);
pLDR64 := GetMemory(cMemLen);
FillChar(pLDR64^, cMemLen, #0);
dwNtState := NtWow64ReadVirtualMemory64(HProcess, pPEBData64^.Ldr, pLDR64,
cMemLen, cReadLen64);
if dwNtState < 0 then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
//5. 循环读取LDR_Entry
cMemLen := SizeOf(LDR_DATA_TABLE_ENTRY64);
pLDREntry64 := GetMemory(cMemLen);
FillChar(pLDREntry64^, cMemLen, #0);
pNextLDREntry64 := pLDR64^.InLoadOrderModuleList.FLink;
while 1 = 1 do
begin
dwNtState := NtWow64ReadVirtualMemory64(HProcess, pNextLDREntry64, pLDREntry64,
cMemLen, cReadLen64);
if dwNtState < 0 then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
if pLDREntry64^.InLoadOrderLinks.FLink = pLDR64^.InLoadOrderModuleList.FLink then
Break;
cUSBufLen := pLDREntry64^.FullDllName.Length + 2;
pUSBuf := GetMemory(cUSBufLen);
FillChar(pUSBuf^, cUSBufLen, #0);
dwNtState := NtWow64ReadVirtualMemory64(HProcess, pLDREntry64^.FullDllName.Buffer,
pUSBuf, cUSBufLen, cReadLen64);
if dwNtState < 0 then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
New(pModule);
StrCopy(@pModule^.DllBase[0], PChar(IntToHex(pLDREntry64^.DllBase, 16)));
StrCopy(@pModule^.EntryPoint[0], PChar(IntToHex(pLDREntry64^.EntryPoint, 16)));
pModule^.ImageSize := pLDREntry64^.SizeOfImage;
pModule^.LoadCount := pLDREntry64^.LoadCount;
sModuleFile := WideCharToString(PWideChar(pUSBuf));
StrCopy(@pModule^.ModuleFile, PChar(sModuleFile));
Result.Add(pModule);
FreeMemory(pUSBuf);
pNextLDREntry64 := pLDREntry64^.InLoadOrderLinks.FLink;
end;
end
else
begin
//目标程序是32位程序
//1. 查询目标进程基信息
cMemLen := SizeOf(PROCESS_BASIC_INFORMATION32);
pBasicInfo32 := GetMemory(cMemLen);
FillChar(pBasicInfo32^, cMemLen, #0);
dwNtState := NtQueryInformationProcess(HProcess, ProcessBasicInformation,
pBasicInfo32, cMemLen, cWritenLen);
if dwNtState < 0 then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
//2. 读取PEB数据
cMemLen := SizeOf(PROCESS_BASIC_INFORMATION32);
pPEBData32 := GetMemory(cMemLen);
FillChar(pPEBData32^, cMemLen, #0);
bRet := ReadProcessMemory(hProcess, pBasicInfo32.PebBaseAddress, pPEBData32,
cMemLen, cReadLen);
if not bRet then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
//3. 读取Parameter
cMemLen := SizeOf(RTL_USER_PROCESS_PARAMETERS32);
pParam32 := GetMemory(cMemLen);
FillChar(pParam32^, cMemLen, #0);
sParamete := '';
bRet := ReadProcessMemory(HProcess, pPEBData32^.ProcessParameters, pParam32,
cMemLen, cReadLen);
if bRet then
begin
cUSBufLen := pParam32^.CommandLine.Length + 2;
pUSBuf := GetMemory(cUSBufLen);
FillChar(pUSBuf^, cUSBufLen, #0);
bRet := ReadProcessMemory(HProcess, pParam32^.CommandLine.Buffer, pUSBuf,
pParam32^.CommandLine.Length, cReadLen);
if bRet then
sParamete := WideCharToString(PWideChar(pUSBuf));
FreeMemory(pUSBuf);
end;
//4. 读取LDR数据
cMemLen := SizeOf(PEB_LDR_DATA32);
pLDR32 := GetMemory(cMemLen);
FillChar(pLDR32^, cMemLen, #0);
bRet := ReadProcessMemory(HProcess, pPEBData32^.Ldr, pLDR32, cMemLen, cReadLen);
if not bRet then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
//5. 循环读取LDR_Entry
cMemLen := SizeOf(LDR_DATA_TABLE_ENTRY32);
pLDREntry32 := GetMemory(cMemLen);
FillChar(pLDREntry32^, cMemLen, #0);
pNextLDREntry32 := pLDR32^.InLoadOrderModuleList.FLink;
while 1 = 1 do
begin
bRet := ReadProcessMemory(HProcess, pNextLDREntry32, pLDREntry32, cMemLen, cReadLen);
if not bRet then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
if pLDREntry32^.InLoadOrderLinks.FLink = pLDR32^.InLoadOrderModuleList.FLink then
Break;
cUSBufLen := pLDREntry32^.FullDllName.Length + 2;
pUSBuf := GetMemory(cUSBufLen);
FillChar(pUSBuf^, cUSBufLen, #0);
bRet := ReadProcessMemory(HProcess, pLDREntry32^.FullDllName.Buffer, pUSBuf, cUSBufLen, cReadLen);
if not bRet then
begin
FreeAndNil(Result);
raise Exception.Create(Format('%d-%s', [GetLastError, SysErrorMessage(GetLastError)]));
end;
New(pModule);
StrCopy(@pModule^.DllBase[0], PChar(IntToHex(pLDREntry32^.DllBase, 8)));
StrCopy(@pModule^.EntryPoint[0], PChar(IntToHex(pLDREntry32^.EntryPoint, 8)));
pModule^.ImageSize := pLDREntry32^.SizeOfImage;
pModule^.LoadCount := pLDREntry32^.LoadCount;
pModule^.TimeStamp := pLDREntry32^.TimeDateStamp;
sModuleFile := WideCharToString(PWideChar(pUSBuf));
StrCopy(@pModule^.ModuleFile, PChar(sModuleFile));
Result.Add(pModule);
FreeMemory(pUSBuf);
pNextLDREntry32 := pLDREntry32^.InLoadOrderLinks.FLink;
end;
end;
finally
if Assigned(pBasicInfo32) then
FreeMemory(pBasicInfo32);
if Assigned(pBasicInfo64) then
FreeMemory(pBasicInfo64);
if Assigned(pPEBData32) then
FreeMemory(pPEBData32);
if Assigned(pPEBData64) then
FreeMemory(pPEBData64);
if Assigned(pLDR32) then
FreeMemory(pLDR32);
if Assigned(pLDR64) then
FreeMemory(pLDR64);
if Assigned(pLDREntry32) then
FreeMemory(pLDREntry32);
if Assigned(pLDREntry64) then
FreeMemory(pLDREntry64);
if Assigned(pParam32) then
FreeMemory(pParam32);
if Assigned(pParam64) then
FreeMemory(pParam64);
end;
end;
3.2.3 进程模块查看器
如下图所示,这是笔者使用delphi开发的遍历进程模块的工具。
版权声明:本文标题:枚举Windows进程中模块的几种方法-PEB内核结构详解 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/dianzi/1725455246a1024126.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论