Pierre le Riche 21 Posted July 18 (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 July 18 by Pierre le Riche Share this post Link to post
Anders Melander 2037 Posted July 19 LdrRegisterDllNotification (Vista+) LdrInitShimEngineDynamic (undocumented, XP+ AFAIK) 2 Share this post Link to post
Pierre le Riche 21 Posted July 19 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
Kas Ob. 148 Posted Sunday at 04:18 PM @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
Kas Ob. 148 Posted Sunday at 04:36 PM 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
Kas Ob. 148 Posted Monday at 07:41 AM 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
Pierre le Riche 21 Posted Monday at 06:14 PM 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
Kas Ob. 148 Posted Tuesday at 08:06 AM 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. 1 Share this post Link to post
Kas Ob. 148 Posted Tuesday at 08:17 AM 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