枚举Windows进程中模块的几种方法-PEB内核结构详解

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.com/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开发的遍历进程模块的工具。

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值