Registry Run Keys
Overview
The Run and RunOnce registry keys are the simplest persistence mechanism Windows exposes. Any
value under them is executed by the session manager (userinit.exe) during logon. The per-user
path under HKCU requires no elevated privileges; the machine-wide path under HKLM requires
administrator rights but survives across all users.
The technique is decades old and comprehensively detected. It remains relevant because it
requires zero exploitation — a low-integrity process can write to HKCU\...\Run — and because
detection gaps still exist on endpoints without Sysmon or a baseline inventory.
RunOnce values are deleted after execution. They are slightly harder to detect retroactively
(the value is gone by the time the analyst looks) but provide no persistence across subsequent
logons without re-writing the key.
The call chain
- 1RegOpenKeyExA(HKCU\Software\Microsoft\Windows\CurrentVersion\Run)Per-user key — no admin rights required. Survives logoff/logon cycles.
- 2RegSetValueExA(name, payload_path)Write the name/value pair pointing at the payload binary or command line.
- 3(trigger) user logs onThe session manager runs all values under the applicable Run keys before the desktop appears.
Reference implementation
#include <windows.h>
#define RUN_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
BOOL install_run_key(const char *name, const char *payload_path) {
HKEY hk;
LSTATUS rc = RegOpenKeyExA(HKEY_CURRENT_USER, RUN_KEY,
0, KEY_SET_VALUE, &hk);
if (rc != ERROR_SUCCESS) return FALSE;
rc = RegSetValueExA(hk, name, 0, REG_SZ,
(const BYTE *)payload_path,
(DWORD)strlen(payload_path) + 1);
RegCloseKey(hk);
return rc == ERROR_SUCCESS;
}
BOOL remove_run_key(const char *name) {
HKEY hk;
RegOpenKeyExA(HKEY_CURRENT_USER, RUN_KEY, 0, KEY_SET_VALUE, &hk);
LSTATUS rc = RegDeleteValueA(hk, name);
RegCloseKey(hk);
return rc == ERROR_SUCCESS;
}
// equivalent one-liner in PowerShell:
// New-ItemProperty -Path "HKCU:\...\Run" -Name "Update" -Value "C:\payload.exe"#include <windows.h>
#define RUN_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
BOOL install_run_key(const char *name, const char *payload_path) {
HKEY hk;
LSTATUS rc = RegOpenKeyExA(HKEY_CURRENT_USER, RUN_KEY,
0, KEY_SET_VALUE, &hk);
if (rc != ERROR_SUCCESS) return FALSE;
rc = RegSetValueExA(hk, name, 0, REG_SZ,
(const BYTE *)payload_path,
(DWORD)strlen(payload_path) + 1);
RegCloseKey(hk);
return rc == ERROR_SUCCESS;
}
BOOL remove_run_key(const char *name) {
HKEY hk;
RegOpenKeyExA(HKEY_CURRENT_USER, RUN_KEY, 0, KEY_SET_VALUE, &hk);
LSTATUS rc = RegDeleteValueA(hk, name);
RegCloseKey(hk);
return rc == ERROR_SUCCESS;
}
// equivalent one-liner in PowerShell:
// New-ItemProperty -Path "HKCU:\...\Run" -Name "Update" -Value "C:\payload.exe"Run key locations
| Key | Scope | Requires |
|---|---|---|
HKCU\Software\Microsoft\Windows\CurrentVersion\Run |
Current user | Nothing |
HKLM\Software\Microsoft\Windows\CurrentVersion\Run |
All users | Admin |
HKCU\...\RunOnce |
Current user, once | Nothing |
HKLM\...\RunOnce |
All users, once | Admin |
HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run |
All users (32-bit) | Admin |
Variations
Indirect execution — write cmd.exe /c start /b payload.exe or use wscript.exe to run
a VBScript dropper. The intermediate interpreter is a signed binary, which can fool path-based
allow lists.
Environment variable paths — %APPDATA%\update.exe instead of an absolute path. Harder to
diff against a clean baseline, but the value still appears in the key.
Alternate value types — REG_EXPAND_SZ with %COMSPEC% /c ... expands environment
variables at logon time, slightly obfuscating the payload command.
Detection
Persistence via Run keys is an inventory problem. Enumerate all Run key values at logon and diff against an approved-software baseline. The rate of change on a managed endpoint from legitimate software is very low; any new value without a matching installer event is suspicious.