Enumerate Remote Process Modules via PEB - Delphi

Description

Notice: The target process must have the same architecture as the process attempting to read it remotely

DarkCoderSc personal avatar
DarkCoderSc

Jean-Pierre LESUEUR

uses
 System.SysUtils, Winapi.Windows, Generics.Collections;

// ...

type
  TProcessInformationClass = (
    // ...
    ProcessBasicInformation = 0
    // ...
  );

// ...

function NtQueryInformationProcess(
  ProcessHandle            : THandle;
  ProcessInformationClass  : TProcessInformationClass;
  ProcessInformation       : Pointer;
  ProcessInformationLength : ULONG;
  var ReturnLength         : ULONG
) : NTSTATUS; stdcall; external 'NTDLL.DLL';

// ...

type
  PPEB = Pointer;

  _PROCESS_BASIC_INFORMATION = record
    Reserved1                    : PVOID;
    PebBaseAddress               : PPEB;
    Reserved2                    : array[0..1] of PVOID;
    UniqueProcessId              : ULONG_PTR;
    Reserved3                    : PVOID;
  end;
  TProcessBasicInformation = _PROCESS_BASIC_INFORMATION;
  PProcessBasicInformation = ^TProcessBasicInformation;

  _LDR_DATA_TABLE_ENTRY = record
    InLoadOrderLinks            : TListEntry;
    InMemoryOrderLinks          : TListEntry;
    InInitializationOrderLinkes : TListEntry;

    DllBase                     : PVOID;
    EntryPoint                  : PVOID;
    SizeOfImage                 : ULONG;
    FullDllName                 : TUnicodeString;
    BaseDllName                 : TUnicodeString;
  end;
  TLdrDataTableEntry = _LDR_DATA_TABLE_ENTRY;
  PLdrDataTableEntry = ^TLdrDataTableEntry;

// ...

(* TModuleInformation *)

type
  TModuleInformation = class
  private
    FImagePath   : String;
    FBaseAddress : NativeUInt;
    FEntryPoint  : NativeUInt;
    FSizeOfImage : Cardinal;
  public
    {@C}
    constructor Create(const AImagePath : String; const ABaseAddress, AEntryPoint: NativeUInt; const ASizeOfImage : Cardinal);

    {@G}
    property ImagePath   : String     read FImagePath;
    property BaseAddress : NativeUInt read FBaseAddress;
    property EntryPoint  : NativeUInt read FEntryPoint;
    property SizeOfImage : Cardinal   read FSizeOfImage;
  end;

// ...

constructor TModuleInformation.Create(const AImagePath : String; const ABaseAddress, AEntryPoint: NativeUInt; const ASizeOfImage : Cardinal);
begin
  inherited Create();
  ///

  FImagePath   := AImagePath;
  FBaseAddress := ABaseAddress;
  FEntryPoint  := AEntryPoint;
  FSizeOfImage := ASizeOfImage;
end;

(* Local *)

function EnumerateProcessModules(const ATargetProcessId : Cardinal; var AList : TObjectList<TModuleInformation>) : Cardinal;
begin
  result := 0;
  ///

  if not Assigned(AList) then
    AList := TObjectList<TModuleInformation>.Create(True)
  else begin
    AList.Clear();

    ///
    if not AList.OwnsObjects then
      AList.OwnsObjects := True;
  end;
  ///

  var hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, False, ATargetProcessId);
  if hProcess = 0 then
    raise EWindowsException.Create('OpenProcess');
  try
    var AProcessBasicInformation : TProcessBasicInformation;
    var AReturnLength : Cardinal;

    var ARet := NtQueryInformationProcess(
      hProcess,
      ProcessBasicInformation,
      @AProcessBasicInformation,
      SizeOf(TProcessBasicInformation),
      AReturnLength
    );

    if ARet < 0 then
      raise Exception.Create('Could not retrieve target process PEB address.');
    ///

    var ppPEBLdrData := Pointer(NativeUInt(AProcessBasicInformation.PebBaseAddress) + {$IFDEF WIN64}$00000018{$ELSE}$0000000c{$ENDIF});
    var pPEBLdrData : Pointer;

    var ABytesRead : SIZE_T;

    if not ReadProcessMemory(hProcess, ppPEBLdrData, @pPEBLdrData, SizeOf(Pointer), ABytesRead) then
      raise EWindowsException.Create('ReadProcessMemory(1)');

    var AEntry : TListEntry;
    var pFirstEntryOffset := Pointer(NativeUInt(pPEBLdrData) + {$IFDEF WIN64}$00000010{$ELSE}$0000000c{$ENDIF});

    if not ReadProcessMemory(hProcess, pFirstEntryOffset, @AEntry, SizeOf(TListEntry), ABytesRead) then
      raise EWindowsException.Create('ReadProcessMemory(2)');

    var pEntryOffset := AEntry.Flink;
    var ADataTableEntry : TLdrDataTableEntry;

    repeat
      if not ReadProcessMemory(hProcess, pEntryOffset, @ADataTableEntry, SizeOf(TLdrDataTableEntry), ABytesRead) then
        raise EWindowsException.Create(Format('ReadProcessMemory(3), Index: %d', [AList.Count]));

      var pImageFileName : PWideChar;
      var pImageFileNameSize := ADataTableEntry.FullDllName.Length * SizeOf(WideChar);

      GetMem(pImageFileName, pImageFileNameSize);
      try
        if not ReadProcessMemory(hProcess, ADataTableEntry.FullDllName.Buffer, pImageFileName, pImageFileNameSize, ABytesRead) then
          raise EWindowsException.Create(Format('ReadProcessMemory(4), Index: %d', [AList.Count]));

        ///
        AList.Add(TModuleInformation.Create(
          string(pImageFileName),
          NativeUInt(ADataTableEntry.DllBase),
          NativeUInt(ADataTableEntry.EntryPoint),
          ADataTableEntry.SizeOfImage
        ));
      finally
        FreeMem(pImageFileName, pImageFileNameSize);
      end;

      ///
      pEntryOffset := ADataTableEntry.InLoadOrderLinks.Flink;
    until pFirstEntryOffset = ADataTableEntry.InLoadOrderLinks.Flink;
  finally
    CloseHandle(hProcess);
  end;

  ///
  result := AList.Count;
end;

// ...

begin
try
  var AList := TObjectList<TModuleInformation>.Create(True);
  try
    EnumerateProcessModules(<target_process_id>, AList);
    ///

    for var AModuleEntry in AList do
      WriteLn(Format('Base:[0x%p], EP:[0x%p], Size:[0x%p] -> %s', [
        Pointer(AModuleEntry.BaseAddress),
        Pointer(AModuleEntry.EntryPoint),
        Pointer(AModuleEntry.SizeOfImage),
        AModuleEntry.ImagePath
      ]));
  finally
    FreeAndNil(AList);
  end;

// ...

Creating and researching code snippets takes time and effort. You’re welcome to share them through your own platforms, but please don’t forget to credit the original author, here: Jean-Pierre LESUEUR.

Depends On


Created

June 2, 2025

Last Revised

June 2, 2025