Drive enumeration & monitoring
TL;DR
Enumerate Windows logical drives (New
LogicalDriveLetters) and watch for new drives (NewWatcher+Watch). EachInfocarries letter, type (TypeFixed/TypeRemovable/TypeNetwork/ …), and volume metadata (label, serial, filesystem). Used for USB-insertion triggers, SMB-share discovery, and removable-media data staging.
Primer
The Windows storage model exposes drives via single-letter
roots (A:-Z:). GetLogicalDrives returns a bitmask of
present letters; GetDriveTypeW classifies each (fixed /
removable / network / CD-ROM / RAM-disk); GetVolumeInformationW
returns label + serial + filesystem.
Operationally:
- Initial discovery — at startup, identify mounted shares, network drives, removable media for staging targets.
- Watch loop — long-running implants poll for new drives; USB key insert is a common data-staging trigger.
How It Works
flowchart LR
GLD[GetLogicalDrives<br>letter bitmask] --> LET[A: B: C: …]
LET --> TYPE[GetDriveTypeW<br>per-letter classification]
LET --> VOL[GetVolumeInformationW<br>label + serial + FS]
TYPE --> INFO[Info<br>Letter / Type / Volume]
VOL --> INFO
INFO --> WATCH[Watcher<br>poll snapshot diff]
WATCH --> EVT[Event<br>EventAdded / EventRemoved]
Watcher polling is configurable (default 200 ms). Snapshots
are diffed; new entries emit EventAdded, removed entries
emit EventRemoved. The FilterFunc lets callers narrow to
e.g. TypeRemovable only.
API Reference
| Symbol | Description |
|---|---|
type Info | Letter + Type + Volume metadata |
type Type | TypeFixed / TypeRemovable / TypeNetwork / TypeCDROM / TypeRAM / TypeUnknown |
type EventKind | EventAdded / EventRemoved |
New(letter) (*Info, error) | Resolve single drive |
LogicalDriveLetters() ([]string, error) | Every present drive letter |
TypeOf(root) Type | Per-root classification |
VolumeOf(root) (*VolumeInfo, error) | Volume label + serial + FS |
NewWatcher(ctx, filter) *Watcher | Watcher (consumed by both watcher modes below) |
(*Watcher).Watch(interval) (<-chan Event, error) | Polling mode. Re-enumerates drives every interval. Headless-process compatible — no message pump required. |
(*Watcher).WatchEvents(buffer) (<-chan Event, error) | Event mode (NEW). Hidden message-only window subscribed to WM_DEVICECHANGE. Zero CPU at idle, ms-latency wake on DBT_DEVICEARRIVAL / DBT_DEVICEREMOVECOMPLETE. Requires an interactive session for the broadcast to land. |
(*Watcher).Snapshot() ([]*Info, error) | Current snapshot |
(*Watcher).WatchEvents(buffer int) (<-chan Event, error)
Event-driven watcher. Internally:
- Locks the goroutine to its OS thread (mandatory — Win32 message pumps can't migrate).
- Registers a
WNDCLASSEXWand creates a message-only window (HWND_MESSAGE). - Receives
WM_DEVICECHANGEand triggersSnapshot+diffonDBT_DEVICEARRIVAL/DBT_DEVICEREMOVECOMPLETE. - On
ctx.Done(), postsWM_CLOSEso the pump exits viaWM_DESTROY → WM_QUIT, destroys the window, unregisters the class, closes the channel.
Parameters:
buffer— channel capacity.0is synchronous;≥ 4recommended for burst-friendly consumers (USB hub re-enumeration emits multipleWM_DEVICECHANGEs in quick succession).
Returns:
<-chan Event— closed onctxcancel.error— non-nil whenRegisterClassExW/CreateWindowExWfails before the pump starts. Per-iteration errors arrive on the channel asEvent{Err: ...}instead of being returned.
Side effects: registers a window class on the calling process for the lifetime of the watcher.
OPSEC: very-quiet — message-only windows aren't enumerated by
EnumWindows and don't appear in Spy++ default views. Visible only
to a debugger walking User Atom Tables for the registered class
name (MaldevDriveWatcher).
Required privileges: unprivileged.
Platform: windows (interactive session — service / SYSTEM
contexts receive no WM_DEVICECHANGE broadcasts).
When to pick which:
| Situation | Use |
|---|---|
| Headless / SYSTEM service / no interactive session | Watch(interval) (polling) |
| Foreground / interactive process | WatchEvents(buffer) (event-driven) |
| You don't care about CPU at idle and want simple semantics | Watch(interval) |
| You want sub-second latency and zero idle CPU | WatchEvents(buffer) |
Examples
Simple — single-drive lookup
import "github.com/oioio-space/maldev/recon/drive"
d, _ := drive.New("C:")
fmt.Printf("%s %s\n", d.Letter, d.Type)
Composed — list all removables
letters, _ := drive.LogicalDriveLetters()
for _, l := range letters {
if drive.TypeOf(l+`\`) == drive.TypeRemovable {
info, _ := drive.New(l)
fmt.Println(info.Letter, info.Volume.Label)
}
}
Advanced — USB-insert trigger (polling)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
w := drive.NewWatcher(ctx, func(d *drive.Info) bool {
return d.Type == drive.TypeRemovable
})
ch, _ := w.Watch(500 * time.Millisecond)
for ev := range ch {
if ev.Kind == drive.EventAdded {
// stage data on the inserted USB
stageData(ev.Drive.Letter)
}
}
Advanced — event-driven (WM_DEVICECHANGE)
Same use-case, zero-CPU at idle. Requires an interactive
session — use the polling variant on services / SYSTEM contexts
where WM_DEVICECHANGE doesn't broadcast.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
w := drive.NewWatcher(ctx, func(d *drive.Info) bool {
return d.Type == drive.TypeRemovable
})
ch, err := w.WatchEvents(4) // buffer 4 — USB hub re-enum bursts
if err != nil {
return err // RegisterClassExW / CreateWindowExW failure
}
for ev := range ch {
if ev.Kind == drive.EventAdded {
stageData(ev.Drive.Letter)
}
}
OPSEC & Detection
| Artefact | Where defenders look |
|---|---|
GetLogicalDrives polling | Universal API — invisible at user-mode |
| Sustained 200 ms polling on idle process | Behavioural EDR may flag CPU patterns; raise interval |
| Subsequent file writes to removable media | EDR file-write telemetry — high-fidelity for sensitive paths |
D3FEND counters:
- D3-FCA — DLP scans on writes to removable media.
Hardening for the operator:
- Raise watch interval (1-2 s) on idle hosts.
- Don't write to removable media while polling — the correlation is the high-fidelity signal.
MITRE ATT&CK
| T-ID | Name | Sub-coverage | D3FEND counter |
|---|---|---|---|
| T1120 | Peripheral Device Discovery | full | D3-FCA |
| T1083 | File and Directory Discovery | partial — drive enumeration is a sibling primitive | D3-FCA |
Limitations
- Two watcher modes, pick per session shape.
Watch(interval)polls and works headless / in services / under SYSTEM (any context with no message broadcast).WatchEvents(buffer)usesWM_DEVICECHANGEand needs an interactive session — service / SYSTEM contexts get no broadcast. Both modes share the sameSnapshot+ diff machinery, so swapping is one line. WatchEventsrequires an OS-thread-locked goroutine. The Win32 message pump cannot migrate threads, so the pump goroutineruntime.LockOSThreads for its entire lifetime. This adds one OS thread to the implant for the duration of the watcher.WatchEventsregisters a window class. The class (MaldevDriveWatcher) is a uint atom in the per-process user-atom table — invisible toEnumWindowsbut discoverable by a debugger walking atom tables.- Volume serial may be 0. Some virtual drives (RAM disks, some VPN drives) report serial 0.
- Network drives cached. Mapped network drives that drop
off may take several poll cycles to surface as
EventRemovedunderWatch.WatchEventsfires onWM_DEVICECHANGE, which DOES broadcast network-drive arrival / removal — better latency on this class. - Windows only. No Linux equivalent in this package; use
inotify/udevdirectly.
See also
recon/folder— sibling Windows special-folder resolution.recon/network— sibling network-interface enumeration (a UNC\\server\share"drive" is a network resource).- Operator path.
- Detection eng path.