Injection techniques
The inject/ package supplies a unified Windows + Linux injection
surface: 16 Windows methods, 3 Linux methods, plus a Pipeline pattern
for custom memory + executor combinations. Every method implements
Injector;
self-process methods additionally implement
SelfInjector
so the freshly-allocated region can be wired into
evasion/sleepmask or
cleanup/memory.WipeAndFree without
re-deriving address and size.
TL;DR
flowchart LR
SC[shellcode] --> M{target}
M -->|self| S[CreateThread / Fiber / EtwpCreateEtwThread / ThreadPool / Callback]
M -->|child suspended| C[EarlyBird APC / Thread Hijack / spoofed args]
M -->|existing PID| R[CRT / NtQueueApcThreadEx / SectionMap / KCT / PhantomDLL]
S -->|via SelfInjector| SM[evasion/sleepmask]
S -->|via SelfInjector| WM[cleanup/memory.WipeAndFree]
Primer — vocabulary
Seven terms recur across this page and the per-method docs:
Self / Local / Remote / Child — the four "target" classes any injection method falls into. Driven entirely by where the shellcode ends up running. See the table below for the per-class API surface.
Injector— interface every method implements:Inject(shellcode []byte) error. The constructor (NewWindowsInjector(cfg)orNewLinuxInjector(cfg)) wires the right method based onConfig.Method.
SelfInjector— extension interface for self-process methods:Region() (addr, size uintptr). Lets downstream consumers (sleepmask, WipeAndFree) recover the allocation without re-deriving it.
*wsyscall.Caller— optional knob (SyscallMethod) selecting how every NTAPI call resolves: WinAPI proc table (default) / direct syscall (asm stub) / indirect syscall (resolve SSN at runtime).nil= WinAPI fallback. Seewin/syscall.APC (Asynchronous Procedure Call) — Windows queue every thread carries; functions enqueued fire when the thread next enters an alertable wait. Several methods (Early Bird, NtQueueApcThreadEx) use the APC delivery path to avoid creating a new thread.
CREATE_SUSPENDED—CreateProcessflag that spawns the child in the suspended state. Threads are created but never resumed until the implant finishes mutating memory. Used by Early Bird, Thread Hijack, and Process Arg Spoofing.Stealth tier — qualitative ranking column in the index below. Combines target class + thread creation + WPM use: low (loud, easy to detect), medium (loses one of the three tells), high (avoids cross-process or thread creation entirely). Not a guarantee against any specific EDR.
Target categories
The target column drives the OPSEC trade-off and the API surface the implant pays for.
| Target | Meaning | Who pays the cost | Typical syscalls |
|---|---|---|---|
| Self | Shellcode runs in the current maldev-built process. | Implant's own process | none cross-process — VirtualAlloc + exec |
| Local | Same as Self, but the technique deliberately avoids spawning a new thread (callback abuse, pool work, module stomping). | Implant's own process | VirtualAlloc + EnumWindows / TpPostWork / stomp |
| Remote | Existing PID supplied by the caller. | Target PID | OpenProcess + VirtualAllocEx + WriteProcessMemory (or a section variant) + thread trigger |
| Child (suspended) | Implant spawns a process in CREATE_SUSPENDED, mutates state, resumes. | Newly-created child | CreateProcess(SUSPENDED) + write + resume / APC / hijack |
Stealth ranking by target (general): Local > Child (suspended) > Remote.
Local avoids cross-process primitives; Child is acceptable because the
process tree is predictable; Remote is the loudest — WriteProcessMemory
into an unrelated running process is a textbook EDR trigger.
Per-method index
| Technique | Method constant | Target | Creates thread? | Uses WriteProcessMemory? | Stealth tier |
|---|---|---|---|---|---|
| CreateRemoteThread | MethodCreateRemoteThread | Remote | yes | yes | low |
| Early Bird APC | MethodEarlyBirdAPC | Child (suspended) | no (APC) | yes | medium |
| Thread Hijack | MethodThreadHijack | Child (suspended) | no | yes | medium |
| NtQueueApcThreadEx | MethodNtQueueApcThreadEx | Remote | no (special APC) | yes | medium |
| Callback execution | ExecuteCallback | Local | no | no | high |
| Thread Pool | ThreadPoolExec | Local | no (pool worker) | no | high |
| Module Stomping | ModuleStomp | Local | caller decides | no | high |
| Section Mapping | SectionMapInject | Remote | yes | no | high |
| Phantom DLL | PhantomDLLInject | Remote (placement only) | no (caller) | yes | very high |
| Kernel Callback Table | KernelCallbackExec | Remote | no | yes | high |
| EtwpCreateEtwThread | MethodEtwpCreateEtwThread | Self | yes (internal) | no | high |
| Process Argument Spoofing | SpawnWithSpoofedArgs | Child (suspended) | n/a — disguise | yes | medium |
| Process Hollowing | Hollow | Child (suspended, image-replaced) | no (reuses spawn thread) | yes | moderate |
Decision flow
flowchart TD
Start([Need to run shellcode]) --> Q1{Self or remote?}
Q1 -->|self-inject| Q2{Need memory stealth?}
Q1 -->|remote process| Q3{Can spawn a new process?}
Q2 -->|yes — image-backed| MS[Module Stomping]
Q2 -->|no| Q4{Avoid thread creation?}
Q4 -->|yes| CB[Callback execution]
Q4 -->|pool is fine| TP[Thread Pool]
Q4 -->|thread is fine| ETW[EtwpCreateEtwThread]
Q3 -->|yes — cover for the spawn| Q5{Need APC stealth?}
Q3 -->|no — existing PID| Q6{Avoid WriteProcessMemory?}
Q5 -->|yes| EB[Early Bird APC]
Q5 -->|register-mutate| TH[Thread Hijack]
Q5 -->|disguise args too| AS[Process Arg Spoofing + EB/TH]
Q6 -->|yes| SM[Section Mapping]
Q6 -->|WPM is OK| Q7{Win10 1903+?}
Q7 -->|yes| APCEX[NtQueueApcThreadEx]
Q7 -->|either way| Q8{Target has windows?}
Q8 -->|yes| KC[KernelCallbackTable]
Q8 -->|no| CRT[CreateRemoteThread]
style MS fill:#2d5016,color:#fff
style SM fill:#2d5016,color:#fff
style CB fill:#2d5016,color:#fff
style TP fill:#2d5016,color:#fff
style KC fill:#2d5016,color:#fff
Quick decision tree
| You want to… | Use |
|---|---|
| …self-inject without spawning a thread | callback-execution.md |
| …self-inject through a thread-pool worker | thread-pool.md |
| …self-inject image-backed (memory looks like a normal module) | module-stomping.md |
| …spawn a clean new process and queue shellcode pre-init | early-bird-apc.md |
| …inject into an existing PID with WPM allowed | create-remote-thread.md |
| …inject into an existing PID without WriteProcessMemory | section-mapping.md |
| …blend with a mapped DLL on disk (path-spoof) | phantom-dll.md |
| …land in the GUI message-loop callback table | kernel-callback-table.md |
| …pivot via a hijacked existing thread | thread-hijack.md |
| …queue a Win10-1903+ APC (special) | nt-queue-apc-thread-ex.md |
| …disguise the spawned child's argv | process-arg-spoofing.md |
| …land via the EtwpCreateEtwThread trampoline | etwp-create-etw-thread.md |
Architecture
All methods implement Injector:
type Injector interface {
Inject(shellcode []byte) error
}
Build() returns a fluent
*InjectorBuilder
that selects syscall mode (WinAPI / NativeAPI / direct / indirect with
arbitrary SSNResolver), pins target,
stacks middleware (WithValidation, WithCPUDelay, WithXOR), and
emits an Injector.
inj, err := inject.Build().
Method(inject.MethodEarlyBirdAPC).
ProcessPath(`C:\Windows\System32\svchost.exe`).
IndirectSyscalls().
Use(inject.WithCPUDelayConfig(inject.CPUDelayConfig{MaxIterations: 10_000_000})).
Create()
The
Pipeline
pattern separates memory setup from execution, allowing mix-and-match
combinations the named methods do not cover:
p := inject.NewPipeline(
inject.RemoteMemory(hProcess, caller),
inject.CreateRemoteThreadExecutor(hProcess, caller),
)
return p.Inject(shellcode)
SelfInjector — recovering the region
Self-process injectors (MethodCreateThread, MethodCreateFiber,
MethodEtwpCreateEtwThread on Windows; MethodProcMem on Linux) place
the shellcode inside the current process. The base Injector interface
throws the address away. The optional SelfInjector interface exposes
it:
type Region struct {
Addr uintptr
Size uintptr
}
type SelfInjector interface {
Injector
InjectedRegion() (Region, bool)
}
Type-assert and feed the region directly into evasion/sleepmask:
inj, _ := inject.NewWindowsInjector(&inject.WindowsConfig{
Config: inject.Config{Method: inject.MethodCreateThread},
SyscallMethod: wsyscall.MethodIndirect,
})
if err := inj.Inject(shellcode); err != nil { return err }
if self, ok := inj.(inject.SelfInjector); ok {
if r, ok := self.InjectedRegion(); ok {
mask := sleepmask.New(sleepmask.Region{Addr: r.Addr, Size: r.Size})
for {
mask.Sleep(30 * time.Second)
}
}
}
Contract:
- Returns
(Region{}, false)before the first successfulInject. - Returns
(Region{}, false)on cross-process methods (CRT, APC, EarlyBird, ThreadHijack, Rtl, NtQueueApcThreadEx) — the region lives in the target, not the implant. - A failed
Injectdoes not clobber a previously-published region. - Decorators (
WithValidation,WithCPUDelay,WithXOR) andPipelineforwardInjectedRegiontransparently.
[!WARNING]
MethodCreateFibernotice —ConvertThreadToFiberpermanently transforms the calling OS thread; Go's M:N scheduler is unaware of fibers, and any goroutine multiplexed onto that thread observes fiber state instead of goroutine state. Real shellcode that callsExitThreadkills the host runtime. Spawn a true OS thread viakernel32!CreateThread(notgo func()—runtime.LockOSThreadis not enough), let it run the fiber dance, let it die when the shellcode exits. The matrix testTestFiber_RealShellcodeis permanently skipped — see the comment ininject/realsc_windows_test.go.
Syscall modes
Every Windows injection method routes through one of the four modes
on the configured *wsyscall.Caller:
| Mode | Constant | Bypasses | Use when |
|---|---|---|---|
| WinAPI | wsyscall.MethodWinAPI | nothing | testing / no EDR |
| Native API | wsyscall.MethodNativeAPI | kernel32 hooks | light EDR |
| Direct syscall | wsyscall.MethodDirect | all userland hooks | medium EDR |
| Indirect syscall | wsyscall.MethodIndirect | userland hooks + CFG check | heavy EDR |
Pair with evasion/unhook to defeat
ntdll inline hooks before the inject fires.
MITRE ATT&CK
| T-ID | Name | Methods | D3FEND counter |
|---|---|---|---|
| T1055 | Process Injection | umbrella | D3-PSA |
| T1055.001 | DLL Injection | CRT, KCT, ModuleStomp, PhantomDLL, SectionMap, ThreadPool, Callback | D3-PSA / D3-PCSV |
| T1055.003 | Thread Execution Hijacking | ThreadHijack | D3-PSA |
| T1055.004 | Asynchronous Procedure Call | EarlyBird, NtQueueApcThreadEx | D3-PSA |
| T1055.015 | ListPlanting | Callback (CreateTimerQueueTimer) | D3-PCSV |
| T1564.010 | Process Argument Spoofing | SpawnWithSpoofedArgs | D3-PSA |
| T1036.005 | Match Legitimate Name or Location | combine with arg spoofing | D3-PSA |
See also
- Operator path: deciding the injection method
- Researcher path: per-method primitives
- Detection eng path: injection telemetry
win/syscall—Callerinterface and SSN resolvers.evasion/sleepmask— pair withSelfInjectorto mask the region during sleep.cleanup/memory— pair to wipe the region on exit.