Controlled Blue Screen of Death

← cleanup index · docs/index

[!CAUTION] This is a destructive, irreversible primitive. Calling bsod.Trigger crashes the host immediately. Use only as a last-resort kill switch when stopping log shipping or process collection is more valuable than the host. The example tests are gated behind a build tag and do not run by default.

TL;DR

Last-resort kill switch: crash the host so in-memory state (unflushed logs, EDR's pending event queue, your secrets) is destroyed before anything can hit disk.

You're up against…UseCost
EDR about to ship telemetry, no time to wipe gracefullyTriggerOne syscall, host gone
Just want to wipe memoryDon't use this — see memory-wipeReversible cleanup

Destructive and irreversible. The host reboots immediately. Use only when stopping log shipping is more valuable than the host itself. Tests for this primitive are gated behind a build tag and never run in CI by default.

What this DOES achieve:

  • Pending writes in EDR's user-mode buffers, Event Log service queues, and Sysmon's pre-flush state are all lost.
  • Memory dumps that would have captured your shellcode / keys are gone — kernel produces a minidump on next boot which excludes user-mode allocations by default.
  • One-syscall trigger; no need for admin shell — only SeShutdownPrivilege (granted by default to interactive users).

What this does NOT achieve:

  • Doesn't hide that it happened — Event Log entry "0xDEADBEEF" + MEMORY.DMP on disk after reboot tell the forensic team a process called NtRaiseHardError. This is a panic button, not stealth.
  • Doesn't survive the reboot — your implant is gone. Persistence (auto-start service / registry run key / scheduled task) must be in place beforehand.
  • Doesn't wipe disk artefacts — anything already on disk (your dropper, prefetch, recent file activity) survives. Pair with cleanup/wipe BEFORE crashing.

Primer

NtRaiseHardError is the kernel's mechanism for raising errors from user-mode that the kernel decides how to handle. With the right parameters (notably OptionShutdownSystem), the kernel treats the report as an unrecoverable system fault and crashes immediately with the specified bug-check code (KeBugCheckEx).

The technique requires SeShutdownPrivilege, which any process running as a Medium-IL user with that privilege available can enable via RtlAdjustPrivilege. Most local accounts have it.

Use cases:

  • Operator wants to abort an exfil operation when an EDR alert fires — faster to crash the host than to clean up.
  • Anti-forensic last resort: terminate the implant + all running collection agents in one shot.
  • Red-team exercise: validate the blue team's "host went silent" response.

How it works

sequenceDiagram
    participant Caller
    participant ntdll
    participant Kernel
    Caller->>ntdll: RtlAdjustPrivilege(SeShutdownPrivilege, true)
    ntdll-->>Caller: NTSTATUS_SUCCESS
    Caller->>ntdll: NtRaiseHardError(STATUS_ASSERTION_FAILURE, 0, 0, NULL, 6 /* OptionShutdownSystem */)
    ntdll->>Kernel: NtRaiseHardError syscall
    Kernel->>Kernel: KeBugCheckEx(0xc0000420, ...)
    Note over Kernel: BSOD — host crashes

OptionShutdownSystem (value 6) tells the kernel to treat the hard error as fatal. The supplied status code propagates into the bug-check parameter shown on the BSOD screen.

API → godoc

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

Examples

Simple

//go:build windows
package main

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

func main() {
    caller := wsyscall.New(wsyscall.MethodIndirect, nil)
    if err := bsod.Trigger(caller); err != nil {
        // Reached only on failure; host doesn't come back on success.
        panic(err)
    }
}

Composed — guard with explicit operator confirmation

if !operatorConfirmed("really BSOD?") {
    return
}
// Wipe in-memory secrets first so even crash dumps reveal less.
memory.WipeAndFree(payloadAddr, payloadSize)
_ = bsod.Trigger(caller)

Advanced — chain with operator-side TLS handshake

A pattern from real ops: BSOD on receipt of a "BURN" command from C2.

go func() {
    for cmd := range c2Channel {
        if cmd == "BURN" {
            memory.WipeAndFree(stateAddr, stateSize)
            _ = bsod.Trigger(caller)
        }
    }
}()

OPSEC & Detection

ArtefactWhere defenders look
Bug-check itselfSystem event log Event 1001 (BugCheck), Event 41 (Kernel-Power) on next boot
Memory dump (if configured)C:\Windows\MEMORY.DMP (or C:\Dumps\ per WER LocalDumps) names the originating process
SeShutdownPrivilege adjustmentPre-crash ETW from Microsoft-Windows-Security-Auditing (Event 4673 with audit policy on)
Cluster of identical bug-checks across hostsSysmon-shipped Event 1001 in SIEM

D3FEND counter: D3-PSEP (weak — bug-checks are inherent to OS); the real counter is availability monitoring (host-up dashboard) and crash-dump analysis post-incident.

MITRE ATT&CK

T-IDNameSub-coverage
T1529System Shutdown/RebootForced bug-check variant

Limitations

  • SeShutdownPrivilege required. Sandboxed processes (Edge Renderer, AppContainer Store apps) cannot adjust this privilege.
  • Crash dump destination depends on system config. If the host is configured for kernel-only minidumps, the dump may not include the full process state — but it WILL include the originating thread.
  • VM hosts with snapshot-on-crash configurations may preserve state better than the operator wants.
  • Win11 modern standby — some bug-checks are recoverable on Win11 if the system is on AC power and the kernel decides to attempt recovery (rare for STATUS_ASSERTION_FAILURE-class codes).

See also