libpeconv
libpeconv copied to clipboard
64bit run_pe load 64bit payload not working on windows 11 24H2
Load 32bit payload ok.
64bit and 32bit are ok on before windows 11 24H2
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 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.
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 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 I have, too, realized, that Windows 24H2 broke RunPE, so I stumbled upon your suggestion. I tried it (with
VirtualProtectExbeforeWriteProcessMemory), 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
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:
- https://github.com/hasherezade/transacted_hollowing
- https://github.com/hasherezade/process_overwriting ; video: https://youtu.be/sZ8tMwKfvXw
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.
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.
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:
- https://github.com/hasherezade/transacted_hollowing
- https://github.com/hasherezade/process_overwriting ; video: https://youtu.be/sZ8tMwKfvXw
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 useMEM_IMAGEfor 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...
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).
Ok, I see where exactly it happens. Those are the subsequent functions called:
LdrpInitializeProcessLdrpProcessMappedModuleRtlpInsertOfRemoveScpCfgFunctionTableZwQueryVirtualMemory
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.
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.
32-bit:
64-bit:
Please try the latest builds from AppVeyor:
- 32-bit: https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/isg3nrtg9elacfqy/artifacts
- 64-bit: https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/3hdges29jaayyo58/artifacts
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 - 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:
64-bit:
Please try the latest builds from AppVeyor:
- 32-bit: https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/isg3nrtg9elacfqy/artifacts
- 64-bit: https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/3hdges29jaayyo58/artifacts
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.
@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:
64-bit:
Please try the latest builds from AppVeyor:
- 32-bit: https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/isg3nrtg9elacfqy/artifacts
- 64-bit: https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/3hdges29jaayyo58/artifacts
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.
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
};
@NotCapengeR Thank you it worked like a charm. I have applied patch just after CreateProcess.
@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?
@hasherezade well, looks like both functions,
NtManageHotPatchandNtQueryVirtualMemoryare 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 well, looks like both functions,
NtManageHotPatchandNtQueryVirtualMemoryare 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.
@harunkocacaliskan - do I understand you correctly that the 64-bit loader:
- https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/3hdges29jaayyo58/artifacts runs on your machine without any changes? Also to inject 32-bit payloads?
The problem is only with the 32-bit build, and you managed to run it with NtManagePatch patch?
@harunkocacaliskan - do I understand you correctly that the 64-bit loader:
- https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/3hdges29jaayyo58/artifacts runs on your machine without any changes? Also to inject 32-bit payloads?
The problem is only with the 32-bit build, and you managed to run it with
NtManagePatchpatch?
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.
@harunkocacaliskan - do I understand you correctly that the 64-bit loader:
- https://ci.appveyor.com/project/hasherezade/libpeconv/build/job/3hdges29jaayyo58/artifacts runs on your machine without any changes? Also to inject 32-bit payloads?
The problem is only with the 32-bit build, and you managed to run it with
NtManagePatchpatch?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?
- https://github.com/hasherezade/transacted_hollowing
- https://github.com/hasherezade/process_overwriting
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 0xC00004AC occurs only if the Memory Integrity check is enabled in the system:
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.
I see where exactly it is coming from, indeed the simplest way to get rid of it is to patch NtManagePatch.
-
LdrpQueryCurrentPatchis called on the implant (address in red): -
The function NtManageHotPatch exits with an error
STATUS_CONFLICTING_ADDRESSES:
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.
I see where exactly it is coming from, indeed the simplest way to get rid of it is to patch
NtManagePatch.
LdrpQueryCurrentPatchis called on the implant (address in red):- The function NtManageHotPatch exits with an error
STATUS_CONFLICTING_ADDRESSES:Maybe later I will come up with something less invasive, for now I just made a similar patch as @NotCapengeR , just made the
NtManagePatchreturnSTATUS_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
);
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
- 64-bit build: https://ci.appveyor.com/project/hasherezade/libpeconv/builds/51469072/job/mpjl683xcy93cyxs/artifacts
- 32-bit build: https://ci.appveyor.com/project/hasherezade/libpeconv/builds/51469072/job/9ku0o93l6x9jtfv5/artifacts
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.
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
- 64-bit build: https://ci.appveyor.com/project/hasherezade/libpeconv/builds/51469072/job/mpjl683xcy93cyxs/artifacts
- 32-bit build: https://ci.appveyor.com/project/hasherezade/libpeconv/builds/51469072/job/9ku0o93l6x9jtfv5/artifacts
But I don't really like the idea of removing the function
NtManageHotPatchaltogether. 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 ✅