-
Notifications
You must be signed in to change notification settings - Fork 185
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
Comments
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. |
So, I did get an Ntdll sample from Windows 11 24H2. The 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;
}
#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. |
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. |
@NotCapengeR I have, too, realized, that Windows 24H2 broke RunPE, so I stumbled upon your suggestion. I tried it (with |
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 |
Any updates for this issue, did anybody tried patch ntdll method? I have tried some other PE Loaders, some of them has same error. |
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. |
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 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. |
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. |
My friend said that in the Windows 11 24H2 kernel, a new check appeared inside the |
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). |
Ok, I see where exactly it happens. Those are the subsequent functions called:
The problem lies indeed in the fact that the payload is not mapped as MEM_IMAGE. ZwQueryVirtualMemory(NtCurrentProcess(), implanted_pe, MemoryImageExtensionInformation, out_buf, out_buf_size, &out_size); If the supplied region is not MEM_IMAGE, it returns |
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. |
I added the patch, and it should work now. Check it out guys, and let me know what do you think. |
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? |
@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. Please try the latest builds from AppVeyor:
And the same payloads that I used: https://learn.microsoft.com/en-us/sysinternals/downloads/loadorder |
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. |
0xC00004ACL is const BYTE patch[5] = {
0x31, 0xC0, // xor eax, eax
0xC2, 0x10, 0x00 // ret 10h
}; |
@NotCapengeR Thank you it worked like a charm. I have applied patch just after CreateProcess. |
@hasherezade well, looks like both functions, |
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.
Maybe we need to patch only 64 bit version of ntdll.dll but 32 bit process is not able to do it. |
In the 32-bit version of my loader, there was no patch applied at all. Maybe this is the problem. 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. |
@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 |
I have downloaded builds from link you provided; Windows 11 Pro 24h2.26100 VirtualBox 24h2 Windows 11 Pro for Workstations 24h2.26100 Laptop with TPM 2.0 running on Laptop run_pe-> 64 bit ------- Payload-> 32 bit 🔴 (Error 0xC00004AC) with @NotCapengeR s patch 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.
This would also give me more clarity on what can be a possible cause. |
Ok, I managed to test it on a real machine. It seems that this error When it is disabled, the Interestingly, the alternative techniques that I mentioned earlier, seem to work even with Memory Integrity checks enabled. |
I see where exactly it is coming from, indeed the simplest way to get rid of it is to patch
Maybe later I will come up with something less invasive, for now I just made a similar patch as @NotCapengeR , just made the It should work for 64-bit. Check it out: I will work on the 32-bit version later. |
Looks like it possible to disable hot patching with changing 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 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 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 // 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 NTSTATUS __stdcall NtManageHotPatch(
HOT_PATCH_INFORMATION_CLASS HotPatchInformation,
PVOID HotPatchData,
ULONG Length,
PULONG ReturnLength
); |
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 |
I have tested these on my laptop all ok. run_pe-> 64 bit ------- Payload-> 32bit ✅ |
Some good and bad news here; After installing Windows Preview Update; January 28, 2025—KB5050094 (OS Build 26100.3037) Preview @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; |
0xC0000141 is |
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 ! |
Load 32bit payload ok.

64bit and 32bit are ok on before windows 11 24H2
The text was updated successfully, but these errors were encountered: