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

64bit run_pe load 64bit payload not working on windows 11 24H2 #59

Open
idigger opened this issue Oct 13, 2024 · 33 comments
Open

64bit run_pe load 64bit payload not working on windows 11 24H2 #59

idigger opened this issue Oct 13, 2024 · 33 comments

Comments

@idigger
Copy link

idigger commented Oct 13, 2024

errrr111111

Load 32bit payload ok.
errrrr22222

64bit and 32bit are ok on before windows 11 24H2

@NotCapengeR
Copy link
Contributor

NotCapengeR commented Nov 15, 2024

Starting from Windows 11 24H2, Microsoft has implemented a new Control Flow Guard or CFG system, which should limit the places where the application can execute code. You can read more info on MSDN.

Hackers from the UnknownCheats forum analyzed the PE loader in Ntdll and found a function that initializes this mechanism for the PE image — RtlpInsertOrRemoveScpCfgFunctionTable. It seems like there is even a working code that patches RtlpInsertOrRemoveScpCfgFunctionTable function according to a hard-coded offset.

const auto NtdllBase = reinterpret_cast<PBYTE>(GetModuleHandleW(L"ntdll.dll"));
const BYTE Patch[4] =
{
    0x48, 0x31, 0xC0, // xor rax, rax
    0xC3 // ret
};
// Patching RtlpInsertOrRemoveScpCfgFunctionTable function of Ntdll using hard-coded offset
WriteProcessMemory(pi.hProcess, NtdllBase + 0x7BE0, Patch, sizeof(Patch), nullptr);

Original post — https://www.unknowncheats.me/forum/4239032-post15.html

Perhaps zeroing IMAGE_LOAD_CONFIG_DIRECTORY of the payload image before copying it to another process can also help.

@NotCapengeR
Copy link
Contributor

NotCapengeR commented Dec 15, 2024

So, I did get an Ntdll sample from Windows 11 24H2. The RtlpInsertOrRemoveScpCfgFunctionTable function is not exported, so patching it by offset is not a good idea, but I found something interesting in IDA...

NTSTATUS __stdcall RtlpInsertOrRemoveScpCfgFunctionTable(PVOID ImageBase, __int64 Reserved, bool insertOrRemove)
{
  NTSTATUS result; // eax
  char *pageBase; // rax
  __int64 offsetToRtlTable; // rcx
  ULONG_PTR ImageExtensionLength; // [rsp+30h] [rbp-38h] BYREF
  PVOID DynamicTable; // [rsp+38h] [rbp-30h] BYREF
  MEMORY_IMAGE_EXTENSION_INFORMATION info; // [rsp+40h] [rbp-28h] BYREF

  ImageExtensionLength = 0i64;
  memset(&info, 0, sizeof(info));
  result = ZwQueryVirtualMemory(
             (HANDLE)0xFFFFFFFFFFFFFFFFi64,
             ImageBase,
             MemoryImageExtensionInformation,
             &info,
             0x18ui64,
             &ImageExtensionLength);
  if ( result == -1073741637 )
    return 279;
  if ( result >= 0 )
  {
    if ( !info.PageSize )
      return 279;
    pageBase = (char *)ImageBase + *(_QWORD *)&info.PageOffset;
    offsetToRtlTable = *(unsigned int *)((char *)ImageBase + *(_QWORD *)&info.PageOffset + 20);
    if ( !(_DWORD)offsetToRtlTable )
      return 279;
    if ( insertOrRemove )
    {
      result = RtlAddGrowableFunctionTable(
                 &DynamicTable,
                 (PRUNTIME_FUNCTION)&pageBase[offsetToRtlTable],
                 1u,
                 1u,
                 (ULONG_PTR)ImageBase + *(_QWORD *)&info.PageOffset,
                 (ULONG_PTR)&pageBase[info.PageSize]);
      if ( result >= 0 )
        return 0;
    }
    else
    {
      RtlDeleteFunctionTable((PRUNTIME_FUNCTION)&pageBase[offsetToRtlTable]);
      return 0;
    }
  }
  return result;
}

RtlpInsertOrRemoveScpCfgFunctionTable is just a wrapper around RtlDeleteFunctionTable and RtlAddGrowableFunctionTable. We are lucky — both of these functions are exported and not used anywhere else, so we can safely patch them. Oh, and by the way: it turned out that another function requires a patch — NtManageHotPatch, it is also exported, so there are no problems for us.
Final code should be something like this:

#ifdef _WIN64 // Dynamic function tables is only for 64-bits images
	auto hNtdll = GetModuleHandleW(L"ntdll.dll"); // Found Ntdll base address
	if (hNtdll) {
		// Stub for functions
		const BYTE patch[4] =
		{
			0x48, 0x31, 0xC0, // xor rax, rax
			0xC3 // ret
		};

		FARPROC NtManageHotPatch = GetProcAddress(hNtdll, "NtManageHotPatch");
		FARPROC RtlAddGrowableFunctionTable = GetProcAddress(hNtdll, "RtlAddGrowableFunctionTable");
		FARPROC RtlDeleteFunctionTable = GetProcAddress(hNtdll, "RtlDeleteFunctionTable");
		
		if (NtManageHotPatch && RtlAddGrowableFunctionTable && RtlDeleteFunctionTable) {
			// Wanna some patches?
			WriteProcessMemory(pi.hProcess, NtManageHotPatch, patch, sizeof(patch), nullptr);
			WriteProcessMemory(pi.hProcess, RtlAddGrowableFunctionTable, patch, sizeof(patch), nullptr);
			WriteProcessMemory(pi.hProcess, RtlDeleteFunctionTable, patch, sizeof(patch), nullptr);
			
			// Apply changes to cache
			FlushInstructionCache(pi.hProcess, NtManageHotPatch, sizeof(patch));
			FlushInstructionCache(pi.hProcess, RtlAddGrowableFunctionTable, sizeof(patch));
			FlushInstructionCache(pi.hProcess, RtlDeleteFunctionTable, sizeof(patch));
		}
	}
#endif

I could do a pull request with this, but I don't have Windows 11 24H2 to test this code.

@hasherezade
Copy link
Owner

Thank you for finding all those details @NotCapengeR ! It is definitely very useful! I will try to get Windows 11 24H2 and test it whenever I get some free time.

@bytecode77
Copy link

@NotCapengeR I have, too, realized, that Windows 24H2 broke RunPE, so I stumbled upon your suggestion. I tried it (with VirtualProtectEx before WriteProcessMemory), but without success, yet. I will do my own research next, but may I ask, if any of you solved this rather new issue?

@NotCapengeR
Copy link
Contributor

NotCapengeR commented Jan 5, 2025

@NotCapengeR I have, too, realized, that Windows 24H2 broke RunPE, so I stumbled upon your suggestion. I tried it (with VirtualProtectEx before WriteProcessMemory), but without success, yet. I will do my own research next, but may I ask, if any of you solved this rather new issue?

As I said, I don't have Windows 11 24H2, so I cannot test my code. If it doesn't working, you need to debug and find a function that is causing the error (btw, what is this error?). But it seems to me that a patch of these functions is enough for everything to work, perhaps a few more functions need a patch too

@harunkocacaliskan
Copy link

Any updates for this issue, did anybody tried patch ntdll method? I have tried some other PE Loaders, some of them has same error.

@pmsjt
Copy link

pmsjt commented Jan 24, 2025

RtlpInsertOrRemoveScpCfgFunctionTable has no effect on the enablement of CFG. The only thing it does is to add exception handlers to handle any exception happening in the CFG function, if they arise. If CFG is off, then CFG code will never run, never raise exceptions and, thus, its EHs will also never run.

My wild guess about what is happening, is that the program is unmapping a DLL legitimately loaded by Windows and then mapping it back, using this PE loader. When the DLL is unloaded, the SCP page is also unloaded. This PE loader does not know what an SCP is, so it won't reconstruct it. Any pointers to it - including dynamic function table pointers - are now pointing to NO_ACCESS memory.

Again, this is just a guess from reading the tea leaves. A PoC or even just a dump would be helpful in helping understand what is really amiss.

If the guess is right, there are some options: Either we disable SCPs for this process, or this loader starts to emulate the SCP construction... The latter is unlikely hard to maintain, but I am listing for the purpose of completeness.

@hasherezade
Copy link
Owner

hasherezade commented Jan 25, 2025

Hi, finally I've got some time, and access to Windows 11 24H2, and started to check it.

First of all, in this variant of RunPE, there is no unmapping of the original module. The new module is loaded additionally, and the PEB is modified to point to it. So it does not seem to be what @pmsjt described.

I have two other loaders, that can be used as an alternative of RunPE - I checked them, and they both work on Windows 11 24H2, while the classic RunPE doesn't work. Check them out:

I am not sure yet, but it seems the problem is related to the fact that the PE to be loaded is in MEM_PRIVATE, because both of the alternative loaders that I mention work almost exactly the same as classic RunPE (the most similar is transacted_hollowing). The only difference is that they use MEM_IMAGE for the payload.
Process Overwriting disables CFG on the process, but Transacted Hollowing doesn't touch it.

I will keep investigating where the problem actually occurs, and will let you know. In the meantime, I recommend you to use one of the alternatives that I mentioned.

@pmsjt
Copy link

pmsjt commented Jan 25, 2025

The ecosystem folks sent me a PoC and, in n that one ntdll.dll was being unmapped. That is even more radical than unmapping another binary because because binaries within the jump displacement vicinity of ntdll.dll use ntdll.dll's SCP page for optimal cache usage reasons.

@NotCapengeR
Copy link
Contributor

Hi, finally I've got some time, and access to Windows 11 24H2, and started to check it.

First of all, in this variant of RunPE, there is no unmapping of the original module. The new module is loaded additionally, and the PEB is modified to point to it. So it does not seem to be what @pmsjt described.

I have two other loaders, that can be used as an alternative of RunPE - I checked them, and they both work on Windows 11 24H2, while the classic RunPE doesn't work. Check them out:

I am not sure yet, but it seems the problem is related to the fact that the PE to be loaded is in MEM_PRIVATE, because both of the alternative loaders that I mention work almost exactly the same as classic RunPE (the most similar is transacted_hollowing). The only difference is that they use MEM_IMAGE for the payload. Process Overwriting disables CFG on the process, but Transacted Hollowing doesn't touch it.

I will keep investigating where the problem actually occurs, and will let you know. In the meantime, I recommend you to use one of the alternatives that I mentioned.

My friend said that in the Windows 11 24H2 kernel, a new check appeared inside the NtMapViewOfSection function, which returns STATUS_INVALID_IMAGE_FORMAT if the section was not mapped from the disk (SEC_IMAGE attribute).
It looks like there is a similar memory region attribute check inside the x64 PE loader, which is why SCP CFG patches don't work...

@NotCapengeR
Copy link
Contributor

RtlpInsertOrRemoveScpCfgFunctionTable has no effect on the enablement of CFG. The only thing it does is to add exception handlers to handle any exception happening in the CFG function, if they arise. If CFG is off, then CFG code will never run, never raise exceptions and, thus, its EHs will also never run.

My wild guess about what is happening, is that the program is unmapping a DLL legitimately loaded by Windows and then mapping it back, using this PE loader. When the DLL is unloaded, the SCP page is also unloaded. This PE loader does not know what an SCP is, so it won't reconstruct it. Any pointers to it - including dynamic function table pointers - are now pointing to NO_ACCESS memory.

Again, this is just a guess from reading the tea leaves. A PoC or even just a dump would be helpful in helping understand what is really amiss.

If the guess is right, there are some options: Either we disable SCPs for this process, or this loader starts to emulate the SCP construction... The latter is unlikely hard to maintain, but I am listing for the purpose of completeness.

I even tried to zeroing the IMAGE_LOAD_CONFIG fields responsible for CFG - it didn’t help. It looks like it breaks for other reasons than CFG (or not just CFG).

@hasherezade
Copy link
Owner

hasherezade commented Jan 25, 2025

Ok, I see where exactly it happens. Those are the subsequent functions called:

  • LdrpInitializeProcess
    • LdrpProcessMappedModule
      • RtlpInsertOfRemoveScpCfgFunctionTable
        • ZwQueryVirtualMemory

The problem lies indeed in the fact that the payload is not mapped as MEM_IMAGE.
The function ZwQueryVirtualMemory is called with a new argument MemoryImageExtensionInformation.

ZwQueryVirtualMemory(NtCurrentProcess(), implanted_pe, MemoryImageExtensionInformation, out_buf, out_buf_size, &out_size);

If the supplied region is not MEM_IMAGE, it returns 0xC0000141 (STATUS_INVALID_ADDRESS), and it further leads to termination of the loader. If we hook ZwQueryVirtualMemory and make it return 0 (STATUS_SUCCESS) for our payload, it will continue to load correctly. I already tested it, and it worked.

@pmsjt
Copy link

pmsjt commented Jan 26, 2025

Great work. Glad you have a working.

Better than STATUS_SUCCESS, you should make it return STATUS_NOT_SUPPORTED. This will be taken as a benign return status, whilst preventing the rest of the function from parsing bogus uninitialized data from the ZwQueryVirtualMemory 4th param buffer.

@hasherezade
Copy link
Owner

I added the patch, and it should work now. Check it out guys, and let me know what do you think.

@harunkocacaliskan
Copy link

I get a similar error for 32-bit applications. If run_pe.exe is compiled as 32-bit and the target and payload are 32-bit, the error "0xc00004ac" is returned. Is anyone else experiencing the same issue?

@hasherezade
Copy link
Owner

hasherezade commented Feb 5, 2025

@harunkocacaliskan - I tested it on Windows 11 24H2, Build 26100.2894, which is the latest up to date (excluding the Preview), and both 32 and 64-bit versions worked without any issues.

32-bit:
Image

64-bit:
Image

Please try the latest builds from AppVeyor:

And the same payloads that I used: https://learn.microsoft.com/en-us/sysinternals/downloads/loadorder
To check if they can run on your side.
IMPORTANT: Don't forget to add the directory with the run_pe builds to Windows Defender exclusions! The loaders are detected, so it may be blocking execution!

@harunkocacaliskan
Copy link

harunkocacaliskan commented Feb 6, 2025

@harunkocacaliskan - I tested it on Windows 11 24H2, Build 26100.2894, which is the latest up to date (excluding the Preview), and both 32 and 64-bit versions worked without any issues.

32-bit: Image

64-bit: Image

Please try the latest builds from AppVeyor:

And the same payloads that I used: https://learn.microsoft.com/en-us/sysinternals/downloads/loadorder To check if they can run on your side. IMPORTANT: Don't forget to add the directory with the run_pe builds to Windows Defender exclusions! The loaders are detected, so it may be blocking execution!

I have tried on a Windows 11 24h2 working on VirtualBox, it worked. Now i will try to figure it out why not running on my laptop. I will try to disable tpm or kernel dma protection, they have same Windows build, Defender deactivated and exploit protection settings are default. Thank you for updates.

Edit:

I have tried it on a friends laptop same error. Should i start a new issue for this?

Downloaded your 32bit build and tried with 32bit loadord.exe.

Image

@NotCapengeR
Copy link
Contributor

NotCapengeR commented Feb 6, 2025

@harunkocacaliskan - I tested it on Windows 11 24H2, Build 26100.2894, which is the latest up to date (excluding the Preview), and both 32 and 64-bit versions worked without any issues.
32-bit: Image
64-bit: Image
Please try the latest builds from AppVeyor:

And the same payloads that I used: https://learn.microsoft.com/en-us/sysinternals/downloads/loadorder To check if they can run on your side. IMPORTANT: Don't forget to add the directory with the run_pe builds to Windows Defender exclusions! The loaders are detected, so it may be blocking execution!

I have tried on a Windows 11 24h2 working on VirtualBox, it worked. Now i will try to figure it out why not running on my laptop. I will try to disable tpm or kernel dma protection, they have same Windows build, Defender deactivated and exploit protection settings are default. Thank you for updates.

Edit:

I have tried it on a friends laptop same error. Should i start a new issue for this?

Downloaded your 32bit build and tried with 32bit loadord.exe.

Image

0xC00004ACL is STATUS_PATCH_CONFLICT. You can try to use my patch of NtManagePatch (but replace xor rax,rax with xor eax,eax and ret with ret 10h) or try to zeroing IMAGE_LOAD_CONFIG_DIRECTORY

const BYTE patch[5] = {
			0x31, 0xC0, // xor eax, eax
			0xC2, 0x10, 0x00 // ret 10h
    };

@harunkocacaliskan
Copy link

@NotCapengeR Thank you it worked like a charm. I have applied patch just after CreateProcess.

@NotCapengeR
Copy link
Contributor

NotCapengeR commented Feb 6, 2025

@hasherezade well, looks like both functions, NtManageHotPatch and NtQueryVirtualMemory are required a patch (32-/64-bits). I still can't figure it out: is Microsoft doing this intentionally or is it a side-effect of the SCP CFG?

@harunkocacaliskan
Copy link

@hasherezade well, looks like both functions, NtManageHotPatch and NtQueryVirtualMemory are required a patch (32-/64-bits). I still can't figure it out: is Microsoft doing this intentionally or is it a side-effect of the SCP CFG?

And I still cant figure it out, how 64 bit runner is able to run both 32 bit 64 bit payloads when your patch is applied.
Because we are patching 64 bit version of ntdll.dll in 64 bit runner but our payload is 32 bit and 32 bit runner is patching 32 bit version of ntdll.dll.
https://learn.microsoft.com/en-us/windows/win32/winprog64/wow64-implementation-details

The WOW64 emulator runs in user mode. It provides an interface between the 32-bit version of Ntdll.dll and the kernel of the processor, and it intercepts kernel calls. The WOW64 emulator consists of the following DLLs:...
These DLLs, along with the 64-bit version of Ntdll.dll, are the only 64-bit binaries that can be loaded into a 32-bit process.

Maybe we need to patch only 64 bit version of ntdll.dll but 32 bit process is not able to do it.

@hasherezade
Copy link
Owner

@hasherezade well, looks like both functions, NtManageHotPatch and NtQueryVirtualMemory are required a patch (32-/64-bits). I still can't figure it out: is Microsoft doing this intentionally or is it a side-effect of the SCP CFG?

In the 32-bit version of my loader, there was no patch applied at all. Maybe this is the problem.
The patch is enabled only for 64-bit builds:
https://github.com/hasherezade/libpeconv/blob/master/run_pe/patch_ntdll.cpp

I can easily write a 32-bit version of it, but I didn't know that it is needed. This issue was only about it not working for 64-bit. I don't have the Windows 11 24H2 on a real machine at the moment, only a VM, so I can't really test it. On the VM it works fine.

I am gonna add the same patch for 32-bit, and let's give it a try.

@hasherezade
Copy link
Owner

@harunkocacaliskan - do I understand you correctly that the 64-bit loader:

The problem is only with the 32-bit build, and you managed to run it with NtManagePatch patch?

@harunkocacaliskan
Copy link

@harunkocacaliskan - do I understand you correctly that the 64-bit loader:

The problem is only with the 32-bit build, and you managed to run it with NtManagePatch patch?

I have downloaded builds from link you provided;

Windows 11 Pro 24h2.26100 VirtualBox 24h2
run_pe-> 64 bit ------- Payload-> 32bit ✅
run_pe-> 64 bit ------- Payload-> 64bit ✅
run_pe-> 32 bit ------- Payload-> 32bit ✅

Windows 11 Pro for Workstations 24h2.26100 Laptop with TPM 2.0 running on Laptop

run_pe-> 64 bit ------- Payload-> 32 bit 🔴 (Error 0xC00004AC)
run_pe-> 64 bit ------- Payload-> 64 bit 🔴 (Error 0xC00004AC)
run_pe-> 32 bit ------- Payload-> 32 bit 🔴 (Error 0xC00004AC)

with @NotCapengeR s patch
run_pe-> 32 bit ------- Payload-> 32bit ✅

I also tried this patch method at https://github.com/adamhlt/Process-Hollowing it was not working either, now it works too for both 64 and 32 bit versions.

If you provide me patched builds i can try them on physical computer. But your ZwQueryVirtualMemory patching didnt worked for me.

@hasherezade
Copy link
Owner

@harunkocacaliskan - do I understand you correctly that the 64-bit loader:

The problem is only with the 32-bit build, and you managed to run it with NtManagePatch patch?

I have downloaded builds from link you provided;

Windows 11 Pro 24h2.26100 VirtualBox 24h2 run_pe-> 64 bit ------- Payload-> 32bit ✅ run_pe-> 64 bit ------- Payload-> 64bit ✅ run_pe-> 32 bit ------- Payload-> 32bit ✅

Windows 11 Pro for Workstations 24h2.26100 Laptop with TPM 2.0 running on Laptop

run_pe-> 64 bit ------- Payload-> 32 bit 🔴 (Error 0xC00004AC) run_pe-> 64 bit ------- Payload-> 64 bit 🔴 (Error 0xC00004AC) run_pe-> 32 bit ------- Payload-> 32 bit 🔴 (Error 0xC00004AC)

with @NotCapengeR s patch run_pe-> 32 bit ------- Payload-> 32bit ✅

I also tried this patch method at https://github.com/adamhlt/Process-Hollowing it was not working either, now it works too for both 64 and 32 bit versions.

If you provide me patched builds i can try them on physical computer. But your ZwQueryVirtualMemory patching didnt worked for me.

Thank you for checking it! I guess I need to get a physical machine with Windows 11 to really have a deep dive into it.
But can you try those alternative methods, and check if any of them work on a physical machine?

This would also give me more clarity on what can be a possible cause.

@hasherezade
Copy link
Owner

Ok, I managed to test it on a real machine. It seems that this error 0xC00004AC occurs only if the Memory Integrity check is enabled in the system:

Image

When it is disabled, the NtQueryVirtualMemory is all you need. But when it is enabled, some additional checks are done.

Interestingly, the alternative techniques that I mentioned earlier, seem to work even with Memory Integrity checks enabled.
I will dig deeper into it whenever I get some free time.

hasherezade added a commit that referenced this issue Feb 6, 2025
@hasherezade
Copy link
Owner

hasherezade commented Feb 6, 2025

I see where exactly it is coming from, indeed the simplest way to get rid of it is to patch NtManagePatch.

  1. LdrpQueryCurrentPatch is called on the implant (address in red):
    Image

  2. The function NtManageHotPatch exits with an error STATUS_CONFLICTING_ADDRESSES:
    Image

Maybe later I will come up with something less invasive, for now I just made a similar patch as @NotCapengeR , just made the NtManagePatch return STATUS_NOT_SUPPORTED.

It should work for 64-bit. Check it out:
https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/vqlakv7vfjrrha74/artifacts

I will work on the 32-bit version later.

@NotCapengeR
Copy link
Contributor

NotCapengeR commented Feb 6, 2025

I see where exactly it is coming from, indeed the simplest way to get rid of it is to patch NtManagePatch.

  1. LdrpQueryCurrentPatch is called on the implant (address in red):
    Image
  2. The function NtManageHotPatch exits with an error STATUS_CONFLICTING_ADDRESSES:
    Image

Maybe later I will come up with something less invasive, for now I just made a similar patch as @NotCapengeR , just made the NtManagePatch return STATUS_NOT_SUPPORTED.

It should work for 64-bit. Check it out: https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/vqlakv7vfjrrha74/artifacts

I will work on the 32-bit version later.

Looks like it possible to disable hot patching with changing LdrpIsHotPatchingEnabled global variable in Ntdll.
I can’t be one hundred percent sure, but it seems that when this variable is false, the hot patching functions are not called when the process is initializing. Example from LdrpInitializeInternal:

NTSTATUS __stdcall LdrpInitializeInternal(PCONTEXT ContextRecord, PVOID ImageBase)
{
  TebAndNtStatus result; // rax union {TEB teb; NTSTATUS status;}
  LONG hotPatchInitialized; // ebx
  LdrpHotPatchContext hotpatchCtx; // [rsp+30h] [rbp-18h] BYREF

  hotpatchCtx = 0i64;
  result.teb = NtCurrentTeb();
  if ( (HIWORD(result.teb->ResourceRetValue) & 0x4000) == 0 )
  {
    hotPatchInitialized = _InterlockedCompareExchange(&LdrpHotPatchInitialized, 1, 0);
    if ( hotPatchInitialized )
    {
      if ( hotPatchInitialized == 1 )
        LdrpWaitForInitializationComplete(&LdrpHotPatchInitialized, &LdrpHotPatchInitCompleteEvent);
    }
    else
    {
      ZwCreateEvent(&LdrpHotPatchInitCompleteEvent, 0x1F0003u, 0i64, NotificationEvent, 0);
      LdrpInitializeHotPatching(); // This function initialize LdrpIsHotPatchingEnabled global variable
      LdrpNtdllHotPatchContext = &hotpatchCtx;
      hotpatchCtx = 0i64;
      if ( LdrpIsHotPatchingEnabled )
        hotpatchCtx.patchStatus = LdrpLoadPatchedNtdll(ImageBase, &hotpatchCtx); // This function calls LdrpQueryCurrentPatch
      LdrpInitializationComplete(&LdrpHotPatchInitialized, &LdrpHotPatchInitCompleteEvent, 0x1488u);
    }
    result.status = LdrpInitialize(ContextRecord, ImageBase);
    if ( !hotPatchInitialized )
      LdrpNtdllHotPatchContext = 0i64;
  }
  return result.status;
}

Example from LdrpMapAndSnapDependency:

void __stdcall LdrpMapAndSnapDependency(PLDRP_LOAD_CONTEXT Ctx) {
// This function has too much code, so I removed the part that we don't need
 if ( LdrpIsHotPatchingEnabled && Ctx->WorkQueueListEntry.Flink ) {
    status = LdrpQueryCurrentPatch(Module->DllBase, &success);
    // ...
  }
// ...
}

So let's go to the LdrpInitializeHotPatching function:

NTSTATUS __stdcall LdrpInitializeHotPatching()
{
  PebAndNtStatus pebAndNtstatus; // rax (it is union {PPEB peb; NTSTATUS status;})
  bool enableHotPatch; // bl
  ULONG ReturnLength; // [rsp+30h] [rbp+8h] BYREF
  QWORD HotPatchData; // [rsp+38h] [rbp+10h] BYREF

  ReturnLength = 0;
  pebAndNtstatus.peb = NtCurrentPeb();
  if ( (pebAndNtstatus.peb->ProcessParameters->Flags & 0x2000000) != 0
    || (enableHotPatch = 1,
        HotPatchData = 1i64,
        pebAndNtstatus.status = ZwManageHotPatch(ManageHotPatchCheckEnabled, &HotPatchData, 8u, &ReturnLength),
        pebAndNtstatus.status == -1073741637) // STATUS_NOT_SUPPORTED
    || pebAndNtstatus.status == -1073741822) // STATUS_NOT_IMPLEMENTED
  {
    enableHotPatch = 0;
  }
  LdrpIsHotPatchingEnabled = enableHotPatch;
  return pebAndNtstatus.status;
}

It calls NtManageHotPatch with ManageHotPatchCheckEnabled information class:

// Source: https://raw.githubusercontent.com/jonaslyk/temp/main/dgdri.bat
typedef enum _HOT_PATCH_INFORMATION_CLASS {
  ManageHotPatchLoadPatch = 0x0,
  ManageHotPatchUnloadPatch = 0x1,
  ManageHotPatchQueryPatches = 0x2,
  ManageHotPatchLoadPatchForUser = 0x3,
  ManageHotPatchUnloadPatchForUser = 0x4,
  ManageHotPatchQueryPatchesForUser = 0x5,
  ManageHotPatchQueryActivePatches = 0x6,
  ManageHotPatchApplyImagePatch = 0x7,
  ManageHotPatchQuerySinglePatch = 0x8,
  ManageHotPatchCheckEnabled = 0x9,
  ManageHotPatchMax = 0xA,
} HOT_PATCH_INFORMATION_CLASS;

Maybe it's better to return STATUS_NOT_SUPPORTED or STATUS_NOT_IMPLEMENTED only for ManageHotPatchCheckEnabled info class... Btw, I also leave the NtManagerHotPatch function signature here (currently Ntdoc doesn't docummented it), maybe it will be useful to someone:

NTSTATUS __stdcall NtManageHotPatch(
        HOT_PATCH_INFORMATION_CLASS HotPatchInformation,
        PVOID HotPatchData,
        ULONG Length,
        PULONG ReturnLength
);

hasherezade added a commit that referenced this issue Feb 6, 2025
@hasherezade
Copy link
Owner

hasherezade commented Feb 6, 2025

My latest commit should finally solve this problem, for both 32- and 64-bit. Check it out, it should work on the system with Memory Integrity checks enabled

But I don't really like the idea of removing the function NtManageHotPatch altogether. I would prefer to add some filtering on it. Maybe I will add it eventually, but I need to study the passed structures more. Thank you for your input @NotCapengeR.

@harunkocacaliskan
Copy link

My latest commit should finally solve this problem, for both 32- and 64-bit. Check it out, it should work on the system with Memory Integrity checks enabled

But I don't really like the idea of removing the function NtManageHotPatch altogether. I would prefer to add some filtering on it. Maybe I will add it eventually, but I need to study the passed structures more. Thank you for your input @NotCapengeR.

I have tested these on my laptop all ok.

run_pe-> 64 bit ------- Payload-> 32bit ✅
run_pe-> 64 bit ------- Payload-> 64bit ✅
run_pe-> 32 bit ------- Payload-> 32bit ✅

@harunkocacaliskan
Copy link

harunkocacaliskan commented Feb 7, 2025

Some good and bad news here;

After installing Windows Preview Update; January 28, 2025—KB5050094 (OS Build 26100.3037) Preview
https://support.microsoft.com/en-us/topic/january-28-2025-kb5050094-os-build-26100-3037-preview-78fda0ea-79e9-468d-8a77-de7914ca1aef

@NotCapengeR 's patching method is not running for 64 bit payloads it gives 0xC0000141 error , but for 32 bit it's working. I am running my own oldschool runpe code not using libpeconv. If i uninstall preview update everything ok.

@hasherezade patching method is still working after preview update.

Bad news;
Maybe Microsoft will release new updates and these methods will start to throw errors again.

@NotCapengeR
Copy link
Contributor

NotCapengeR commented Feb 7, 2025

Some good and bad news here;

After installing Windows Preview Update; January 28, 2025—KB5050094 (OS Build 26100.3037) Preview https://support.microsoft.com/en-us/topic/january-28-2025-kb5050094-os-build-26100-3037-preview-78fda0ea-79e9-468d-8a77-de7914ca1aef

@NotCapengeR 's patching method is not running for 64 bit payloads it gives 0xC0000141 error , but for 32 bit it's working. I am running my own oldschool runpe code not using libpeconv. If i uninstall preview update everything ok.

@hasherezade patching method is still working after preview update.

Bad news; Maybe Microsoft will release new updates and these methods will start to throw errors again.

0xC0000141 is STATUS_INVALID_ADDRESS. Looks like it's NtQueryVirtualMemory error. So, as I said earlier, both functions, NtQueryVirtualMemory and NtManageHotPatch, require a patch. For 32-bits NtQueryVirtualMemory patch is not required, because a RtlpInsertOrRemoveScpCfgFunctionTable function which calls it is for dynammic function tables that presents only on 64-bits

@hasherezade
Copy link
Owner

hasherezade commented Feb 7, 2025

Some good and bad news here;
After installing Windows Preview Update; January 28, 2025—KB5050094 (OS Build 26100.3037) Preview https://support.microsoft.com/en-us/topic/january-28-2025-kb5050094-os-build-26100-3037-preview-78fda0ea-79e9-468d-8a77-de7914ca1aef
@NotCapengeR 's patching method is not running for 64 bit payloads it gives 0xC0000141 error , but for 32 bit it's working. I am running my own oldschool runpe code not using libpeconv. If i uninstall preview update everything ok.
@hasherezade patching method is still working after preview update.
Bad news; Maybe Microsoft will release new updates and these methods will start to throw errors again.

0xC0000141 is STATUS_INVALID_ADDRESS. Looks like it's NtQueryVirtualMemory error. So, as I said earlier, both functions, NtQueryVirtualMemory and NtManageHotPatch, require a patch. For 32-bits NtQueryVirtualMemory patch is not required, because a RtlpInsertOfRemoveScpCfgFunctionTable function which calls it is for dynammic function tables that presents only on 64-bits

Agree, in 64-bit both functions should be patched, and it will do the job. Now we have the patches that address the root of the problem, so it should not get broken by the next builds, unless Microsoft do changes in the logic of the loader. But they already did some deeper changes, so I think it is unlikely that they will do that much in the nearest future.

Thanks for testing it @harunkocacaliskan !

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

6 participants