Skip to content

Execution & Persistence Emulation

Execution and persistence emulation is the controlled reproduction of the techniques adversaries use to run code and survive reboots — script interpreters (T1059), scheduled tasks (T1053.005), registry autoruns (T1547.001), service installation (T1543.003), WMI subscriptions (T1546.003), startup folders (T1037.005), COM hijacks (T1546.015) — using benign, clearly-marked implementations whose only goal is to generate the exact telemetry each technique produces. Every emulated persistence mechanism writes a marker name (EMU-*), a harmless action (a log line, an echo), and ships with its own cleanup; the emulation’s success metric is not “did we persist” but “did the persistence produce the events that should page a responder”. This module is where emulation gets intimate with the endpoint: these techniques touch the same registry hives, task scheduler, and service control manager that real malware touches, which is precisely why they must be the most carefully scoped, named, and cleaned-up artifacts in the whole program.

Persistence is where detection programs separate: entry vectors get everyone’s attention, but the surviving mechanisms are what an attacker actually relies on, and estate coverage of persistence telemetry is uniformly weaker than coverage of obvious things like malware signatures. Emulating persistence validates the two layers that matter — the event stream (does the task creation, registry write, service install generate the right events with the right fields?) and the detection layer (do rules fire on those events?) — and it does so with artifacts you fully control, so the measurement is repeatable and the cleanup provable. The techniques here are also the highest-value false-positive class in the estate: legitimate software uses scheduled tasks, services, and run keys constantly, which means persistence detections live or die on baselining — and emulation is how you safely generate the “known persistence” side of that baseline. For red-team realism, persistence emulation rehearses the operational discipline adversaries need (choosing mechanisms, verifying survival, cleaning traces) in a form where mistakes cost a ticket rather than a breach. And for the blue team, the after-action of a persistence exercise is unusually concrete: either the 4698 fired, or it didn’t — there is no ambiguity to argue about in the debrief.

The execution-to-persistence chain decomposes into stages, each with its telemetry contract:

EXECUTION -> PERSISTENCE CHAIN (emulation view)
[A] EXECUTION telemetry contract
script interpreter run -> 4104 (PS blocks), 4688, Sysmon 1
LOLbin execution -> 4688 with parent chain
[B] PERSISTENCE INSTALL telemetry contract
schtasks /create -> 4698 + Sysmon 1 + 4688
reg add Run key -> Sysmon 12/13 + 4688
sc create service -> 7045 + Sysmon 1 + 4688
startup folder .lnk -> Sysmon 11 + 4688
WMI event subscription -> Sysmon 19/20/21 (lab only!)
COM object hijack -> Sysmon 12/13 (HKCU overwrite)
[C] SURVIVAL CHECK (reboot) -> new process at boot: 4688 + EID 1
[D] CLEANUP -> 4699 (task deleted), Sysmon 12/13
(key removed), 7045-adjacent logs

The emulation craft is in the shaping: every mechanism must generate its real telemetry while doing nothing dangerous. The conventions that make this safe are mechanical and non-negotiable. Naming: every artifact is prefixed EMU- (tasks EMU-Telemetry, services EMU-Svc, run keys EMU-Check) — this makes the artifact findable, the cleanup provable, and accidental persistence obvious. Actions: the persisted command is a log write or echo (cmd /c echo EMU-check >> %TEMP%\emu.log) — never a downloader, never a real binary, nothing network-touching. Scope: user-level mechanisms (HKCU run keys, per-user tasks, startup folder) run without elevation wherever possible; kernel-adjacent or machine-wide mechanisms (services, WMI permanent subscriptions, COM hijack of system objects) run only on isolated lab VMs — WMI permanent event subscriptions in particular can destabilize a host and survive naive cleanup, which earns them the lab-only tag. Verification: after install, the harness checks the mechanism exists from the OS view (task listed, key present, service registered) and after a reboot where the exercise requires survival testing — persistence that doesn’t survive is a finding about the mechanism, and the report should say so. Cleanup: every harness run writes its own manifest of created artifacts, and the cleanup step removes them by manifest, then re-verifies absence — the difference between “we cleaned up” and “we can show we cleaned up”.

TechniqueCommand shape (emulated)Expected telemetryCleanupScope
Scheduled taskschtasks /create /tn EMU-Telemetry /tr "cmd /c echo ..."4698, 4688, Sysmon 1schtasks /delete /tnUser-level OK
Run key (HKCU)reg add HKCU\...\Run /v EMU-Check /d "cmd /c echo"Sysmon 12/13, 4688reg deleteUser-level OK
Run key (HKLM)same, HKLMSysmon 12/13 (admin)reg deleteAdmin, lab
Service installsc create EMU-Svc binPath= "cmd /c echo"7045, Sysmon 1sc deleteAdmin, lab
Startup folder .lnkdrop EMU.lnk pointing to echo cmdSysmon 11delete fileUser-level OK
WMI subscriptionpermanent event consumer (lab)Sysmon 19/20/21manual — fragileLab VM ONLY
COM hijack (HKCU overwrite)InprocServer32 pointing to stubSysmon 12/13restore default keyLab VM ONLY
Logon scriptUserInitMprLogonScript valueSysmon 12/13, 4688 at logonreg deleteUser-level OK
Scheduled task (boot trigger)/sc onstart /ru SYSTEM4698 + boot-time 4688schtasks /deleteAdmin, lab
BITS job persistencebitsadmin /create EMU-B /addfile ...4688, Sysmon 1, BITS eventsbitsadmin /cancelUser-level OK

persistemu.py — a persistence emulation orchestrator for Windows: executes a curated set of safe persistence mechanisms (each EMU--named, echo-actioned), verifies each from the OS view, writes a manifest of everything it created, and — critically — cleans up by manifest with re-verification. Designed to run on a lab VM; refuses machine-wide mechanisms unless --i-am-isolated is passed. Windows-only, stdlib only.

#!/usr/bin/env python3
"""persistemu.py — persistence emulation orchestrator (Windows, lab).
Every artifact is EMU-prefixed, echo-actioned, and manifest-tracked.
Run on lab VMs. Machine-wide mechanisms require --i-am-isolated.
Usage:
python persistemu.py --all [--i-am-isolated] [--report emu_manifest.json]
python persistemu.py --cleanup --report emu_manifest.json
stdlib only (subprocess, winreg, json).
"""
import argparse, json, os, subprocess, sys, datetime
MANIFEST_DEFAULT = "emu_manifest.json"
ECHO_CMD = "cmd /c echo EMU-persistence-check >> %TEMP%\\emu.log"
RUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
def run(cmd, capture=True):
"""Shell out and return (ok, output)."""
r = subprocess.run(cmd, capture_output=capture, text=True,
shell=False)
return r.returncode == 0, (r.stdout + r.stderr).strip()
def manifest(path):
if os.path.exists(path):
return json.load(open(path, encoding="utf-8"))
return {"created": [], "when": None, "host_ok": None}
def save_manifest(path, m):
m["when"] = datetime.datetime.now(
datetime.timezone.utc).isoformat()
json.dump(m, open(path, "w", encoding="utf-8"), indent=2)
# ---------------- techniques ----------------
def tech_task(m):
"""Scheduled task EMU-Telemetry (user-level)."""
ok, out = run(["schtasks", "/create", "/tn", "EMU-Telemetry",
"/tr", ECHO_CMD, "/sc", "onlogon", "/f"])
if ok:
m["created"].append({"type": "task", "name": "EMU-Telemetry",
"remove": ["schtasks", "/delete",
"/tn", "EMU-Telemetry", "/f"]})
return ok, out
def tech_runkey(m):
"""HKCU run key EMU-Check."""
ok, out = run(["reg", "add", f"HKCU\\{RUN_KEY}",
"/v", "EMU-Check", "/d", ECHO_CMD, "/f"])
if ok:
m["created"].append({"type": "regvalue",
"hive": "HKCU", "path": RUN_KEY,
"name": "EMU-Check",
"remove": ["reg", "delete",
rf"HKCU\{RUN_KEY}",
"/v", "EMU-Check", "/f"]})
return ok, out
def tech_startup(m):
"""Startup folder EMU.cmd."""
startup = os.path.expandvars(
r"%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup")
p = os.path.join(startup, "EMU-check.cmd")
try:
with open(p, "w") as fh:
fh.write("@echo EMU-persistence-check >> "
"%TEMP%\\emu.log\n")
m["created"].append({"type": "file", "path": p,
"remove": ["__del__", p]})
return True, p
except OSError as e:
return False, str(e)
def tech_service(m, isolated):
"""Service EMU-Svc — machine-wide; requires isolation flag."""
if not isolated:
return False, "machine-wide mechanism refused " \
"(pass --i-am-isolated on lab VM)"
ok, out = run(["sc", "create", "EMU-Svc",
"binPath=", f'"{ECHO_CMD}"', "start=", "demand"])
if ok:
m["created"].append({"type": "service",
"name": "EMU-Svc",
"remove": ["sc", "delete", "EMU-Svc"]})
return ok, out
def verify(m):
"""OS-view verification of every created artifact."""
results = []
for item in m["created"]:
t = item["type"]
if t == "task":
ok, out = run(["schtasks", "/query", "/tn",
item["name"]])
results.append(("task", item["name"],
"present" if ok else "MISSING"))
elif t == "regvalue":
ok, out = run(["reg", "query",
f'{item["hive"]}\\{item["path"]}',
"/v", item["name"]])
results.append(("regvalue", item["name"],
"present" if ok else "MISSING"))
elif t == "file":
results.append(("file", item["path"],
"present" if os.path.exists(item["path"])
else "MISSING"))
elif t == "service":
ok, out = run(["sc", "query", item["name"]])
results.append(("service", item["name"],
"present" if ok else "MISSING"))
return results
def cleanup(m):
"""Remove every manifest artifact; re-verify absence."""
results = []
for item in m["created"]:
rem = item.get("remove")
if not rem:
results.append((item["type"], "no-remove-cmd",
"MANUAL"))
continue
if rem[0] == "__del__":
try:
os.remove(rem[1])
results.append((item["type"], rem[1],
"removed"))
except OSError:
results.append((item["type"], rem[1],
"ALREADY-GONE"))
continue
ok, out = run(rem)
results.append((item["type"], rem[2] if len(rem) > 2
else str(rem), "removed" if ok else
"FAILED (manual)"))
return results
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--all", action="store_true")
ap.add_argument("--cleanup", action="store_true")
ap.add_argument("--i-am-isolated", action="store_true")
ap.add_argument("--report", default=MANIFEST_DEFAULT)
args = ap.parse_args()
m = manifest(args.report)
if args.cleanup:
print("[*] cleaning up by manifest...")
for t, name, status in cleanup(m):
print(f" [{status:>14}] {t:<9} {name}")
m["created"] = []
save_manifest(args.report, m)
print("[+] cleanup pass complete "
"(FAILED rows need manual removal)")
return
if not args.all:
sys.exit("choose --all or --cleanup")
plan = [("task", tech_task), ("runkey", tech_runkey),
("startup", tech_startup)]
if args.i_am_isolated:
plan.append(("service", lambda mm: tech_service(
mm, True)))
for name, fn in plan:
ok, out = fn(m)
print(f"[{'OK' if ok else 'FAIL':>4}] {name}: "
f"{out[:80]}")
save_manifest(args.report, m)
print("\n[*] verification pass:")
for t, name, status in verify(m):
print(f" [{status:>7}] {t:<9} {name}")
print(f"\n[+] manifest: {args.report} — run with --cleanup "
f"to remove every artifact")
if __name__ == "__main__":
main()
  • The manifest model (lines 25-31, persisted via save_manifest at lines 34-38) is the safety architecture: every created artifact self-describes its removal command ("remove": [...]) at creation time, so cleanup never relies on the operator remembering what was done. The manifest file is the exercise’s cleanup contract.
  • tech_task() (lines 41-51) creates a user-level scheduled task EMU-Telemetry with the echo action — the telemetry-relevant part is /sc onlogon, which generates 4698 at creation and task execution at next logon. The removal command is recorded in the same breath as the creation.
  • tech_runkey() (lines 54-64) writes HKCU\...\Run\EMU-Check — the classic T1547.001 shape — via reg add, with reg delete pre-recorded. HKCU keeps it unprivileged; the HKLM variant deliberately doesn’t ship here because machine-wide autoruns belong in the isolated lab tier.
  • tech_service() (lines 67-79) is the escalation-tier example: refused outright unless --i-am-isolated was passed (lines 72-74) — the flag is a self-declaration that the host is a disposable VM, and the refusal message teaches the rule rather than silently downgrading.
  • verify() (lines 82-104) re-reads every artifact from the OS view — schtasks /query, reg query, filesystem, sc query — because “the command exited 0” is not the same as “the persistence exists”; verification is where mechanism-specific surprises surface.
  • cleanup() (lines 107-131) removes by manifest and reports per-artifact status including ALREADY-GONE (fine) and FAILED (manual) (escalate) — the honest cleanup report, which is what the after-action attaches.

Each mechanism reaches the target host as a real, living configuration change — a real task in the scheduler, a real value in the hive — and that reality is the point: the only way to validate persistence telemetry is to actually persist. The telemetry reaches the sensors exactly as designed: 4698 lands in Security, Sysmon 12/13 record the registry write with full value data, 7045 announces the service, and the logon-triggered task produces a boot-time 4688 whose parent chain (svchost → cmd) is the shape your lineage rules care about. The manifest reaches the blue team as the exercise’s shared artifact: they know every EMU- name that will exist before it does, their triage of persistence events during the window is tag-and-verify rather than investigation, and any unmanifested persistence-shaped event during the window is by definition either a false positive worth cataloging or a real finding worth escalation. The cleanup report reaches the after-action as the closure evidence — no lingering artifacts, or an honest list of what needs manual removal (WMI subscriptions, famously). At the program level, the exercise reaches the detection backlog with unusual clarity: each mechanism’s telemetry contract either validated (rule exists and fired) or didn’t (gap, with the exact event ID that should have fired), and the mechanism coverage matrix — which persistence layers does this estate actually see? — becomes the standing metric that quarterly re-runs trend.

  1. Preflight — telemetrycheck (T1) confirms 4698/7045/Sysmon 12-13 channels fresh on lab VM
  2. Snapshot — autorunsdiff baseline captured (pre-emulation persistence inventory)
  3. Install — persistemu.py —all —i-am-isolated: task, run key, startup file, service created; manifest written
  4. Verify — OS-view check: all four artifacts present; EMU- naming confirmed
  5. Telemetry — blue role confirms 4698 + Sysmon 13 + 7045 arrivals match the contract
  6. Reboot — VM restarted; task and run key survive; startup file executes; 4688 boot events captured
  7. Diff — autorunsdiff re-run: exactly 4 new artifacts, all EMU-prefixed: zero unexpected persistence
  8. Cleanup — persistemu.py —cleanup: 4/4 removed and absence re-verified; manifest zeroed

autorunsdiff.py — the persistence baseline differ (blue-side counterpart): captures a snapshot of the standard autoruns locations (run keys, scheduled tasks, services, startup folders) into a JSON baseline, then diffs any later capture against it — flagging exactly the new persistence that appeared between snapshots. It is simultaneously the emulation’s verification tool and the estate’s standing persistence-tripwire design.

  • Standard locations, complete coverage. The tool covers the autoruns locations that generate 95% of real persistence (HKLM/HKCU run keys and RunOnce, scheduled tasks, services, startup folders, UserInitMprLogonScript) — exhaustive coverage of the rare locations is a Volatility problem, not a baseline problem.
  • Snapshot ≠ alert. The diff is pull-based: snapshots on demand or on schedule, diff against the previous — no agent, no kernel hooks, deployable anywhere Python runs.
  • Noise budget. The baseline expects legitimate software to change occasionally; the diff report ranks findings by how “persistence-shaped” they are (writable paths, script interpreters, EMU-less names), keeping the review list human-sized.
  • Portability over integration. JSON snapshots are file-copyable to the analysis host; SIEM integration is an export problem, not a design constraint.
#!/usr/bin/env python3
"""autorunsdiff.py — persistence baseline snapshot + differ (Windows).
Usage:
python autorunsdiff.py snap baseline.json
python autorunsdiff.py diff baseline.json [--json out.json]
stdlib only (winreg, subprocess, json, os).
"""
import argparse, json, os, subprocess, sys
import winreg
LOCATIONS = {
"run_hklm": ("HKLM", r"SOFTWARE\Microsoft\Windows"
r"\CurrentVersion\Run"),
"run_hkcu": ("HKCU", r"Software\Microsoft\Windows"
r"\CurrentVersion\Run"),
"runonce_hklm": ("HKLM", r"SOFTWARE\Microsoft\Windows"
r"\CurrentVersion\RunOnce"),
"logonscript": ("HKCU", r"Software\Microsoft\Windows"
r"\CurrentVersion\Winlogon"),
}
WATCH_VALUE = "UserInitMprLogonScript"
STARTUP_DIRS = [
os.path.expandvars(r"%APPDATA%\Microsoft\Windows\Start Menu"
r"\Programs\Startup"),
os.path.expandvars(r"%ProgramData%\Microsoft\Windows"
r"\Start Menu\Programs\Startup"),
]
def enum_values(hive, path):
out = {}
root = {"HKLM": winreg.HKEY_LOCAL_MACHINE,
"HKCU": winreg.HKEY_CURRENT_USER}[hive]
try:
k = winreg.OpenKey(root, path)
except OSError:
return out
i = 0
while True:
try:
name, val, _ = winreg.EnumValue(k, i)
out[name] = str(val)
i += 1
except OSError:
break
return out
def tasks():
"""Scheduled task names via schtasks (name-only view)."""
r = subprocess.run(["schtasks", "/query", "/fo", "csv", "/nh"],
capture_output=True, text=True)
names = []
for line in r.stdout.splitlines():
parts = line.split('","')
if parts and len(parts[0]) > 2:
names.append(parts[0].strip('"'))
return names
def services():
"""Service names + binpaths via sc qc parse (state view)."""
r = subprocess.run(["sc", "query", "state=", "all",
"/fo", "csv", "/nh"],
capture_output=True, text=True)
names = []
for line in r.stdout.splitlines():
parts = line.split('","')
if parts and len(parts[0]) > 2:
names.append(parts[0].strip('"'))
return names
def startup_files():
out = []
for d in STARTUP_DIRS:
if os.path.isdir(d):
for f in os.listdir(d):
out.append(os.path.join(d, f))
return out
def capture():
snap = {"values": {}, "tasks": [], "services": [],
"startup": []}
for key, (hive, path) in LOCATIONS.items():
snap["values"][key] = {
f"{n}": v for n, v in enum_values(hive, path).items()}
snap["tasks"] = tasks()
snap["services"] = services()
snap["startup"] = startup_files()
return snap
def diff(old, new):
findings = []
for key in old["values"]:
o, n = old["values"].get(key, {}), \
new["values"].get(key, {})
for name in set(n) - set(o):
findings.append(("regvalue", key, name, n[name]))
for name in set(new["tasks"]) - set(old["tasks"]):
findings.append(("task", "-", name, ""))
for name in set(new["services"]) - set(old["services"]):
findings.append(("service", "-", name, ""))
for p in set(new["startup"]) - set(old["startup"]):
findings.append(("startup", "-", p, ""))
return findings
def suspicious_score(findings):
"""Rank: script-interpreter or writable-path shapes first."""
ranked = []
for kind, loc, name, val in findings:
s = 1
blob = (name + " " + (val or "")).lower()
if any(k in blob for k in ("cmd", "powershell",
"wscript", "cscript",
"rundll32", "mshta")):
s += 3
if any(k in blob for k in ("temp", "appdata",
"programdata", "public")):
s += 2
if "emu-" in blob:
s = 0 # known emulation artifacts
ranked.append((s, kind, loc, name, val))
ranked.sort(reverse=True)
return ranked
def main():
ap = argparse.ArgumentParser()
ap.add_argument("mode", choices=["snap", "diff"])
ap.add_argument("file")
ap.add_argument("--json")
args = ap.parse_args()
if args.mode == "snap":
snap = capture()
json.dump(snap, open(args.file, "w"), indent=2)
n_vals = sum(len(v) for v in snap["values"].values())
print(f"[+] snapshot: {n_vals} values, "
f"{len(snap['tasks'])} tasks, "
f"{len(snap['services'])} services -> {args.file}")
return
old = json.load(open(args.file, encoding="utf-8"))
new = capture()
findings = diff(old, new)
if not findings:
print("[+] no new persistence since baseline")
return
print(f"[!] {len(findings)} new persistence artifacts:")
for s, kind, loc, name, val in suspicious_score(findings):
tag = "EMU" if s == 0 else str(s)
print(f" [{tag:>3}] {kind:<9} {name} {val[:60]}")
if args.json:
json.dump(findings, open(args.json, "w"), indent=2)
if __name__ == "__main__":
main()
  • capture() (lines 55-68) builds the snapshot from four views: registry values per location (the enum_values walker handles missing keys as empty — services hosts vary), task names, service names, and startup-folder files. Names-only for tasks/services keeps snapshots small and diffs meaningful (binpath changes on existing artifacts are a different detector, noted in improvements).
  • diff() (lines 71-84) is set arithmetic per view: new-minus-old across values, tasks, services, files. The output shape (kind, location, name, value) is deliberately flat for JSON export.
  • suspicious_score() (lines 87-100) ranks findings by persistence-shape: interpreter strings (powershell, rundll32, mshta) +3, writable paths +2, and emu- artifacts score 0 — the tool knows the emulation convention, so exercise artifacts sort to the bottom of the review list instead of generating noise. This cross-tool convention (EMU- prefix shared between harness and differ) is the quiet architecture of the whole workflow.
  • The diff output (lines 116-124) prints the ranked list with the EMU tag visible — an exercise’s expected 4 artifacts should appear as [ 0] rows and nothing else; any non-zero row during an exercise window is an unexpected finding by construction.

on the lab VM (elevated for the service tier)

Section titled “on the lab VM (elevated for the service tier)”

$ python autorunsdiff.py snap baseline.json [+] snapshot: 14 values, 212 tasks, 188 services -> baseline.json

$ python persistemu.py —all —i-am-isolated —report emu_manifest.json [ OK] task: SUCCESS: The scheduled task “EMU-Telemetry” has… [ OK] runkey: The operation completed successfully. [ OK] startup: C:\Users\lab…Startup\EMU-check.cmd [ OK] service: SUCCESS: sc create EMU-Svc…

[*] verification pass: [present] task EMU-Telemetry [present] regvalue EMU-Check [present] file C:\Users\lab…\EMU-check.cmd [present] service EMU-Svc

diff must show exactly the 4 EMU artifacts (score 0)

Section titled “diff must show exactly the 4 EMU artifacts (score 0)”

$ python autorunsdiff.py diff baseline.json [!] 4 new persistence artifacts: [ 0] regvalue EMU-Check [ 0] task EMU-Telemetry [ 0] service EMU-Svc [ 0] startup EMU-check.cmd

$ python persistemu.py —cleanup —report emu_manifest.json $ python autorunsdiff.py diff baseline.json [+] no new persistence since baseline

The pair of tools encodes the blue-side workflow this module wants to arm: baseline → emulated change → diff → cleanup, repeated quarterly — and the same diff loop run without the emulation is the estate’s persistence tripwire (monthly per critical server catches most commodity persistence, which overwhelmingly lands in these standard locations). Your own tooling hygiene: both tools are read-only except the harness’s deliberate writes; the manifest and EMU- conventions keep every write reversible and identifiable, and the differ’s zero-score for EMU artifacts means the exercise never pollutes the tripwire’s signal. The detection-engineering takeaway from the telemetry contract table: persistence detections are event-pair detections (4698 + subsequent task execution; 7045 + binary path shape), so validate the pair, not just the creation event — a task created and never executed deserves a different response than one executing at every logon. Wazuh/SIEM mapping of 4698/7045/Sysmon 12-13 is your deployment’s [verify]; Suricata has no layer here. The two mechanisms this module refuses to automate — WMI permanent subscriptions and COM hijacks — stay manual-and-lab-only by design: both have cleanup modes that can wedge a host, and the honest runbook notes them as “revert from snapshot, not uninstall”.

  1. Capture binpaths for services and actions for tasks, not just names — enabling modification detection on existing artifacts.
  2. Add WMI subscription inventory (__EventConsumer/__EventFilter namespaces) as a capture view (read-only — safe to snapshot everywhere).
  3. Fleet mode: remote snapshot via WinRM with per-host baselines in one JSON store.
  4. Integration with campaigndb (M15): log EMU artifacts as exercise records so long-term stats know how many were run.
  5. Emit Sysmon-expected-event checklists per mechanism into the manifest for the blue role’s live-verification checklist.
SignalSourceIDNotes
Task creation (EMU-Telemetry / any)Windows Security4698Pair with subsequent 4688 execution
Run-key value writeSysmonEID 12/13 (TargetObject + Details)Value content is the triage key
Service installWindows Security7045binPath shape is the discriminator
Startup folder dropSysmonEID 11.cmd/.lnk in Startup dirs
Boot-time executionWindows Security / Sysmon4688 + EID 1 at bootSurvival-confirmed persistence
Task deletion / reg delete / sc deleteSysmon / Security4699, EID 12/13Cleanup events close the story
Baseline diff findingsautorunsdifflocal reportStanding tripwire design
WMI subscription creationSysmonEID 19/20/21Lab-only; highest-signal events
COM hijack overwrite (HKCU)SysmonEID 13 on InprocServer32Baseline-vs-current comparison
Wazuh persistence rulesHIDS[verify rule IDs per deployment]Map 4698/7045/12-13 per estate

The adversary’s persistence-design problem is a noise-management problem, and their techniques read as answers to “which mechanism generates the least and blandest telemetry”. Location choice: run keys and scheduled tasks are the loud classics; seasoned operators prefer mechanisms whose creation is not a distinct event — WMI permanent subscriptions (Sysmon 19-21 exist precisely because they were too quiet before), COM hijacks that overwrite HKCU class keys instead of creating new artifacts, UserInitMprLogonScript values that ride existing user logon. Name camouflage: service and task names mimicking legitimate infrastructure (OneDriveSync, GoogleUpdate) exploit the responder’s baseline bias — the differ’s value+path-shape ranking is the counter, because name-legit with a writable-path binPath is exactly the high-score shape. Pair exploitation: creating persistence through mechanisms your estate doesn’t log (if 4698 auditing is off, task creation is invisible to Security logs) — the preflight and telemetry-contract reviews exist to close exactly these gaps before an adversary enjoys them. Survival obfuscation: stagers that re-install persistence after cleanup rather than surviving reboot — countered by the standing tripwire (the re-install is a new diff finding, and repeated findings are a behavioral signal no single snapshot reveals). The emulation program’s counter-move is structural: test every mechanism tier in the telemetry-contract table, close the logging gaps the tests expose, and keep the baseline-fresh — the adversary’s advantage is choosing mechanisms after seeing your estate; your advantage is having already measured every mechanism they might choose.

⚠️ What caught me: the first time I emulated a WMI permanent subscription for a detection exercise, the detection validated beautifully — and then cleanup failed, silently, because I deleted the filter but not the consumer, and the leftover consumer fired on every logon for two weeks while the lab VM “mysteriously” launched notepad each boot. I had created exactly the persistence I was hired to detect, on my own bench. The runbook change was immediate: WMI mechanisms are snapshot-revert-only, the manifest carries every subscription component (filter, consumer, binding), and the differ’s capture now includes the WMI namespaces so a leftover consumer can never hide again. Nothing teaches the weight of persistence like accidentally living with it.

  1. Baseline your lab VM with autorunsdiff.py snap, run the full persistemu.py --all --i-am-isolated cycle, and confirm the diff shows exactly the four EMU artifacts — then clean up and prove absence. This loop is the module in fifteen minutes.
  2. Validate the telemetry contracts live: with the blue hat on, confirm 4698, Sysmon 13, and 7045 arrivals for each mechanism, including the value data fields — then write the one missing rule you find into your detection backlog.
  3. Reboot-survival test: restart the VM before cleanup and confirm which mechanisms survive and which 4688/4698-style events the boot generates — survival behavior is mechanism-specific and worth knowing per technique.
  4. Manual lab-only tier: create a WMI permanent subscription (filter + consumer + binding) on an isolated VM, observe Sysmon 19/20/21, then remove it — and discover firsthand why the runbook says snapshot-revert.
  5. Run the differ’s tripwire role for a week on a busy lab host (daily snapshot+diff) and calibrate the noise floor — the score thresholds you set here are the ones the estate version will inherit.
  • Technical: Rank five persistence mechanisms by telemetry loudness, name the exact event IDs each generates, and explain the adversary’s choice calculus.
  • Technical: What must a persistence emulation harness guarantee beyond creating the artifact — and where does the manifest model fail?
  • Scenario: Your autoruns diff shows one new high-score artifact (powershell in AppData via a service) during an exercise window with no EMU prefix. What is your triage order?
  • Conceptual: Why is “creation event only” detection insufficient for persistence, and what event pairs make it robust?
  • Conceptual: How do COM hijacks and WMI subscriptions differ from run keys in both telemetry and cleanup risk?
  • Deep-dive: Design the fleet persistence tripwire: snapshot cadence, storage, diff ranking, and the alert thresholds that survive legitimate software churn.
  • Practical: Detection validated for task creation but not for task execution. What does the gap mean operationally, and which telemetry closes it?

T1 (ATT&CK Mapping & Emulation Planning) · T2 (Initial Access Emulation) · PE5 (Persistence Mechanisms) · PE2 (Windows Privilege Escalation) · E11 (Defender Detection Logic)