Injection techniques

← maldev README · docs/index

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]

Target categories

The target column drives the OPSEC trade-off and the API surface the implant pays for.

TargetMeaningWho pays the costTypical syscalls
SelfShellcode runs in the current maldev-built process.Implant's own processnone cross-process — VirtualAlloc + exec
LocalSame as Self, but the technique deliberately avoids spawning a new thread (callback abuse, pool work, module stomping).Implant's own processVirtualAlloc + EnumWindows / TpPostWork / stomp
RemoteExisting PID supplied by the caller.Target PIDOpenProcess + VirtualAllocEx + WriteProcessMemory (or a section variant) + thread trigger
Child (suspended)Implant spawns a process in CREATE_SUSPENDED, mutates state, resumes.Newly-created childCreateProcess(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

TechniqueMethod constantTargetCreates thread?Uses WriteProcessMemory?Stealth tier
CreateRemoteThreadMethodCreateRemoteThreadRemoteyesyeslow
Early Bird APCMethodEarlyBirdAPCChild (suspended)no (APC)yesmedium
Thread HijackMethodThreadHijackChild (suspended)noyesmedium
NtQueueApcThreadExMethodNtQueueApcThreadExRemoteno (special APC)yesmedium
Callback executionExecuteCallbackLocalnonohigh
Thread PoolThreadPoolExecLocalno (pool worker)nohigh
Module StompingModuleStompLocalcaller decidesnohigh
Section MappingSectionMapInjectRemoteyesnohigh
Phantom DLLPhantomDLLInjectRemote (placement only)no (caller)yesvery high
Kernel Callback TableKernelCallbackExecRemotenoyeshigh
EtwpCreateEtwThreadMethodEtwpCreateEtwThreadSelfyes (internal)nohigh
Process Argument SpoofingSpawnWithSpoofedArgsChild (suspended)n/a — disguiseyesmedium

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 threadcallback-execution.md
…self-inject through a thread-pool workerthread-pool.md
…self-inject image-backed (memory looks like a normal module)module-stomping.md
…spawn a clean new process and queue shellcode pre-initearly-bird-apc.md
…inject into an existing PID with WPM allowedcreate-remote-thread.md
…inject into an existing PID without WriteProcessMemorysection-mapping.md
…blend with a mapped DLL on disk (path-spoof)phantom-dll.md
…land in the GUI message-loop callback tablekernel-callback-table.md
…pivot via a hijacked existing threadthread-hijack.md
…queue a Win10-1903+ APC (special)nt-queue-apc-thread-ex.md
…disguise the spawned child's argvprocess-arg-spoofing.md
…land via the EtwpCreateEtwThread trampolineetwp-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 successful Inject.
  • Returns (Region{}, false) on cross-process methods (CRT, APC, EarlyBird, ThreadHijack, Rtl, NtQueueApcThreadEx) — the region lives in the target, not the implant.
  • A failed Inject does not clobber a previously-published region.
  • Decorators (WithValidation, WithCPUDelay, WithXOR) and Pipeline forward InjectedRegion transparently.

[!WARNING] MethodCreateFiber noticeConvertThreadToFiber permanently 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 calls ExitThread kills the host runtime. Spawn a true OS thread via kernel32!CreateThread (not go func()runtime.LockOSThread is not enough), let it run the fiber dance, let it die when the shellcode exits. The matrix test TestFiber_RealShellcode is permanently skipped — see the comment in inject/realsc_windows_test.go.

Syscall modes

Every Windows injection method routes through one of the four modes on the configured *wsyscall.Caller:

ModeConstantBypassesUse when
WinAPIwsyscall.MethodWinAPInothingtesting / no EDR
Native APIwsyscall.MethodNativeAPIkernel32 hookslight EDR
Direct syscallwsyscall.MethodDirectall userland hooksmedium EDR
Indirect syscallwsyscall.MethodIndirectuserland hooks + CFG checkheavy EDR

Pair with evasion/unhook to defeat ntdll inline hooks before the inject fires.

MITRE ATT&CK

T-IDNameMethodsD3FEND counter
T1055Process InjectionumbrellaD3-PSA
T1055.001DLL InjectionCRT, KCT, ModuleStomp, PhantomDLL, SectionMap, ThreadPool, CallbackD3-PSA / D3-PCSV
T1055.003Thread Execution HijackingThreadHijackD3-PSA
T1055.004Asynchronous Procedure CallEarlyBird, NtQueueApcThreadExD3-PSA
T1055.015ListPlantingCallback (CreateTimerQueueTimer)D3-PCSV
T1564.010Process Argument SpoofingSpawnWithSpoofedArgsD3-PSA
T1036.005Match Legitimate Name or Locationcombine with arg spoofingD3-PSA

See also