Suspending/Freezing Windows Processes with Job Objects
Welcome to this new Medium post, today we are going to explore how to suspend and freeze Windows processes using Job Objects, a less known primitive that lets you halt an entire process without…

Welcome to this new Medium post, today we are going to explore how to suspend and freeze Windows processes using Job Objects, a less known primitive that lets you halt an entire process without touching a single thread handle
This technique comes from the next GitHub repository:
https://github.com/zero2504/FrostLock-Injection

Want to go deeper into Windows offensive development?
Video-based courses from beginner to advanced, and text-based modules (mini courses) with new releases constantly. Plus a technique database with 100+ real techniques updated weekly, and custom C2 agents and consulting for teams.
Introduction
Before we walk through the implementation, you need to understand three things: what a Job Object is, what JobObjectFreezeInformation does, and why this is different from calling SuspendThread
Job Objects are a Windows mechanism designed to group one or more processes under a single management handle. When you assign a process to a Job, the kernel can enforce policies on the entire group at once
SetInformationJobObject is the primary API for configuring a Job:
BOOL SetInformationJobObject(
[in] HANDLE hJob,
[in] JOBOBJECTINFOCLASS JobObjectInformationClass,
[in] LPVOID lpJobObjectInformation,
[in] DWORD cbJobObjectInformationLength
);
In the documentation you can find all the classes and members to modify in a job
https://learn.microsoft.com/en-us/windows/win32/api/jobapi2/nf-jobapi2-setinformationjobobject
Since JobObjectFreezeInformation is undocumented, we need to define the structures ourselves:
typedef struct _JOBOBJECT_WAKE_FILTER
{
ULONG HighEdgeFilter;
ULONG LowEdgeFilter;
} JOBOBJECT_WAKE_FILTER, *PJOBOBJECT_WAKE_FILTER;
typedef struct _JOBOBJECT_FREEZE_INFORMATION
{
union
{
ULONG Flags;
struct
{
ULONG FreezeOperation : 1;
ULONG FilterOperation : 1;
ULONG SwapOperation : 1;
ULONG Reserved : 29;
};
};
BOOLEAN Freeze;
BOOLEAN Swap;
UCHAR Reserved0[2];
JOBOBJECT_WAKE_FILTER WakeFilter;
} JOBOBJECT_FREEZE_INFORMATION, *PJOBOBJECT_FREEZE_INFORMATION;
The two fields that matter for a basic freeze: FreezeOperation set to 1 tells the kernel this is a freeze or unfreeze operation, and Freeze set to TRUE or FALSE controls the direction
Methodology
Create a Job Object: Call CreateJobObject(NULL, NULL) to get a handle. The handle stays alive for as long as we need to freeze and unfreeze the target
Assign the target process: Call AssignProcessToJobObject with our Job handle and the target process handle
Send the freeze instruction: Populate JOBOBJECT_FREEZE_INFORMATION with FreezeOperation = 1 and Freeze = TRUE, then call SetInformationJobObject with class 18 cast to JOBOBJECTINFOCLASS
Unfreeze: Same call, same structure, same Job handle, only Freeze changes to FALSE
Implementation
FreezeProcess
bool FreezeProcess(HANDLE hProcess) {
globalJobHandle = CreateJobObject(NULL, NULL);
if (!globalJobHandle) {
std::cerr << "Failed to create Job Object. Error: " << GetLastError() << std::endl;
return false;
}
if (!AssignProcessToJobObject(globalJobHandle, hProcess)) {
std::cerr << "Failed to assign process to Job Object. Error: " << GetLastError() << std::endl;
CloseHandle(globalJobHandle);
globalJobHandle = NULL;
return false;
}
JOBOBJECT_FREEZE_INFORMATION freezeInfo = { 0 };
freezeInfo.FreezeOperation = 1;
freezeInfo.Freeze = TRUE;
if (!SetInformationJobObject(globalJobHandle, (JOBOBJECTINFOCLASS)JobObjectFreezeInformation, &freezeInfo, sizeof(freezeInfo))) {
std::cerr << "Failed to freeze Job Object. Error: " << GetLastError() << std::endl;
CloseHandle(globalJobHandle);
globalJobHandle = NULL;
return false;
}
return true;
}UnfreezeProcess
bool UnfreezeProcess(HANDLE hProcess) {
if (!globalJobHandle) {
std::cerr << "No valid job handle available for thawing. Did you freeze the process first?" << std::endl;
return false;
}
JOBOBJECT_FREEZE_INFORMATION freezeInfo = { 0 };
freezeInfo.FreezeOperation = 1;
freezeInfo.Freeze = FALSE;
if (!SetInformationJobObject(globalJobHandle, (JOBOBJECTINFOCLASS)JobObjectFreezeInformation, &freezeInfo, sizeof(freezeInfo))) {
std::cerr << "Failed to thaw Job Object. Error: " << GetLastError() << std::endl;
return false;
}
return true;
}The hProcess parameter is not used in the unfreeze path, the Job Object handle is sufficient
Full Code
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <Windows.h>
#include <thread>
#include <chrono>
#include <TlHelp32.h>
#define JobObjectFreezeInformation 18
typedef struct _JOBOBJECT_WAKE_FILTER
{
ULONG HighEdgeFilter;
ULONG LowEdgeFilter;
} JOBOBJECT_WAKE_FILTER, *PJOBOBJECT_WAKE_FILTER;
typedef struct _JOBOBJECT_FREEZE_INFORMATION
{
union
{
ULONG Flags;
struct
{
ULONG FreezeOperation : 1;
ULONG FilterOperation : 1;
ULONG SwapOperation : 1;
ULONG Reserved : 29;
};
};
BOOLEAN Freeze;
BOOLEAN Swap;
UCHAR Reserved0[2];
JOBOBJECT_WAKE_FILTER WakeFilter;
} JOBOBJECT_FREEZE_INFORMATION, *PJOBOBJECT_FREEZE_INFORMATION;
HANDLE globalJobHandle = NULL;
bool FreezeProcess(HANDLE hProcess) {
globalJobHandle = CreateJobObject(NULL, NULL);
if (!globalJobHandle) {
std::cerr << "Failed to create Job Object. Error: " << GetLastError() << std::endl;
return false;
}
if (!AssignProcessToJobObject(globalJobHandle, hProcess)) {
std::cerr << "Failed to assign process to Job Object. Error: " << GetLastError() << std::endl;
CloseHandle(globalJobHandle);
globalJobHandle = NULL;
return false;
}
JOBOBJECT_FREEZE_INFORMATION freezeInfo = { 0 };
freezeInfo.FreezeOperation = 1;
freezeInfo.Freeze = TRUE;
if (!SetInformationJobObject(globalJobHandle, (JOBOBJECTINFOCLASS)JobObjectFreezeInformation, &freezeInfo, sizeof(freezeInfo))) {
std::cerr << "Failed to freeze Job Object. Error: " << GetLastError() << std::endl;
CloseHandle(globalJobHandle);
globalJobHandle = NULL;
return false;
}
return true;
}
bool UnfreezeProcess(HANDLE hProcess) {
if (!globalJobHandle) {
std::cerr << "No valid job handle available for thawing. Did you freeze the process first?" << std::endl;
return false;
}
JOBOBJECT_FREEZE_INFORMATION freezeInfo = { 0 };
freezeInfo.FreezeOperation = 1;
freezeInfo.Freeze = FALSE;
if (!SetInformationJobObject(globalJobHandle, (JOBOBJECTINFOCLASS)JobObjectFreezeInformation, &freezeInfo, sizeof(freezeInfo))) {
std::cerr << "Failed to thaw Job Object. Error: " << GetLastError() << std::endl;
return false;
}
std::cout << "Process thawed successfully!" << std::endl;
return true;
}
int main() {
DWORD pid;
printf("Please enter the PID of the target Process: \n");
scanf("%d", &pid);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProcess == NULL) {
printf("Failed to open Process PID %d\n", pid);
printf("Error Code: %d", GetLastError());
}
else {
if (FreezeProcess(hProcess)) {
std::cout << "Process frozen" << std::endl;
}
}
}
Proof of Concept
Now let’s just run this code:
Let’s run a notepad, and then pass the Process Id to the program:

The process and all its thread are now suspended!
Detection
0x12DarkSandbox
Test your own payloads against the same stack with 5 free scans on sign up, or go deeper with a scan pack or monthly plan
Now let’s upload this binary into the sandbox:
https://0x12darksandbox.net/results/352
YARA
Here a YARA rule to detect this technique:
rule JobObject_ProcessFreeze
{
meta:
author = "0x12 Dark Development"
description = "Detects process freeze via undocumented JobObjectFreezeInformation (class 18)"
reference = "https://0x12darkdev.net"
strings:
$api1 = "CreateJobObject" ascii wide
$api2 = "AssignProcessToJobObject" ascii wide
$api3 = "SetInformationJobObject" ascii wide
// JOBOBJECT_FREEZE_INFORMATION struct init: Flags union set to 1 (FreezeOperation bit)
$struct_init = { C7 44 24 ?? 01 00 00 00 }
// Freeze = TRUE pattern after zeroed struct
$freeze_true = { C6 44 24 ?? 01 }
// Freeze = FALSE (unfreeze path)
$freeze_false = { C6 44 24 ?? 00 }
condition:
uint16(0) == 0x5A4D and
all of ($api*) and
($struct_init and ($freeze_true or $freeze_false))
}
The rule matches PE files that import the three Job Object APIs and contain byte code consistent with populating a JOBOBJECT_FREEZE_INFORMATION structure with FreezeOperation = 1
Here you have my collection of YARA rules:
GitHub - S12cybersecurity/YaraRules: Collection of interesting Yara Rules
Conclusions
Job Object freezing is a clean primitive for suspend a process without touching thread handles. From a telemetry perspective it looks nothing like a SuspendThread loop, which is the expected shape of a process suspension attack
📌 Follow me: 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter | YouTube
S12.