Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows 11 24H2 #1

Closed
jarroddavis68 opened this issue Dec 8, 2024 · 32 comments
Closed

Windows 11 24H2 #1

jarroddavis68 opened this issue Dec 8, 2024 · 32 comments
Assignees

Comments

@jarroddavis68
Copy link

Hi, will this work in windows 24H2?

@EvanMcBroom
Copy link
Owner

Yes

@jarroddavis68
Copy link
Author

ahh, ok, I wonder what I'm doing wrong with my library. It will refuse to work. I based from the libs you reference in the readme. It's a Delphi adaptation.
https://github.com/tinyBigGAMES/MemoryDLL

@EvanMcBroom
Copy link
Owner

Ah, interesting. I know the concepts themselves work for 24H2, but I just tested the library for this project and reproduced the issue you experienced. I'll troubleshoot this and work on a fix.

@EvanMcBroom EvanMcBroom reopened this Dec 11, 2024
@EvanMcBroom
Copy link
Owner

EvanMcBroom commented Dec 12, 2024

Still need to troubleshoot why the normal manual mapping approach is not currently working in this library on 24H2. The concept still works so it must be an implementation issue. I did test and find that Module Doppelgänging does still work fine on 24H2 if that'd help your project. I'll update this issue once I have more information.

@jarroddavis68
Copy link
Author

I see. I used the redirecting LoadLibrary approach, being the easier one to understand. I've been trying many things to get it working, but I just don't understand what is going wrong in 24H2.

I guess I need to try and understand Module Doppelganging. Also is it possible to have a C interface to your lib? and will it work from a DLL?

@EvanMcBroom
Copy link
Owner

I guess I need to try and understand Module Doppelgänging

If it helps, I believe the approach can be simplified down to this:

  1. Hook NtOpenFile
  2. In NtOpenFile, check if the requested path matches what you provided to LoadLibrary. If it does:
    1. Remove the hook
    2. Create a transaction and call RtlSetCurrentTransaction to have all file IO done on the current thread be done within the transaction
    3. Open the file with write access and overwrite the file with the bytes of your in-memory DLL. This write will occur within the transaction and will not affect the file as it exists on the file system
    4. Close that handle, reopen the file with read access, and return that handle as the output parameter for NtOpenFile
  3. Once LoadLibrary completes, call RtlSetCurrentTransaction again to remove the transaction from your thread, call RollbackTransaction, and finally call CloseHandle on the transaction handle (which it appears I missed doing in this library)

Also is it possible to have a C interface to your lib?

I'd be happy to add this. I need to fix some things with the library (ex. making the manual mapping approach work on 24H2) so I can add this in with that work. Would this definition meet your needs?

/// <summary>
///     Loads the provided module bytes into the address space of the calling process.
///     The process of loading the module may cause other modules to be loaded.
/// </summary>
/// <param name='DllBase'>
///     The address to the bytes of the module to load.
/// </param>
/// <param name='DllSize'>
///     The size in bytes of the module to load.
/// </param>
/// <param name='Flags'>
///     The action to be taken when loading the module.
///     Refer to the LoadLibraryEx* documentation for a full list of accepted values.
/// </param>
/// <param name='FileName'>
///     Refer to the Pl::LoadLibrary documentation for detailed information.
///     If no value is supplied then an empty temporary file will be created and its path will be used.
/// </param>
/// <param name='PlFlags'>
///     The approach and post processing options for loading the module.
///     Refer to the Pl::LoadFlags documentation for a full list of accepted values.
/// </param>
/// <param name='ModListName'>
///     An optional name to use as the DLL name in the module list.
///     fileName will be used if nothing is specified.
///     Only valid when used with the Pl::UseTxf flag.
/// </param>
/// <returns>
///     If the function succeeds, the return value is a handle to the loaded module.
///     If the function fails, the return value is NULL. To get extended error information, call GetLastError.
/// </returns>
HMODULE LoadDllFromMemory([in] LPVOID DllBase, [in] SIZE_T DllSize, [in, optional] DWORD Flags, [in, optional] LPCWSTR FileName, [in, optional] DWORD PlFlags, [in, optional] LPCWSTR ModListName);

will it work from a DLL?

It should, yes.

@jarroddavis68
Copy link
Author

thanks, that would be great.

I tried doing something similar, but it will just crash for me:

#ifdef __cplusplus
extern "C" {
#endif

// Define the C wrapper function
__declspec(dllexport) HMODULE LoadLibraryWrapper(
    const wchar_t* fileName,
    const uint8_t* bytes,
    size_t bytesSize,
    DWORD flags,
    const wchar_t* modListName,
    DWORD nativeFlags) {
    // Forward declare the original function
    HMODULE LoadLibrary(const std::wstring& fileName,
        const std::vector<std::byte>& bytes,
        DWORD flags,
        const std::wstring& modListName,
        DWORD nativeFlags);

    // Convert C types to C++ types
    std::wstring cppFileName(fileName);
    std::vector<std::byte> cppBytes(reinterpret_cast<const std::byte*>(bytes), reinterpret_cast<const std::byte*>(bytes) + bytesSize);
    std::wstring cppModListName(modListName);

    // Call the original function and return the result
    return Pl::LoadLibrary(cppFileName, cppBytes, flags, cppModListName, nativeFlags);
}


#ifdef __cplusplus
}
#endif

it's probably that code, I'm, not a c/c++ programmer. I know enough to try and adapt for Dephi.

And I will look over those steps you outlined, many thanks.

@EvanMcBroom EvanMcBroom changed the title Winodows 24H2 Windows 24H2 Dec 24, 2024
@EvanMcBroom
Copy link
Owner

Hey @jarroddavis68, I added a C API for the project which is exposed by the pl.dll build artifact. Apologies for the delay on that. Here's a short example for using it that I believe will work for your project on 24H2:

#include "perfect_loader.h"

int wmain(int argc, wchar_t** argv) {
    // snippet ...
    HMODULE library = LoadDllFromMemory(dllBytes, dllSize, 0, fileName, PL_LOAD_FLAGS_USETXF, L"");
    // snippet ...
}

The fileName value will ultimately be passed as the first argument to LoadLibrary but it does not have to be a valid DLL. It just needs to be a valid path that LoadLibrary can resolve (ex. C:\Windows\Temp\example.txt) and it must be a file that your current user has permissions to write to. Although your user will need permissions to write to the file, the file's contents will only be modified in memory (in a temporary file transaction) which will not affect the file on disk.

The library's other technique for reflectively loading a library (manually mapping a DLL in memory then redirecting LoadLibrary via a hook to use that mapping) does still work on 24H2, but there are some implementation issues specific to this library that I'll need to fix which currently cause the library to fail for that Windows release. It'll be a little longer before I'll be able to implement the fixes for those, but I'll keep this ticket open in the meantime until that's resolved.

I hope this helps your work, and thank you for noticing the failure on 24H2 and filling the issue! 🙂

@jarroddavis68
Copy link
Author

jarroddavis68 commented Dec 24, 2024

Hey @jarroddavis68, I added a C API for the project which is exposed by the pl.dll build artifact. Apologies for the delay on that. Here's a short example for using it that I believe will work for your project on 24H2:

Many Thanks!

The library's other technique for reflectively loading a library (manually mapping a DLL in memory then redirecting LoadLibrary via a hook to use that mapping) does still work on 24H2, but there are some implementation issues specific to this library that I'll need to fix which currently cause the library to fail for that Windows release. It'll be a little longer before I'll be able to implement the fixes for those, but I'll keep this ticket open in the meantime until that's resolved.

The concept continued to work for me too, the hooks would fire, but when LoadLibrary returns, it will always be NULL.

I hope this helps your work, and thank you for noticing the failure on 24H2 and filling the issue! 🙂

I will check this out, again, thank you!

@EvanMcBroom
Copy link
Owner

Sure thing! For the issue regarding manual mapping, at some point Microsoft introduced logic in the loader such that the loader will reattempt mapping the section one additional time when our library returns STATUS_IMAGE_NOT_AT_BASE in its NtMapViewOfSection hook. I have tested having the library maintain its hook on NtMapViewOfSection for that one additional reattempt the loader does which appears to work well on 24H2. So that should be the root cause of the issue and the fix, but just want to clean up the new code for that fix before adding it in.

@jarroddavis68
Copy link
Author

here is my hooked NtMapViewOfSection code, how can I adapt it to what you described above. It never seems to get called again.

function HookNtMapViewOfSection(
  ASectionHandle: THandle;
  AProcessHandle: THandle;
  ABaseAddress: PPVOID;
  AZeroBits: ULONG_PTR;
  ACommitSize: SIZE_T;
  ASectionOffset: PLARGEINTEGER;
  AViewSize: PSIZE_T;
  AInheritDisposition: ULONG;
  AAllocationType: ULONG;
  AWin32Protect: ULONG
): NTSTATUS; stdcall;
var
  LOldNtMapViewOfSection: TNtMapViewOfSection; // Original NtMapViewOfSection function
begin
  // Stop the current hook to prevent recursion
  NtMapViewOfSectionHook.Stop();

  if DllData = nil then
  begin
    // Retrieve the original NtMapViewOfSection function pointer
    LOldNtMapViewOfSection := TNtMapViewOfSection(NtMapViewOfSectionHook.GetOldFunction());
    // Call the original NtMapViewOfSection function with provided parameters
    Result := LOldNtMapViewOfSection(
      ASectionHandle,
      AProcessHandle,
      ABaseAddress^,
      AZeroBits,
      ACommitSize,
      ASectionOffset,
      AViewSize^,
      AInheritDisposition,
      AAllocationType,
      AWin32Protect
    );
    Exit;
  end;

  // Map the DLL into memory
  HookMapDll(DllData, ABaseAddress^, AViewSize^);
  DllData := nil; // Clear DLL data after mapping

  // Return a custom NTSTATUS code indicating successful mapping
  Result := $40000003;
end;

function HookNtMapViewOfSection(
  ASectionHandle: THandle;
  AProcessHandle: THandle;
  ABaseAddress: PPVOID;
  AZeroBits: ULONG_PTR;
  ACommitSize: SIZE_T;
  ASectionOffset: PLARGEINTEGER;
  AViewSize: PSIZE_T;
  AInheritDisposition: ULONG;
  AAllocationType: ULONG;
  AWin32Protect: ULONG
): NTSTATUS; stdcall;
var
  LOldNtMapViewOfSection: TNtMapViewOfSection;
begin
  // Stop the current hook to prevent recursion
  NtMapViewOfSectionHook.Stop();

  try
    // Retrieve the original NtMapViewOfSection function pointer
    LOldNtMapViewOfSection := TNtMapViewOfSection(NtMapViewOfSectionHook.GetOldFunction());

    // Perform manual mapping if DllData is set
    if DllData <> nil then
    begin
      HookMapDll(DllData, ABaseAddress^, AViewSize^);
      DllData := nil; // Clear DLL data after mapping
      Result := $40000003; // Indicate successful mapping
      Exit;
    end;

    // Call the original NtMapViewOfSection function
    Result := LOldNtMapViewOfSection(
      ASectionHandle,
      AProcessHandle,
      ABaseAddress^,
      AZeroBits,
      ACommitSize,
      ASectionOffset,
      AViewSize^,
      AInheritDisposition,
      AAllocationType,
      AWin32Protect
    );

    // Handle retry for STATUS_IMAGE_NOT_AT_BASE
    if Result = STATUS_IMAGE_NOT_AT_BASE then
    begin
      // Retry mapping logic for Windows 11 24H2 compatibility
      NtMapViewOfSectionHook.Start('ntdll.dll', 'NtMapViewOfSection', FARPROC(@HookNtMapViewOfSection));
      Result := LOldNtMapViewOfSection(
        ASectionHandle,
        AProcessHandle,
        ABaseAddress^,
        AZeroBits,
        ACommitSize,
        ASectionOffset,
        AViewSize^,
        AInheritDisposition,
        AAllocationType,
        AWin32Protect
      );
    end;

  finally
    // Restart the hook to ensure future calls are handled
    NtMapViewOfSectionHook.Start('ntdll.dll', 'NtMapViewOfSection', FARPROC(@HookNtMapViewOfSection));
  end;
end;

function HookNtOpenFile(
  AFileHandle: PHANDLE;
  ADesiredAccess: ACCESS_MASK;
  AObjectAttributes: POBJECT_ATTRIBUTES;
  AIoStatusBlock: PIO_STATUS_BLOCK;
  AShareAccess: ULONG;
  AOpenOptions: ULONG
): NTSTATUS; stdcall;
var
  LResultStatus: NTSTATUS;            // Result status from the original NtOpenFile
  LOldNtOpenFile: TNtOpenFile;        // Original NtOpenFile function
begin
  // Temporarily suspend the NtOpenFile hook to avoid recursion
  NtOpenFileHook.Suspended();

  // Retrieve the original NtOpenFile function pointer
  LOldNtOpenFile := TNtOpenFile(NtOpenFileHook.GetOldFunction());

  // Call the original NtOpenFile function with provided parameters
  LResultStatus := LOldNtOpenFile(
    AFileHandle,
    ADesiredAccess,
    AObjectAttributes,
    AIoStatusBlock,
    AShareAccess,
    AOpenOptions
  );

  // Check if the ObjectName contains the hook reference DLL name
  if wcsstr(AObjectAttributes^.ObjectName^.Buffer, HOOK_REFERENCE_DLL) <> nil then
  begin
    // Stop the NtOpenFile hook and start the NtMapViewOfSection hook
    NtOpenFileHook.Stop();
    NtMapViewOfSectionHook.Start('ntdll.dll', 'NtMapViewOfSection', FARPROC(@HookNtMapViewOfSection));
    Exit(LResultStatus); // Return the original result status
  end;

  // Restore the NtOpenFile hook before returning
  NtOpenFileHook.Restore;
  Result := LResultStatus;
end;

function MemoryLoadLibrary(const AData: Pointer): THandle;
begin
  // Enter critical section to ensure thread-safe operations
  CriticalSection.Enter();
  try
    // Start hooking the NtOpenFile function in ntdll.dll with HookNtOpenFile as the replacement
    NtOpenFileHook.Start('ntdll.dll', 'NtOpenFile', @HookNtOpenFile);
    DllData := AData; // Store the DLL data pointer
    Result := LoadLibraryW(HOOK_REFERENCE_DLL); // Load the reference DLL using wide-character API
  finally
    // Leave the critical section after operations are complete
    CriticalSection.Leave();
  end;
end;

@EvanMcBroom
Copy link
Owner

Hey @jarroddavis68, I have never written Pascal code before and as such I'm not equipped to help troubleshoot your code. I wish I could provide better help with that and I'm sorry I can't 😕

By chance, did you attempt using the C interface that was added for your request to see if it'd solve your need?

@jarroddavis68
Copy link
Author

Hey @jarroddavis68, I have never written Pascal code before and as such I'm not equipped to help troubleshoot your code. I wish I could provide better help with that and I'm sorry I can't 😕

No worries, understand.

By chance, did you attempt using the C interface that was added for your request to see if it'd solve your need?

Yes, while it does work, the library itself is c++ as such I'm unable to used it from Delphi without using it as a DLL, which defeats the purpose of not being able to use DLLs, heh.

Delphi can statically link to a C library however using {$L perfect-loader.o}, where I can use C++ Builder 12.2 (part of RAD Studio along with Delphi) to compile the code to an .o and that .o is compatible with Delphi, since it can handle C ABI only.

@EvanMcBroom
Copy link
Owner

Ah dang, that makes sense and its unfortunate it wouldn't work in for your use case in its current form. I appreciate the extra insight!

@jarroddavis68
Copy link
Author

Ok, I think I may have a working solution for Delphi that can use perfect-loader. Seems to be working. Let me do a few more tests and I will report back on how I got it working. 🫰

@jarroddavis68
Copy link
Author

jarroddavis68 commented Jan 3, 2025

Everything seems to be working on par with OS LoadLibrary. Here is what I did to get it working with Delphi:

  • Compiled perfect-loader to pl.dll
  • Converted pl.dll to a static constant byte array, added it to my MemoryDLL unit
  • Used a traditional dll memory loader to load pl.dll into memory and bind to exported function
  • Create a temp text file with a GUID filename in the OS user temp folder
  • Use LoadDllFromMemory to load user requested DLL into memory, using the temp text file created above
  • On app shutdown, free pl.dll, delete the temp text file.

I will be reactivating my MemoryDLL project after a bit more testing.

Many thanks my friend! 👏🏿

// Pprovides functionality for dynamically loading a DLL from memory,
// managing its lifecycle, and cleaning up resources upon application shutdown.

var
  // Handle to the loaded DLL. Initially nil.
  DllHandle: Pointer = nil;

  // Temporary file name used for storing data related to the DLL loading process.
  TempFilename: string = '';

// Loads a DLL from memory.
// @param AData Pointer to the memory block containing the DLL data.
// @param ASize Size of the memory block in bytes.
// @return Handle to the loaded DLL on success; 0 on failure.
function LoadMemoryDLL(const AData: Pointer; const ASize: NativeUInt): THandle;
begin
  // Calls an external function to load the DLL from memory.
  Result := LoadDllFromMemory(AData, ASize, 0, PChar(TempFilename), PL_LOAD_FLAGS_USETXF, nil);
end;

// Loads the custom DLL and initializes its exported function.
// @param AError Output parameter that stores the error message, if any.
// @return True if the DLL is loaded successfully; False otherwise.
function LoadDLL(var AError: string): Boolean;
begin
  Result := False;

  // Check if the DLL is already loaded.
  if Assigned(DllHandle) then
  begin
    Result := True;
    Exit;
  end;

  // Load the DLL into memory using a custom loader function.
  DllHandle := MemoryLoadLibrary(@PERFECT_LOADER[0]);
  if not Assigned(DllHandle) then
  begin
    AError := 'Unable to load perfect-loader dll';
    Exit;
  end;

  // Retrieve the address of the `LoadDllFromMemory` function from the loaded DLL.
  LoadDllFromMemory := MemoryGetProcAddress(DllHandle, 'LoadDllFromMemory');
  if not Assigned(LoadDllFromMemory) then
  begin
    AError := 'Unable to get perfect-loader dll exports';
    Exit;
  end;

  // Generate a temporary file name for auxiliary operations.
  TempFilename := TPath.Combine(TPath.GetTempPath, TPath.GetGUIDFileName + '.txt');

  // Write a dummy text to the temporary file to verify file system access.
  TFile.WriteAllText(TempFilename, 'MemoryDLL');

  // Verify that the temporary file exists.
  Result := TFile.Exists(TempFilename);
end;

// Unloads the DLL and releases allocated resources.
procedure UnloadDLL();
begin
  // Check if the DLL handle is valid.
  if not Assigned(DllHandle) then Exit;

  // Free the loaded DLL from memory.
  MemoryFreeLibrary(DllHandle);
  DllHandle := nil;

  // Delete the temporary file, if it exists.
  if TFile.Exists(TempFilename) then
    TFile.Delete(TempFilename);
end;

// Initialization block to load the DLL during application startup.
initialization
var
  LError: string;
begin
  // Enable memory leak reporting for debugging purposes.
  ReportMemoryLeaksOnShutdown := True;

  try
    // Attempt to load the DLL. Terminate the application on failure.
    if not LoadDLL(LError) then
    begin
      MessageBox(0, PChar(LError), 'Critical Initialization Error', MB_ICONERROR);
      Halt(1);
    end;
  except
    on E: Exception do
    begin
      // Display any exceptions encountered during initialization.
      MessageBox(0, PChar(E.Message), 'Critical Initialization Error', MB_ICONERROR);
    end;
  end;
end;

// Finalization block to clean up resources during application shutdown.
finalization
begin
  try
    // Unload the DLL and delete temporary files.
    UnloadDLL();
  except
    on E: Exception do
    begin
      // Display any exceptions encountered during finalization.
      MessageBox(0, PChar(E.Message), 'Critical Shutdown Error', MB_ICONERROR);
    end;
  end;
end;

@EvanMcBroom
Copy link
Owner

That's fantastic news! I'm so happy it's able to provide a solution for your project. Awesome work! 😄

@jarroddavis68
Copy link
Author

Ok, repo updated and now using perfect-loader. Thanks again for all your help!
https://github.com/tinyBigGAMES/MemoryDLL

@jarroddavis68
Copy link
Author

Sure thing! For the issue regarding manual mapping, at some point Microsoft introduced logic in the loader such that the loader will reattempt mapping the section one additional time when our library returns STATUS_IMAGE_NOT_AT_BASE in its NtMapViewOfSection hook. I have tested having the library maintain its hook on NtMapViewOfSection for that one additional reattempt the loader does which appears to work well on 24H2. So that should be the root cause of the issue and the fix, but just want to clean up the new code for that fix before adding it in.

Ok, I've been playing around trying to get this to work, I noticed it will attempt 3 times, then succeed, but when I GetProcAddress to a routine and try to call it, I will get a DEP error. Any ideas?

@EvanMcBroom
Copy link
Owner

EvanMcBroom commented Jan 5, 2025

Oh odd. iirc, I only saw it reattempt once in my testing but I could definitely be misremembering 🤔

What path did you provide to LoadLibray to kickoff the loading process? I feel like it may be due to a different characteristic of the DLL causing LoadLibray to do different processing. I chatted with @rbmm previously who developed the same method for a different project (link) and he mentioned he needed to provide the path for a DLL "without CFG that is larger than the in-memory library you intend to load." @rbmm, do you know if the DEP error @jarroddavis68 experienced is related to this maybe due to something different?

@jarroddavis68
Copy link
Author

memdorydll.mp4
function HookNtMapViewOfSection(
  ASectionHandle: THandle;
  AProcessHandle: THandle;
  ABaseAddress: PPVOID;
  AZeroBits: ULONG_PTR;
  ACommitSize: SIZE_T;
  ASectionOffset: PLARGEINTEGER;
  AViewSize: PSIZE_T;
  AInheritDisposition: ULONG;
  AAllocationType: ULONG;
  AWin32Protect: ULONG
): NTSTATUS; stdcall;
begin
  inc(count);
  writeln('HookNtMapViewOfSection Attempt #: ', count);
  // Only do our mapping if we have DLL data
  HookMapDll(DllData, ABaseAddress^, AViewSize^);
  BaseAddress := THandle(ABaseAddress^);
  // Always return STATUS_IMAGE_NOT_AT_BASE to let loader retry
  Result := STATUS_IMAGE_NOT_AT_BASE;
end;

I'm using advapi32res.dll as the reference DLL.

@jarroddavis68
Copy link
Author

Interesting, A-Normal-User, said failed to work also on 24H2, but after disabling security, it worked. Hmmm. I've been working on this for the past few days, and I don't know what else to try at this point. We obviously cannot ask a user to disable security, so what can be done I wonder?

@EvanMcBroom
Copy link
Owner

What do you mean by "disable security?" I ask to ensure I have not missed a test case, but I have gotten the manual mapping approach to work on 24H2 without disabling anything. I just need time to provide an update to the library.

We obviously cannot ask a user to disable security, so what can be done I wonder?

The alternative Module Doppelgänging approach works without any needed updates. Do you have a specific need for the manual mapping approach instead of this one?

@jarroddavis68
Copy link
Author

I'm just sharing what he told me:
"Hello! After I upgraded to version 24H2, I also experienced a situation where it was not possible to use my program.
But more interest. Today, after I uninstalled Windows Defender and disabled Virtualization-based Security, I was able to use it normally. I suspect that a certain setting (or a new security policy) within the security center might have been responsible for this issue."

no, what you have is working fine for me. I was just playing around trying to see if I could get my original 23H2 code to work on 24H2. It's pure pascal and worked perfect on 23H2. It would just be cool if I could also get it working. I usually have two projects going on, a main one and a side project. When I get tired, bored, stumped on one, I jump on the order just to keep the creative juices flowing, heh.

@rbmm
Copy link
Contributor

rbmm commented Jan 6, 2025

do you know if the DEP error

if you post the binary file(s) - I can look at it under the debugger and tell you what actually happened, where the error is

you can also look/test link : load.exe (static linked code) or load2.exe ( shell code used for load dll) or load3.exe ( compressed shell code) (the same for x86 too ) with dll.dll or any your dll

and yes, if ZwMapViewOfSection return STATUS_IMAGE_NOT_AT_BASE loader by some reason unload dll and then reload it again. i dont understand sense of this. in general secuense is next:

ntdll.dll!ZwMapViewOfSection
ntdll.dll!LdrpMapViewOfSection + 6c
ntdll.dll!LdrpMinimalMapModule + 116
ntdll.dll!LdrpMapDllWithSectionHandle + 18
ntdll.dll!LdrpMapDllNtFileName + 19b
ntdll.dll!LdrpMapDllFullPath + e0
ntdll.dll!LdrpProcessWork + 77
ntdll.dll!LdrpLoadDllInternal + 1a0
ntdll.dll!LdrpLoadDll + b0
ntdll.dll!LdrLoadDll + fa


Load: base=000001DB3DB60000, size=000db000, ep=0000000000000000 \Device\HarddiskVolume9\Windows\System32\adtschema.dll

LdrpCheckForRetryLoading

Unload: base=000001DB3DB60000 \Device\HarddiskVolume9\Windows\System32\adtschema.dll

ntdll.dll!ZwMapViewOfSection
ntdll.dll!LdrpMapViewOfSection + 6c
ntdll.dll!LdrpMinimalMapModule + 116
ntdll.dll!LdrpMapDllWithSectionHandle + 18
ntdll.dll!LdrpMapDllNtFileName + 19b
ntdll.dll!LdrpMapDllRetry + a8
ntdll.dll!LdrpProcessWork + 152
ntdll.dll!LdrpDrainWorkQueue + 184
ntdll.dll!LdrpLoadDllInternal + 1ac
ntdll.dll!LdrpLoadDll + b0
ntdll.dll!LdrLoadDll + fa


Load: base=000001DB3DB60000, size=000db000, ep=0000000000000000 \Device\HarddiskVolume9\Windows\System32\adtschema.dll
Unload: base=000001DB3DB60000 \Device\HarddiskVolume9\Windows\System32\adtschema.dll

@EvanMcBroom
Copy link
Owner

EvanMcBroom commented Jan 8, 2025

@rbmm, thank you for the response! It's helpful to see that you are experiencing LoadLibrary attempting to remap the file mapping when STATUS_IMAGE_NOT_AT_BASE is returned as well. I appreciate your help! 🙂

no, what you have is working fine for me. I was just playing around trying to see if I could get my original 23H2 code to work on 24H2. It's pure pascal and worked perfect on 23H2. It would just be cool if I could also get it working. I usually have two projects going on, a main one and a side project. When I get tired, bored, stumped on one, I jump on the order just to keep the creative juices flowing, heh.

That makes since, and I totally understand! I'm the same way regarding the main and side projects 👍

Got an update that I believe will help you @jarroddavis68. Admittedly, I thought I was testing on 24H2 but was only testing on 24H1 🤦‍♂️. My apologies for the bad testing on my end! I have been troubleshooting since Monday and have manual mapping finally (and actually this time) working again for 24H2.

The first fix that needed to occur is to maintain the hook on NtMapViewOfSection until LoadLibrary returns to handle anytime LoadLibrary reattempted its mapping of a dll. Code to reattempt mapping was introduced to LoadLibrary before 24H2, but I had not updated this library to support that behavior yet. That issue is now fixed.

As a side note, after much more testing, I still have only encountered LoadLibrary reattempted its mapping of a dll only once, which aligns with what @rbmm experienced and how LdrpMinimalMapModule and LdrpCheckForRetryLoading is written. That being said, I have experienced additional calls to NtMapViewOfSection when other 3rd party software was installed on a host (specifically, 3rd party EDRs). I'm curious if there's additional software on your host that is potentially causing the additional reattempts? Regardless of the cause, I feel that maintaining the hook until LoadLibrary returns is the best approach to handle any reattempts that may occur.

Now: Here is the relevant change to Windows that caused the other issues for the project on 24H2. On 24H2, Microsoft introduced a new technology called SCPCFG. There's a great blog post on the technology written by Miloš (ynwarcs) which you can access here:

SCPCFG is not a security mitigation itself, but it allows Microsoft to hot patch PE modules (e.g., apply code updates without requiring a reboot). Microsoft added additional code to support SCPCFG in a few places, including LoadLibrary. When LoadLibrary does its post processing after mapping a module (ex. to process relocations), it will now additionally check for SCPCFG data and process it if present.

Here is why that caused an issue for the manual mapping approach. In a nutshell, when a module has SCPCFG data and is mapped like normal, you can query information about that data via NtQueryVirtualMemory. You can also work with that data via NtManageHotPatch. LoadLibrary will attempt to do both of these actions, which will fail for the manual mapping approach because LoadLibrary will be using these syscalls to work with our in-memory bytes and not a normal file mapping.

This specifically happens in two locations and both need to be accounted for. The first is in LdrpProcessMappedModule which on 24H2 added a call to RtlpInsertOrRemoveScpCfgFunctionTable to process SCPCFG if present. Here is my pseudo code for the function (note: the referenced types can be found in phnt):

void RtlpInsertOrRemoveScpCfgFunctionTable(PVOID DllBase, bool Reserved, char Insert) {
    MEMORY_IMAGE_EXTENSION_INFORMATION memoryInfo = { 0 };
    SIZE_T returnLength = 0;
    NTSTATUS status = NtQueryVirtualMemory(-1, DllBase, MemoryImageExtensionInformation, &memoryInfo, sizeof(memoryInfo), &returnLength);
    if (status != STATUS_NOT_SUPPORTED && NT_SUCCESS(status) && memoryInfo.ExtensionSize != 0) {
        PRTL_SCP_CFG_HEADER scpCfg = (PRTL_SCP_CFG_HEADER)((size_t)DllBase + MemoryInfo.ExtensionImageBaseRva);
        ULONG fnTableRva = (scpCfg->Common).FnTableRva;
        if (fnTableRva) {
            PRUNTIME_FUNCTION FunctionTable = (PRUNTIME_FUNCTION)((size_t)scpCfg + fnTableRva);
            if (Insert) {
                PVOID DynamicTable;
                status = RtlAddGrowableFunctionTable(&DynamicTable, FunctionTable, 1, 1, RangeBase, RangeBase + memoryInfo.ExtensionSize);
            }
            else {
                status = RtlDeleteFunctionTable(FunctionTable);
            }
        }
    }
    return status;
}

Here is how to account for this code:

  1. Hook NtQueryVirtualMemory
  2. When called, check for if MemoryImageExtensionInformation was requested for your in-memory dll
  3. If so, return STATUS_NOT_SUPPORTED, otherwise issue the syscall like normal and return its result

The second location is in LdrpMapAndSnapDependency which on 24H2 added a call to LdrpQueryCurrentPatch to query for patch information. Here is my pseudo code for the function:

// The type name is based off of the operation names defined in
// chc's hot patch testing repo:
// https://github.com/chc/NtManageHotpatchTests
//
// The structure of the type was identified through manual auditing.
typedef struct _QUERY_SINGLE_LOADED_PATCH {
    ULONG Version; // Valid versions are 1 or above
    ULONG Unknown1;
    LPVOID Unknown2;
    LPVOID DllBase;
    GUID Unknown3; // Type was assumed based on its length and may be wrong
    LPVOID Unknown4;
} QUERY_SINGLE_LOADED_PATCH, *PQUERY_SINGLE_LOADED_PATCH;

NTSTATUS NTAPI NtManageHotPatch(ULONG Operation, PVOID SubmitBuffer, ULONG SubmitBufferLength, NTSTATUS* OperationStatus);

NTSTATUS LdrpQueryCurrentPatch(LPVOID DllBase, PQUERY_SINGLE_LOADED_PATCH Buffer) {
    Buffer.Version = 1;
    Buffer.Unknown1 = 0;
    Buffer.Unknown2 = -1;
    Buffer.DllBase = DllBase;
    Buffer.Unknown3 = { 0 };
    Buffer.Unknown4 = (LPVOID)0x0;
    NTSTATUS operationStatus = 0;
    // The operation name for argument 1 was also taken from chc's repo.
    NTSTATUS status = NtManageHotPatch(OPERATION_QUERY_SINGLE_LOADED_PATCH, &buffer, sizeof(buffer), &operationStatus);
    if (status == STATUS_BUFFER_TOO_SMALL) {
        return STATUS_PATCH_CONFLICT;
    }
    return status;
}

There are multiple ways to account for this code. The code is only called if an ntdll global variable named LdrpIsHotPatchingEnabled is set to a non-zero value (e.g., if it is true). If symbols are available, you can resolve the address of LdrpIsHotPatchingEnabled and manually set it to zero. Doing so will have LoadLibrary skip calling LdrpQueryCurrentPatch altogether. You can alternatively disable SCPCFG for a process by adding a registry entry for the undocumented ImageExpansionMitigation image file execution option (IFEO) for an executable and setting it to 2. Doing so will cause ntdll to set ImageExpansionMitigation to zero during the initialization code for a process. If both of these methods aren't feasible for a situation, you can also follow these steps to account for this code:

  1. Hook NtManageHotPatch
  2. When called, check for if OPERATION_QUERY_SINGLE_LOADED_PATCH was requested. You can optionally do an additional filter in this step for if the call references your in-memory dll by inspecting the passed in buffer
  3. If your check in step 2 succeeds, set SubmitBuffer to zero and return a success value (ex. STATUS_SUCCESS). Although not checked, OperationStatus should also be set to zero. If you check in step 2 does not succeed, issue the syscall like normal and return its result

The perfect loader library now implements the last method to account for LdrpQueryCurrentPatch. It does not do the additional filter in step 2 because we do not know yet if the format of the input buffer will stay stable with new Windows releases.

Again, I apologize for my bad testing earlier which made me think the project was working! I appreciate you reporting the issue and helping work through a solution for Windows 11 24H2! 🙏

I am going to close the issue now that this project's implementation of the manual mapping approach is working again on 24H2, but please follow up if you get it working in your Pascal project as well. It would be cool to see that working again too.

@EvanMcBroom EvanMcBroom self-assigned this Jan 8, 2025
@EvanMcBroom
Copy link
Owner

EvanMcBroom commented Jan 8, 2025

LoadLibrary reattempts were fixed in 4d70be5, and handling SCPCFG was fixed in 298402e and c40c3f5.

@EvanMcBroom EvanMcBroom changed the title Windows 24H2 Windows 11 24H2 Jan 8, 2025
@rbmm
Copy link
Contributor

rbmm commented Jan 8, 2025

hm.. i only just now install 24H2 (never look it before and yet not read about new cfg here). after fast test look like my binary work ok, without any fix. and if set LdrpIsHotPatchingEnabled to a non-zero value - any loadlibrary (normal, not from memory) fail with "A system patch could not be applied due to conflicting accesses to the system image. " error. interesting are you can test https://github.com/rbmm/ARL/tree/main/x64/Release on self system (24h2) (load[n].exe - load dll in current process and unload it after message box closed. possible use dll.dll for test or any system or your dll. exe.exe - load dll to explorer and then to fontdrvhost.exe.

@rbmm
Copy link
Contributor

rbmm commented Jan 8, 2025

As such, it’s bound to break a few programs here and there, and one of the unlucky victims this time was x64dbg

funny - after very fast test own debugger and tools, i not found any breaks, but what i view unusual just - almost all dlls in all processes now have LDRP_IMAGE_NOT_AT_BASE (in Loader Data Table Entry Flags) even ntdll.dll strange. but probably sense of this undocumented flag changed

@EvanMcBroom
Copy link
Owner

i only just now install 24H2 (never look it before and yet not read about new cfg here). after fast test look like my binary work ok, without any fix

I believe I may have the answer for this. After debugging load.exe and reading the source, it appears that ARL allows LoadLibrary to map the "on the filesystem" dll into memory as a section view (like LoadLibrary normally does) then overwrites the data in that view with the content of the in-memory dll (done in the OverwriteSection function). Please correct me if my understanding of the application is wrong.

That approach allows the code for RtlpInsertOrRemoveScpCfgFunctionTable and LdrpIsHotPatchingEnabled to work as intended because they are now processing a normal mapped view of a section. RtlpInsertOrRemoveScpCfgFunctionTable will be able to process the extra pages of SCPCFG data that the kernel may add the mapped view (if present) and the NtManageHotPatch syscall that LdrpIsHotPatchingEnabled uses will succeed because its targeting the address of a normal mapped section view.

The manual mapping approach that's implemented in perfect-loader does not allow LoadLibrary to create a mapped view of the section for the "on the filesystem" dll. Instead, it allocates memory, manually maps the dll inside that allocated memory, then returns it for LoadLibrary to continue processing. That causes RtlpInsertOrRemoveScpCfgFunctionTable and LdrpIsHotPatchingEnabled to not process a mapped view of a section (which Microsoft intended) but instead process the bytes in our memory allocation. That causes the need for extra handling in perfect-loader to allow these two functions to succeed.

I may be wrong, but I believe that is why ARL has continued to work on 24H2 but perfect-loader required additional code.

what i view unusual just - almost all dlls in all processes now have LDRP_IMAGE_NOT_AT_BASE (in Loader Data Table Entry Flags) even ntdll.dll strange. but probably sense of this undocumented flag changed

That is interesting! I agree that this behavior is likely related to Microsoft's implementation of hot patching, but definitely want to investigate that behavior further 🙂

@rbmm
Copy link
Contributor

rbmm commented Jan 9, 2025

allows LoadLibrary to map the "on the filesystem" dll into memory as a section view (like LoadLibrary normally does) then overwrites the data in that view with the content of the in-memory dll (done in the OverwriteSection function).

yes, correct. i select dll without cfg (otherwise will be crash if cfg enabled in process) and map it as image section. we need no cfg for any address inside this section will be valid. for virtual memory allocation this is true.
also debug load.exe (or load2.exe load3.exe ) is can be bit problematic - because i use single step exception in code and debugger usually stop on it unconditionally. so need or have possibility not stop, or pass this exception back to code

about LDRP_IMAGE_NOT_AT_BASE - i sure sense of this flag is changed. now it mean something else. almost all dlls have it on (despite it at base) but some not have

@rbmm
Copy link
Contributor

rbmm commented Jan 9, 2025

That approach allows the code for RtlpInsertOrRemoveScpCfgFunctionTable and LdrpIsHotPatchingEnabled to work as intended..

I doubt it, at least because I don't display the entire section (not the entire system dll), but the minimum required size so that my (smaller) dll fits. So everything that goes further doesn't apply here. Nevertheless, in my tests, loading works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants