Jump to content
Pierre le Riche

LoadLibrary and FreeLibrary notification

Recommended Posts

Posted (edited)

Hi,

 

For performance reasons I am caching a map of the address space in the stack tracing code inside FastMM_FullDebugMode.dll. This map goes stale whenever a library is loaded or unloaded, and I've been dealing with this rather crudely: by handling the ensuing access violations in a try...except block. These access violations can be quite annoying when running the application under the debugger, so I would like to make them less likely. Invalidating the map whenever a library is loaded or unloaded would be an improvement.

 

I've therefore been looking at ways to be notified when a library is loaded or unloaded, but the only solution I have found is patching the LoadLibrary and FreeLibrary calls in kernel32.dll in-memory to insert a hook.

 

Is there a better strategy? The mechanism doesn't have to be 100% accurate, so if there's a proxy for LoadLibrary and FreeLibrary that is less invasive I would rather go for that.

 

Thanks,

Pierre

Edited by Pierre le Riche

Share this post


Link to post

Thanks Anders,

 

I think I'll go with the LdrRegisterDllNotification option. The Vista+ restriction shouldn't be a big deal, since I only need to deal with the exceptions under a debugger and I can't imagine too many developers are still using Windows XP. I see there's a note "[This function may be changed or removed from Windows without further notice.]" in the documentation, so I'll implement it in such a way that it doesn't crash if it it is not available.

 

Best regards,

Pierre

Share this post


Link to post

@Pierre le Riche I searched for an article i read sometimes ago, scrapped few lines based on its idea, which is using PEB to walk the loaded modules, so no callback notification and no caching is needed, you can resolve on spot and when needed, also you can extract the table in-place to use per one stack walk operation.

 

the article 

https://exploit-notes.hdks.org/exploit/reverse-engineering/debugger/windows-process-internals-with-windbg/

PEB structure

https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/pebteb/peb/index.htm

PEB_LDR_DATA

https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntpsapi_x/peb_ldr_data.htm?tx=186

LDR_DATA_TABLE_ENTRY

https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntldr/ldr_data_table_entry/index.htm?ta=10&tx=96,97,105,106,109,114,173,177,179,186,192,195

 

the unit

unit lcAddressToModule;

interface

uses
  Windows;

type
  PLIST_ENTRY = ^LIST_ENTRY;
  LIST_ENTRY = packed record
    Flink: PLIST_ENTRY;
    Blink: PLIST_ENTRY;
  end;

  PUNICODE_STRING = ^UNICODE_STRING;
  UNICODE_STRING = packed record
    Length: Word;
    MaximumLength: Word;
    {$IFDEF CPUX64}
    Padding: array[0..3] of Byte; // Align Buffer to 8 bytes
    {$ENDIF}
    Buffer: PWideChar;
  end;

  PLDR_DATA_TABLE_ENTRY = ^LDR_DATA_TABLE_ENTRY;
  LDR_DATA_TABLE_ENTRY = packed record
    InLoadOrderLinks: LIST_ENTRY;
    InMemoryOrderLinks: LIST_ENTRY;
    InInitializationOrderLinks: LIST_ENTRY;
    DllBase: Pointer;
    EntryPoint: Pointer;
    SizeOfImage: ULONG;
    {$IFDEF CPUX64}
    Padding: array[0..3] of Byte; // Align FullDllName to 60
    {$ENDIF}
    FullDllName: UNICODE_STRING;
    BaseDllName: UNICODE_STRING;
  end;

  PPEB_LDR_DATA = ^PEB_LDR_DATA;
  PEB_LDR_DATA = packed record
    Length: ULONG;
    Initialized: Boolean;
    {$IFDEF CPUX64}
    Padding: array[0..2] of Byte; // Align SsHandle to 8 bytes
    {$ENDIF}
    SsHandle: Pointer;
    InLoadOrderModuleList: LIST_ENTRY;
  end;

  PPEB = ^PEB;
  PEB = packed record
    Reserved: array[0..2] of Byte;
    BeingDebugged: Byte;
    {$IFDEF CPUX64}
    Reserved2: array[0..11] of Byte; // Align Ldr to 24
    {$ELSE}
    Reserved2: array[0..3] of Byte; // Align Ldr to 12
    {$ENDIF}
    ImageBaseAddress: Pointer;
    Ldr: PPEB_LDR_DATA;
  end;

  TModuleInfo = record
    ModuleBase: Pointer;
    ModuleSize: ULONG;
    ModuleName: string;
    FullPath: string;
    EntryPoint: Pointer;
    IsValid: Boolean;
  end;

function FindModuleByAddress(Address: Pointer): TModuleInfo;

implementation

function GetCurrentPEB: PPEB;
asm
  {$IFDEF CPUX64}
  mov rax, gs:[$60]  // PEB at offset 0x60 in TEB on x64
  {$ELSE}
  mov eax, fs:[$30]  // PEB at offset 0x30 in TEB on x86
  {$ENDIF}
end;

function AddressViolateUpperRange(Address: Pointer): Boolean;
begin
  Result := NativeUInt(Address) > {$IFDEF CPUX64} $7FFFFFFFFFFF {$ELSE} $7FFFFFFF {$ENDIF};
end;

function AddressViolateLowerRange(Address: Pointer): Boolean;
begin
  Result := NativeUInt(Address) < $10000;   // always in user mode then it shold be above 64kb
end;

function FindModuleByAddress(Address: Pointer): TModuleInfo;
var
  PEB: PPEB;
  LdrData: PPEB_LDR_DATA;
  ModuleEntry: PLDR_DATA_TABLE_ENTRY;
  CurrentEntry, FirstEntry: PLIST_ENTRY;
  ModuleBaseAddr, ModuleEndAddr: NativeUInt;
  FlinkPtr: PPointer;
begin
  FillChar(Result, SizeOf(Result), 0);
  Result.IsValid := False;

  if not Assigned(Address) or AddressViolateLowerRange(Address) or AddressViolateUpperRange(Address) then
    Exit;

  PEB := GetCurrentPEB;
  if not Assigned(PEB) or not Assigned(PEB^.Ldr) then
    Exit;

  LdrData := PEB^.Ldr;
  if not LdrData^.Initialized then
    Exit;

  {$IFDEF CPUX64}
  FlinkPtr := PPointer(NativeUInt(LdrData) + 24); // InLoadOrderModuleList.Flink at offset 24 on x64
  {$ELSE}
  FlinkPtr := PPointer(NativeUInt(LdrData) + 12); // InLoadOrderModuleList.Flink at offset 12 on x86
  {$ENDIF}
  CurrentEntry := FlinkPtr^;
  if AddressViolateLowerRange(CurrentEntry) or AddressViolateUpperRange(CurrentEntry) then
    Exit;

  FirstEntry := CurrentEntry;

  while Assigned(CurrentEntry) and (CurrentEntry <> PList_Entry(NativeUInt(LdrData) + {$IFDEF CPUX64} 24 {$ELSE} 12 {$ENDIF})) do
  begin
    if AddressViolateLowerRange(CurrentEntry) or AddressViolateUpperRange(CurrentEntry)  then
      Break;

    ModuleEntry := PLDR_DATA_TABLE_ENTRY(CurrentEntry);
    try
      if Assigned(ModuleEntry^.DllBase) then
      begin
        ModuleBaseAddr := NativeUInt(ModuleEntry^.DllBase);
        ModuleEndAddr := ModuleBaseAddr + ModuleEntry^.SizeOfImage;
        if (NativeUInt(Address) >= ModuleBaseAddr) and (NativeUInt(Address) < ModuleEndAddr) then
        begin
          Result.ModuleBase := ModuleEntry^.DllBase;
          Result.ModuleSize := ModuleEntry^.SizeOfImage;
          Result.EntryPoint := ModuleEntry^.EntryPoint;
          if Assigned(ModuleEntry^.BaseDllName.Buffer) then
            Result.ModuleName := ModuleEntry^.BaseDllName.Buffer
          else
            Result.ModuleName := '';
          if Assigned(ModuleEntry^.FullDllName.Buffer) then
            Result.FullPath := ModuleEntry^.FullDllName.Buffer
          else
            Result.FullPath := '';
          Result.IsValid := True;
          Exit;
        end;
      end;
    except
      // Ignore access violations
    end;

    CurrentEntry := CurrentEntry^.Flink;
    if (not Assigned(CurrentEntry)) or (CurrentEntry = FirstEntry) then
      Break;
  end;
end;

end.

Test project for it 

program AddressToModuleTest;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  Windows,
  lcAddressToModule in 'lcAddressToModule.pas';

function PointerToHex(P: Pointer): string;
begin
  Result := IntToHex(NativeUInt(P), 8);
end;

procedure FindModuleFromAddress(Address: Pointer);
var
  ModuleInfo: TModuleInfo;
begin
  Writeln('Resolving address : ' + PointerToHex(Address));
  ModuleInfo := FindModuleByAddress(Address);
  if ModuleInfo.IsValid then
  begin
    Writeln(#9'Found module : ' + ModuleInfo.ModuleName);
    Writeln(#9'Full Path    : ', ModuleInfo.FullPath);
    Writeln(#9'Base Address : ', PointerToHex(ModuleInfo.ModuleBase));
    Writeln(#9'Module Size  : ', ModuleInfo.ModuleSize);
    Writeln(#9'Entry Point  : ', PointerToHex(ModuleInfo.EntryPoint));
  end
  else
  begin
    Writeln('Failed to get module....');
  end;
  Writeln;
end;

procedure ResolveAddressesIntoModules;
var
  DLLHandle: HMODULE;
  Address: Pointer;
begin
  Address := nil;
  FindModuleFromAddress(nil);

  FindModuleFromAddress(Addr(PointerToHex));  // address to local function
  FindModuleFromAddress(Addr(ResolveAddressesIntoModules));  // address to local function

  // test PsApi.dll
  DLLHandle := LoadLibrary('PsApi.dll');
  if DLLHandle <> INVALID_HANDLE_VALUE then
  begin
    Address := GetProcAddress(DLLHandle, 'EnumProcessModules');
    if Assigned(Address) then
      FindModuleFromAddress(Address);
    FreeLibrary(DLLHandle);
  end;

  // test Ws2_32.dll
  DLLHandle := LoadLibrary('Ws2_32.dll');
  if DLLHandle <> INVALID_HANDLE_VALUE then
  begin
    Address := GetProcAddress(DLLHandle, 'WSAStartup');
    if Assigned(Address) then
      FindModuleFromAddress(Address);
    FreeLibrary(DLLHandle);
  end;

  if Assigned(Address) then
    FindModuleFromAddress(Address);   // should be "not module found"

  // invalid address
  FindModuleFromAddress(Pointer($DEADBEEF));
end;

begin
  try
    ResolveAddressesIntoModules;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  Writeln('Done.');
  Readln;
end.

the output for 64bit (also works for 32bit)

Resolving address : 00000000
Failed to get module....

Resolving address : 00426640
        Found module : AddressToModuleTest.exe
        Full Path    : D:\Projects Delphi\PEBDemo\Win64\Debug\AddressToModuleTest.exe
        Base Address : 00400000
        Module Size  : 1531904
        Entry Point  : 00429FB0

Resolving address : 00426A30
        Found module : AddressToModuleTest.exe
        Full Path    : D:\Projects Delphi\PEBDemo\Win64\Debug\AddressToModuleTest.exe
        Base Address : 00400000
        Module Size  : 1531904
        Entry Point  : 00429FB0

Resolving address : 7FFFB7411010
        Found module : PsApi.dll
        Full Path    : C:\WINDOWS\System32\PsApi.dll
        Base Address : 7FFFB7410000
        Module Size  : 32768
        Entry Point  : 7FFFB7411110

Resolving address : 7FFFB762EB10
        Found module : Ws2_32.dll
        Full Path    : C:\WINDOWS\System32\Ws2_32.dll
        Base Address : 7FFFB7620000
        Module Size  : 438272
        Entry Point  : 7FFFB7634300

Resolving address : 7FFFB762EB10
Failed to get module....

Resolving address : DEADBEEF
Failed to get module....

Done.

Here i want to point that the above is tested on few OSs, i used and manipulated PEB so many times and i know for fact that Geoff Chappell documentation is very accurate have no errors and no mistake, and based on his documentation the above code should work from Windows 2000 till Windows 11.

 

Hope you find that helpful or useful and thank you for your great contribution over so many years ! 

 

 

ps: the structures was big and it hit me for this specific use we don't the complexity of that structures so i cut them short, and i think they can be trimmed even more or removed in whole, leaving few offsets in their places.

Share this post


Link to post

This may be better than using Assembly 

function RtlGetCurrentPeb: Pointer; stdcall; external 'ntdll.dll';
function GetCurrentPEB: PPEB;
begin
  Result := RtlGetCurrentPeb;
end;

 

Share this post


Link to post

Here a more precise and protected version will less redundant code

unit lcAddressToModule;

interface

uses
  Windows;

type
  TModuleInfo = record
    ModuleBase: Pointer;
    ModuleSize: Cardinal;
    ModuleName: string;
    FullPath: string;
    EntryPoint: Pointer;
    IsValid: Boolean;
  end;

function FindModuleByAddress(Address: Pointer): TModuleInfo;

implementation

const
  Flink_OFFSET = {$IFDEF CPUX64} 24 {$ELSE} 12 {$ENDIF};  // Offset is 12 for 32bit and 24 for 64bit
  LOOPING_PROTECTION_LIMIT = 1000;

type
  PLIST_ENTRY = ^LIST_ENTRY;
  LIST_ENTRY = packed record
    Flink: PLIST_ENTRY;
    Blink: PLIST_ENTRY;
  end;

  PUNICODE_STRING = ^UNICODE_STRING;
  UNICODE_STRING = packed record
    Length: Word;
    MaximumLength: Word;
    {$IFDEF CPUX64}
    Padding: array[0..3] of Byte;         // Align Buffer to 8 bytes
    {$ENDIF}
    Buffer: PWideChar;
  end;

  PLDR_DATA_TABLE_ENTRY = ^LDR_DATA_TABLE_ENTRY;
  LDR_DATA_TABLE_ENTRY = packed record
    InLoadOrderLinks: LIST_ENTRY;
    InMemoryOrderLinks: LIST_ENTRY;
    InInitializationOrderLinks: LIST_ENTRY;
    DllBase: Pointer;
    EntryPoint: Pointer;
    SizeOfImage: ULONG;
    {$IFDEF CPUX64}
    Padding: array[0..3] of Byte;         // Align FullDllName to 60
    {$ENDIF}
    FullDllName: UNICODE_STRING;
    BaseDllName: UNICODE_STRING;
  end;

  PPEB_LDR_DATA = ^PEB_LDR_DATA;
  PEB_LDR_DATA = packed record
    Length: ULONG;
    Initialized: Boolean;
    {$IFDEF CPUX64}
    Padding: array[0..2] of Byte;         // Align SsHandle to 8 bytes
    {$ENDIF}
    SsHandle: Pointer;
    InLoadOrderModuleList: LIST_ENTRY;
  end;

  PPEB = ^PEB;
  PEB = packed record
    Reserved: array[0..2] of Byte;
    BeingDebugged: Byte;
    {$IFDEF CPUX64}
    Reserved2: array[0..11] of Byte;      // Align Ldr to 24
    {$ELSE}
    Reserved2: array[0..3] of Byte;       // Align Ldr to 12
    {$ENDIF}
    ImageBaseAddress: Pointer;
    Ldr: PPEB_LDR_DATA;
  end;


function RtlGetCurrentPeb: Pointer; stdcall; external 'ntdll.dll';
function GetCurrentPEB: PPEB;
begin
  Result := RtlGetCurrentPeb;
end;

function AddressViolateMemoryRange(Address: Pointer): Boolean;
begin
  Result := (NativeUInt(Address) < $10000) or   // always in user mode then it shold be above 64kb
            // $BFFFFFFF when (/3G) and/or IMAGE_FILE_LARGE_ADDRESS_AWARE is enabled, when disabled it should be $7FFFFFFF
            (NativeUInt(Address) > {$IFDEF CPUX64} $7FFFFFFFFFFFFFFF {$ELSE} $BFFFFFFF {$ENDIF});
end;

function FindModuleByAddress(Address: Pointer): TModuleInfo;
var
  PEB: PPEB;
  LdrData: PPEB_LDR_DATA;
  ModuleEntry: PLDR_DATA_TABLE_ENTRY;
  CurrentEntry, FirstEntry: PLIST_ENTRY;
  ModuleBaseAddr, ModuleEndAddr: NativeUInt;
  FlinkPtr: PPointer;
  ProtCounter: Integer;   // protection counter against looping over ring doubly linked list
begin
  FillChar(Result, SizeOf(Result), 0);
  Result.IsValid := False;

  if not Assigned(Address) or AddressViolateMemoryRange(Address) then
    Exit;

  PEB := GetCurrentPEB;
  if not Assigned(PEB) or not Assigned(PEB^.Ldr) then
    Exit;

  LdrData := PEB^.Ldr;
  if not LdrData^.Initialized then
    Exit;

  FlinkPtr := PPointer(NativeUInt(LdrData) + Flink_OFFSET);     // Offset is 12 for 32bit and 24 for 64bit

  CurrentEntry := FlinkPtr^;
  if AddressViolateMemoryRange(CurrentEntry) then
    Exit;

  FirstEntry := CurrentEntry;       // protection against endless looping over ring list
  ProtCounter := 0;                 // protection against endless looping over ring list in case FirstEntry is unloaded after the fact

  while Assigned(CurrentEntry) and (not AddressViolateMemoryRange(CurrentEntry)) and
        (CurrentEntry <> PList_Entry(NativeUInt(LdrData) + Flink_OFFSET)) do
  begin

    ModuleEntry := PLDR_DATA_TABLE_ENTRY(CurrentEntry);
    try
      if Assigned(ModuleEntry^.DllBase) then
      begin
        ModuleBaseAddr := NativeUInt(ModuleEntry^.DllBase);
        ModuleEndAddr := ModuleBaseAddr + ModuleEntry^.SizeOfImage;
        if (NativeUInt(Address) >= ModuleBaseAddr) and (NativeUInt(Address) < ModuleEndAddr) then
        begin
          // found module that have the address
          Result.ModuleBase := ModuleEntry^.DllBase;
          Result.ModuleSize := ModuleEntry^.SizeOfImage;
          Result.EntryPoint := ModuleEntry^.EntryPoint;
          if Assigned(ModuleEntry^.BaseDllName.Buffer) and (ModuleEntry^.BaseDllName.Length > 0) then
            Result.ModuleName := ModuleEntry^.BaseDllName.Buffer;
          if Assigned(ModuleEntry^.FullDllName.Buffer) and (ModuleEntry^.FullDllName.Length > 0) then
            Result.FullPath := ModuleEntry^.FullDllName.Buffer;
          Result.IsValid := True;
          Exit;
        end;
      end;
    except
      // Ignore access violations, do not handle anything here
    end;

    if ProtCounter >= LOOPING_PROTECTION_LIMIT then
      Exit;
    Inc(ProtCounter);

    CurrentEntry := CurrentEntry^.Flink;
    if CurrentEntry = FirstEntry then
      Break;
  end;
end;

end.

 

Share this post


Link to post
10 hours ago, Kas Ob. said:

Here a more precise and protected version will less redundant code

Thanks. I have already implemented the notification system using LdrRegisterDllNotification as suggested by Anders, and from user feedback it seems to prevent the annoying A/Vs. I'm goint to test how your suggestion performs, but I reckon it would be hard to beat caching of an address space map vs walking the list of modules before every stack trace.

 

Currently I only map the low 4GB of address space, and assume that no DLL will be loaded above that. It's not an accurate assumption, but it seems to hold where it matters. If it becomes an issue I will definitely have to revisit this.

Share this post


Link to post
13 hours ago, Pierre le Riche said:

but I reckon it would be hard to beat caching of an address space map vs walking the list of modules before every stack trace.

Yes, it might be be slower, but the idea is even with your cache you will have to walk it, and if you re-refactored my code above it is really just loop over linked-list, 

 

In case you want to test it, then notice that PEB is immovable like its LdrData, so that part can be in initialization clause, this will leave the code close to 

 

function FindModuleByAddress(Address: Pointer): TModuleInfo;
var
  ModuleEntry: PLDR_DATA_TABLE_ENTRY;
  CurrentEntry, FirstEntry: PLIST_ENTRY;
  ModuleBaseAddr, ModuleEndAddr: NativeUInt;
  FlinkPtr: PPointer;
  ProtCounter: Integer;   // protection counter against looping over ring doubly linked list
begin
  Result.IsValid := False;

  FlinkPtr := PPointer(NativeUInt(LdrData) + Flink_OFFSET);     // Offset is 12 for 32bit and 24 for 64bit

  CurrentEntry := FlinkPtr^;
  if AddressViolateMemoryRange(CurrentEntry) then
    Exit;

  FirstEntry := CurrentEntry;       // protection against endless looping over ring list
  ProtCounter := 0;                 // protection against endless looping over ring list in case FirstEntry is unloaded after the fact

  while Assigned(CurrentEntry) and (not AddressViolateMemoryRange(CurrentEntry)) and
        (CurrentEntry <> PList_Entry(NativeUInt(LdrData) + Flink_OFFSET)) do
  begin

    ModuleEntry := PLDR_DATA_TABLE_ENTRY(CurrentEntry);
    try
      if Assigned(ModuleEntry^.DllBase) then
      begin
        ModuleBaseAddr := NativeUInt(ModuleEntry^.DllBase);
        ModuleEndAddr := ModuleBaseAddr + ModuleEntry^.SizeOfImage;
        if (NativeUInt(Address) >= ModuleBaseAddr) and (NativeUInt(Address) < ModuleEndAddr) then
        begin
          // found module that have the address
          ..
          Result.IsValid := True;
          Exit;
        end;
      end;
    except
      // Ignore access violations, do not handle anything here
    end;

    if ProtCounter >= LOOPING_PROTECTION_LIMIT then
      Exit;
    Inc(ProtCounter);

    CurrentEntry := CurrentEntry^.Flink;
    if CurrentEntry = FirstEntry then
      Break;
  end;
end;

As for caching, i think you are the best one to decide if you want to cache few (may be 3 and that it) modules addresses that will be %90 of the times in any stack call, the EXE module itself and kernel and User32, saving these 3 will make the protection counter way better than random 1000, by comparing against loaded EXE address and trigger exit after second occurrence, this will prevent loop up to 1000.

 

In all cases, you are welcome.

  • Like 1

Share this post


Link to post
13 hours ago, Pierre le Riche said:

Currently I only map the low 4GB of address space,

It just hit me, 

 

Don't cache them all, as some DLL will never show in the stack, like never, even very common libraries like "AdvApi32.dll" or "Ws2_32.dll" will not be on stack call unless they used callback (used once), so if cached only the found ones you will end up with short list and sorted by importance and appearance. 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×