Skip to main content

Registry Persistence Detection

This example shows how to build a detection for Windows registry persistence mechanisms. Attackers modify registry keys to maintain persistence on compromised systems. You can identify suspicious registry modifications by combining baseline anomaly detection with pattern matching for known persistence techniques.

Attack overview​

Registry persistence is a technique where attackers modify specific registry keys to ensure their malware runs automatically:

  1. Registry modification - Attackers write to persistence locations like Run keys
  2. Payload execution - Modified registry entries execute malicious code at startup
  3. Persistence establishment - Malware survives reboots and user logoffs
  4. Defense evasion - Registry modifications can be subtle and hard to detect

You can identify both high-volume registry modifications and modifications to known persistence locations with suspicious characteristics.

Detection strategy​

The detection uses a two-pronged approach to identify persistence mechanisms while minimizing false positives. Hamelin establishes baselines for normal registry activity per host, then identifies modifications to known persistence locations. The query combines multiple indicators to calculate threat severity and excludes common legitimate registry operations.

The detection focuses on registry event codes 12, 13, and 14 from Sysmon, which capture registry key creation, value modification, and key deletion.

Complete detection query​

This detection query combines baseline analysis with pattern matching to identify suspicious registry persistence mechanisms. The query establishes normal registry operation baselines for each host, calculates statistical thresholds, then identifies anomalous activity and suspicious patterns in real-time registry modifications:

// Detection Name: Windows Registry Persistence (Tuned)
//
// Detection Logic:
// - Uses simba.events source
// - Extracts and casts event_data fields: CommandLine, Details, Image, TargetObject
// - Identifies anomalous patterns via regexp_like and filters legitimate cases
// - Calculates risk score from base and anomaly components
// - Projects ECS-compliant fields and rule metadata (name, mitre, severity)
// - TUNED: Added exclusions for common false positives
//

// Baseline for registry operations per host
DEF baseline_reg = FROM simba.sysmon_events
| WHERE event.code IN ["12", "13", "14"]
| SET computer_name = host.name AS string
| AGG reg_ops = count()
BY timestamp = timestamp@m, computer_name
| AGG avg = avg(reg_ops),
stddev = stddev(reg_ops)
BY computer_name;

// Main detection
FROM simba.sysmon_events
| WHERE event.code IN ["12", "13", "14"]
| SET computer_name = host.name AS string
| SET reg_key = coalesce(winlog.parsed_event_data.target_object AS string, "") AS string
| SET reg_value = coalesce(winlog.parsed_event_data.details AS string, "") AS string
| SET process_name = coalesce(winlog.parsed_event_data.image AS string, "") AS string
| SET process_cmd = coalesce(winlog.parsed_event_data.command_line AS string, "") AS string

// Known good exclusions - EXPANDED TO REDUCE FALSE POSITIVES
| WHERE NOT (
// Windows Installer
regexp_like(lower(reg_key),
'(?i).*\\microsoft\\windows\\currentversion\\installer\\.*') OR
// Microsoft Office Updates
regexp_like(lower(reg_key),
'(?i).*\\microsoft\\office\\.*updates\\.*') OR
// Windows Defender
regexp_like(lower(reg_key),
'(?i).*\\microsoft\\windows defender\\.*') OR
// DHCP/Network Configuration by svchost
(regexp_like(lower(reg_key),
'(?i).*\\services\\tcpip\\parameters\\interfaces\\.*dhcp.*') AND
regexp_like(lower(process_name), '(?i).*\\svchost\\.exe$')) OR
// Windows Inventory/Telemetry
regexp_like(lower(reg_key),
'(?i).*\\registry\\a\\.*\\root\\inventory.*') OR
// Windows Compatibility Telemetry
(regexp_like(lower(reg_key),
'(?i).*inventory.*writeperms.*') AND
regexp_like(lower(process_name), '(?i).*\\compattelrunner\\.exe$')) OR
// LogonUI Session Data
(regexp_like(lower(reg_key),
'(?i).*\\authentication\\logonui\\sessiondata\\.*') AND
regexp_like(lower(process_name), '(?i).*\\logonui\\.exe$')) OR
// WMI/ETW AutoLogger Configuration
(regexp_like(lower(reg_key),
'(?i).*\\control\\wmi\\autologger\\.*') AND
regexp_like(lower(process_name), '(?i).*\\svchost\\.exe$')) OR
// Windows Update
regexp_like(lower(reg_key),
'(?i).*\\microsoft\\windows\\currentversion\\windowsupdate\\.*') OR
// Group Policy Cache
regexp_like(lower(reg_key),
'(?i).*\\microsoft\\windows\\currentversion\\group policy\\.*cache.*') OR
// Edge/Chrome Updates
regexp_like(lower(reg_key),
'(?i).*\\(microsoft|google)\\update\\.*') OR
// VSCode Inventory (from earlier analysis)
regexp_like(lower(reg_key),
'(?i).*codesetup-stable.*programid.*')
)

// Suspicious registry locations
| SET is_run_key = regexp_like(lower(reg_key),
'(?i).*(\\\\run\\\\|\\\\runonce\\\\|\\\\runonceex\\\\|\\\\explorer\\\\run).*') AS boolean
| SET is_startup_key = regexp_like(lower(reg_key),
'(?i).*(\\\\windows\\\\currentversion\\\\shell\\\\|\\\\startup\\\\|\\\\load\\\\|\\\\userinit\\\\|\\\\shell folders\\\\).*') AS boolean
| SET is_service_key = regexp_like(lower(reg_key),
'(?i).*(\\\\services\\\\[^\\\\]+\\\\(imagepath|servicedll)|\\\\currentcontrolset\\\\services\\\\[^\\\\]+$)') AS boolean
| SET is_logon_key = regexp_like(lower(reg_key),
'(?i).*(\\\\winlogon\\\\(shell|userinit)|\\\\policies\\\\system\\\\|\\\\groupolicy\\\\scripts\\\\).*') AS boolean
| SET is_extension_key = regexp_like(lower(reg_key),
'(?i).*(\\\\command\\\\|\\\\shell\\\\open\\\\command|\\\\handlers\\\\|\\\\fileexts\\\\.*\\\\openwithprogids).*') AS boolean

// Suspicious values/data
| SET sus_value_data = regexp_like(lower(reg_value),
'(?i).*(powershell|cmd|wscript|cscript|rundll32|regsvr32|mshta|certutil|bitsadmin)\\.exe.*') AS boolean
| SET sus_value_location = regexp_like(lower(reg_value),
'(?i).*(\\\\temp\\\\|\\\\appdata\\\\|\\\\programdata\\\\public\\\\|\\\\downloads\\\\|\\\\users\\\\all users\\\\).*') AS boolean
| SET sus_encoded_data = regexp_like(lower(reg_value),
'(?i).*(base64|encoded|encrypt|\\-enc\\s|\\-e\\s|iex\\s|downloadstring).*') AS boolean

// Suspicious process context
| SET sus_process = regexp_like(lower(process_name),
'(?i).*(powershell|cmd|wscript|cscript|rundll32|regsvr32|mshta|certutil|bitsadmin)\\.exe.*') AS boolean
| SET sus_cmdline = regexp_like(lower(process_cmd),
'(?i).*(hidden|verysilent|/q|/quiet|/s|/silent|-w hidden|-windowstyle hidden|-enc|-encode).*') AS boolean

// Additional filtering for known legitimate patterns
| WHERE NOT (
// Filter out IP addresses and GUIDs as registry values (common for network config)
(regexp_like(reg_value, '^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$') AND
NOT sus_value_data) OR
// Filter out DWORD values that are just numbers
(regexp_like(reg_value, '^DWORD \\(0x[0-9a-fA-F]+\\)$') AND
NOT sus_value_data)
)

// Calculate risk score
| SET risk_score = (
if(is_run_key, 30, 0) + // Increased weight for run keys
if(is_startup_key, 30, 0) + // Increased weight for startup keys
if(is_service_key, 25, 0) + // Slightly increased for services
if(is_logon_key, 25, 0) + // Slightly increased for logon
if(is_extension_key, 15, 0) + // Reduced weight for extensions
if(sus_value_data, 40, 0) + // Increased weight for suspicious data
if(sus_value_location, 25, 0) + // Increased weight for suspicious locations
if(sus_encoded_data, 40, 0) + // Increased weight for encoded data
if(sus_process, 25, 0) + // Increased weight for suspicious process
if(sus_cmdline, 25, 0) // Increased weight for suspicious cmdline
)
| SET registry = {key: reg_key, value: reg_value}

// Aggregate by minute
| AGG minute_count = count(),
related.registry = array_agg(registry),
process_names = array_agg(process_name),
command_lines = array_agg(process_cmd),
event_ids = array_agg(event.code),
users = array_agg(coalesce(winlog.user.name AS string, '') AS string),
risk_factors.has_run_keys = any(is_run_key),
risk_factors.has_startup_keys = any(is_startup_key),
risk_factors.has_service_keys = any(is_service_key),
risk_factors.has_logon_keys = any(is_logon_key),
risk_factors.has_extension_keys = any(is_extension_key),
risk_factors.has_sus_data = any(sus_value_data),
risk_factors.has_sus_location = any(sus_value_location),
risk_factors.has_encoded_data = any(sus_encoded_data),
risk_factors.has_sus_process = any(sus_process),
risk_factors.has_sus_cmdline = any(sus_cmdline),
max_risk = max(risk_score)
BY timestamp = timestamp@m, computer_name

// Join with baseline
| JOIN baseline_reg ON computer_name == baseline_reg.computer_name
| SET threshold = baseline_reg.avg + 3 * baseline_reg.stddev

// Alert criteria - ADJUSTED THRESHOLDS
| SET suspicious_condition_count = (
if(risk_factors.has_run_keys, 1, 0) +
if(risk_factors.has_startup_keys, 1, 0) +
if(risk_factors.has_service_keys, 1, 0) +
if(risk_factors.has_logon_keys, 1, 0) +
if(risk_factors.has_extension_keys, 1, 0) +
if(risk_factors.has_sus_data, 2, 0) + // Weight suspicious data more
if(risk_factors.has_sus_location, 1, 0) +
if(risk_factors.has_encoded_data, 2, 0) + // Weight encoded data more
if(risk_factors.has_sus_process, 1, 0) +
if(risk_factors.has_sus_cmdline, 1, 0)
)
// Increased thresholds to reduce noise
| WHERE minute_count > threshold OR suspicious_condition_count >= 3 OR max_risk >= 70

// ECS required single-value fields
| SET primary_process_name = array_distinct(process_names)[0]
| SET primary_command_line = array_distinct(command_lines)[0]
| SET primary_event_code = array_distinct(event_ids)[0]
| SET primary_user = array_distinct(users)[0]
| SET registry = related.registry[0]
// Create simple message and structured labels
| SET message = "Windows Registry Persistence Signal"

// Create text values for labels
| SET run_keys_text = if(risk_factors.has_run_keys, "Yes", "No") AS string
| SET startup_keys_text = if(risk_factors.has_startup_keys, "Yes", "No") AS string
| SET service_keys_text = if(risk_factors.has_service_keys, "Yes", "No") AS string
| SET logon_keys_text = if(risk_factors.has_logon_keys, "Yes", "No") AS string
| SET extension_keys_text = if(risk_factors.has_extension_keys, "Yes", "No") AS string
| SET sus_data_text = if(risk_factors.has_sus_data, "Yes", "No") AS string
| SET sus_location_text = if(risk_factors.has_sus_location, "Yes", "No") AS string
| SET encoded_data_text = if(risk_factors.has_encoded_data, "Yes", "No") AS string
| SET sus_process_text = if(risk_factors.has_sus_process, "Yes", "No") AS string
| SET sus_cmdline_text = if(risk_factors.has_sus_cmdline, "Yes", "No") AS string
| SET minute_count_text = minute_count AS string
| SET threshold_text = threshold AS string
| SET condition_count_text = suspicious_condition_count AS string

// Create structured labels map
| SET labels = map(
"Severity": if(max_risk >= 80, 'HIGH',
if(max_risk >= 60, 'MEDIUM', 'LOW')),
"Host": computer_name,
"User": primary_user,
"Registry Key": registry.key,
"Registry Value": registry.value,
"Process": primary_process_name,
"Command Line": primary_command_line,
"Event Count": minute_count_text,
"Anomaly Threshold": threshold_text,
"Run Keys": run_keys_text,
"Startup Keys": startup_keys_text,
"Service Keys": service_keys_text,
"Logon Keys": logon_keys_text,
"Extension Keys": extension_keys_text,
"Suspicious Data": sus_data_text,
"Suspicious Location": sus_location_text,
"Encoded Data": encoded_data_text,
"Suspicious Process": sus_process_text,
"Suspicious Command Line": sus_cmdline_text,
"Suspicious Condition Count": condition_count_text
)

// Create detailed description for rule object
| SET detailed_description =
"Detection of suspicious registry persistence mechanisms" +
if(computer_name != "", " on host: " + computer_name, "") +
if(registry.key is not NULL, ". Registry key: " + registry.key, "") +
if(registry.value is not NULL, ". Registry value: " + registry.value, "")

// Final output
| SELECT
timestamp,
message,
labels,
host.name = computer_name,
event.count = minute_count,
anomaly_threshold = threshold,
registry,
process.name = primary_process_name,
related.process.name = array_distinct(process_names),
process.command_line = primary_command_line,
related.process.command_line = array_distinct(command_lines),
event.code = primary_event_code,
related.event.code = array_distinct(event_ids),
event.start = event.start,
event.end = event.end,
user.name = primary_user,
related.user.name = array_distinct(users),
condition_count = suspicious_condition_count,
risk_factors,
event.risk_score = max_risk,
rule = {
name: 'Windows Registry Persistence',
description: detailed_description,
mitre_tactics: [
'Persistence',
'Privilege Escalation',
'Defense Evasion'
],
mitre_techniques: [
'T1547.001',
'T1546.001',
'T1112',
'T1037'
],
severity: if(max_risk >= 80, 'high',
if(max_risk >= 60, 'medium', 'low')),
risk_factors: [ // Keep descriptions in the rule object
if(risk_factors.has_run_keys, "Contains Run keys for persistence", null),
if(risk_factors.has_startup_keys, "Contains Startup keys for persistence", null),
if(risk_factors.has_service_keys, "Contains Service keys for persistence", null),
if(risk_factors.has_logon_keys, "Contains Logon keys for persistence", null),
if(risk_factors.has_extension_keys, "Contains Extension keys for persistence", null),
if(risk_factors.has_sus_data, "Contains suspicious data in registry values", null),
if(risk_factors.has_sus_location, "Contains suspicious file locations in registry values", null),
if(risk_factors.has_encoded_data, "Contains encoded or obfuscated data in registry values", null),
if(risk_factors.has_sus_process, "Registry modifications by suspicious process", null),
if(risk_factors.has_sus_cmdline, "Registry modifications with suspicious command line", null),
if(minute_count > threshold, "Anomalous number of registry operations detected", null)
]
}
| SORT timestamp DESC

Query breakdown​

This detection demonstrates several advanced Hamelin techniques working together to identify registry persistence while minimizing false positives. Let's examine each component:

Baseline establishment​

The detection starts by calculating normal registry activity baselines for each host. This lets you identify unusual spikes in registry operations that might indicate malicious activity:

DEF baseline_reg = FROM simba.sysmon_events
| WHERE event.code IN ["12", "13", "14"]
| AGG reg_ops = count()
BY timestamp = timestamp@m, computer_name
| AGG avg = avg(reg_ops),
stddev = stddev(reg_ops)
BY computer_name;

This calculates the average and standard deviation of registry operations per minute for each host. Hamelin uses these statistics to create baselines for anomaly detection.

False positive filtering​

The detection includes extensive filtering to exclude common legitimate registry operations. Without this filtering, Windows Update, Office updates, and other normal system processes would generate false positives:

| WHERE NOT (
// Windows Installer
regexp_like(lower(reg_key),
'(?i).*\\microsoft\\windows\\currentversion\\installer\\.*') OR
// Microsoft Office Updates
regexp_like(lower(reg_key),
'(?i).*\\microsoft\\office\\.*updates\\.*') OR
// Windows Defender
regexp_like(lower(reg_key),
'(?i).*\\microsoft\\windows defender\\.*')
// ... additional exclusions
)

These patterns filter out registry modifications by Windows Update, Office updates, Windows Defender, and other legitimate system processes.

Persistence location detection​

The detection identifies modifications to known registry persistence locations using boolean flags. Each flag captures a different persistence mechanism that attackers commonly exploit:

| SET is_run_key = regexp_like(lower(reg_key),
'(?i).*(\\\\run\\\\|\\\\runonce\\\\|\\\\runonceex\\\\|\\\\explorer\\\\run).*')
| SET is_startup_key = regexp_like(lower(reg_key),
'(?i).*(\\\\windows\\\\currentversion\\\\shell\\\\|\\\\startup\\\\|\\\\load\\\\).*')
| SET is_service_key = regexp_like(lower(reg_key),
'(?i).*(\\\\services\\\\[^\\\\]+\\\\(imagepath|servicedll)).*')

These patterns identify the most common registry-based persistence mechanisms that attackers use to maintain access to compromised systems.

Risk scoring system​

The detection calculates a risk score based on multiple factors. Hamelin assigns higher weights to more suspicious indicators like encoded data and suspicious executables:

| SET risk_score = (
if(is_run_key, 30, 0) + // High weight for run keys
if(sus_value_data, 40, 0) + // Very high weight for suspicious executables
if(sus_encoded_data, 40, 0) + // Very high weight for encoded data
if(sus_process, 25, 0) // Moderate weight for suspicious processes
)

This scoring system lets you prioritize the most dangerous types of registry modifications and focus analyst attention on the highest-risk events.

Anomaly detection integration​

The detection combines pattern matching with statistical anomaly detection. Hamelin establishes thresholds based on historical behavior for each host:

| SET threshold = baseline_reg.avg + 3 * baseline_reg.stddev
| WHERE minute_count > threshold OR suspicious_condition_count >= 3 OR max_risk >= 70

This triggers alerts when registry activity exceeds normal baselines or when multiple suspicious conditions are present, ensuring you catch both volume-based and pattern-based threats.

Advanced techniques demonstrated​

This detection showcases several techniques that make Hamelin effective for complex threat detection:

Baseline anomaly detection establishes statistical baselines for normal behavior and identifies deviations that might indicate malicious activity.

Multi-factor risk scoring combines multiple indicators with weighted scoring to prioritize the most dangerous threats.

Comprehensive filtering removes known false positives while preserving detection of actual threats through extensive exclusion patterns.

Boolean logic aggregation uses array functions like any() to aggregate boolean flags across multiple events in time windows.

Dynamic thresholding adjusts alert criteria based on both statistical baselines and fixed thresholds for suspicious conditions.

Structured output creates both human-readable labels and machine-readable risk factors for different analysis workflows.

Detection tuning​

You can adapt this detection for different environments:

Adjust baseline periods by modifying the time aggregation intervals used for calculating statistical baselines.

Update exclusion patterns by adding environment-specific legitimate registry operations to the filtering logic.

Modify risk weights by adjusting the scoring values based on observed attack patterns in your environment.

Tune alert thresholds by changing the suspicious condition count and risk score thresholds based on acceptable false positive rates.

The modular structure makes it easy to test individual components and adjust scoring weights without affecting the overall detection logic.