home / malware / runkey-beacon-loader

Masqueraded .NET loader with Run-key persistence and a fixed-interval beacon

2026-11-04 · self-authored specimen · Windows 11 23H2, isolated range

At a glance

Question
What does a minimal, user-land loader look like in host and network telemetry — and which of those signals survive a rebuild that changes every hash?
Specimen
wm-loader, a 38 KB unsigned .NET 8 assembly written for this study. Copies itself, persists without administrator rights, beacons once, then idles. Its command handler accepts three inert verbs and can run nothing else.
Platform
Windows 11 23H2 (22631), x64, virtualised guest with a snapshot taken before the first run.
Tooling
dnSpy for static analysis, Sysmon 15.15 for host telemetry, Zeek 7.0 on the range sensor, CyberChef to decode the config blob, Sigma for the rule.
Environment
Default-deny VLAN with no route out; a stub command server on 10.20.30.40. No real credentials, no real data, reverted to snapshot after each run. See the isolated research range.
Status
COMPLETE — two runs, one Windows build, one sensor.
Artifacts
None. The specimen is withheld under the sanitization policy; what is published is the telemetry, the analysis and the rule.

Objective and scope

Loaders are the part of an intrusion that a SOC analyst meets first: something small, unsigned and unremarkable runs once on a workstation, arranges to run again, and then talks to the internet. This study builds the smallest specimen that does those three things and asks what a defender can see.

In scope: the persistence mechanism, the masquerading choice, the beacon's timing, and the TLS metadata left behind. Out of scope: evasion against EDR, privilege escalation, lateral movement, and any kind of code injection. The specimen has no capability in those directions, so any conclusion about them would be about a different piece of software. Deliberately not tested: behaviour when the C2 is unreachable, and behaviour with a non-zero jitter setting.

Laboratory environment

written before the run

Three hosts, one flat monitored VLAN with no gateway. The guest cannot reach anything except the sensor's span port and the stub server, and the stub server can reach nothing outside the range.

The blind spots matter as much as the coverage: nothing sees the loader's memory after the config is decrypted, PowerShell script-block logging is irrelevant because the specimen is .NET rather than a script, and no EDR product was in the path, so none of the conclusions below should be read as a claim about EDR detection.

HostRoleTelemetry
lab-win11Analyst-controlled guest, local administrator for the runSysmon 15.15: process create (1), file create (11), registry set (13), network connect (3), DNS (22). Windows Security 4688 with command-line auditing. No Defender real-time telemetry: protection is disabled in the range by design.
lab-sensorZeek 7.0 on the VLAN span portconn, http, ssl, dns. No packet capture beyond headers, so no payload reconstruction inside TLS.
lab-c2Stub command server, 10.20.30.40:8443Its own request log, used only to confirm the beacon arrived. No second stage exists on it.

Methodology

  1. The specimen was written and built outside the range, then transferred in over a read-only share so no build tooling exists on the guest. The source was not analysed until after the first run, so the host telemetry was interpreted without knowing the intended order of operations.
  2. Snapshot revert, then a single execution from C:\Users\lab\Desktop\. The guest was left idle for 30 minutes with no other user activity, so any beacon in the window belongs to the specimen.
  3. Host events were exported as XML and network logs collected from the sensor. The run was repeated from the same snapshot: the second run reproduced the same sequence at the same interval, which is why the timing claim below is labelled corroborated rather than observed once.
  4. Static analysis followed: strings, the base64 blob, and the decompiled Main in dnSpy. Where static analysis is the only source for a claim, the evidence block says so.

Evidence and observations

one record per claim

Timestamps are UTC on 2026-11-03. Records are reproduced as captured and sanitised: paths and addresses are the range's own, and no hash is published because the specimen is withheld.

EV-001The specimen runs once from the desktop, unsigned, with no command-line argumentsFACT
Sysmon 1  2026-11-03 09:41:07.214
Image        C:\Users\lab\Desktop\wm-loader.exe
CommandLine  "C:\Users\lab\Desktop\wm-loader.exe"
ParentImage  C:\Windows\explorer.exe
User         LAB\lab
Company      -
Signed       false
Integrity    Medium

source sysmon operational log, exported XML · sha256 not published (specimen withheld)

Interpretation. A double-click from Explorer, not a service or a scheduled task. Nothing in this record distinguishes the specimen from any unsigned utility a user might run, which is the point: the first event of the chain carries almost no signal on its own.

EV-002It writes a copy of itself into AppData under a Windows Update service nameFACT
Sysmon 11  2026-11-03 09:41:07.902
Image         C:\Users\lab\Desktop\wm-loader.exe
TargetFilename C:\Users\lab\AppData\Roaming\WinUpdate\wuauserv.exe
CreationUtcTime 2026-11-03 09:41:07.887
Contents      copy of the running image

source sysmon operational log · sha256 not published

Interpretation. wuauserv is the Windows Update service, and the real one never exists as an image: it runs inside svchost.exe from System32. An executable with that name under %APPDATA% is therefore anomalous by construction, but the record proves only that the file was created. It does not prove the copy is later executed — EV-003 and EV-004 are needed for that.

EV-003A logon-run value is created that points at the copy, with no administrator rightsFACT
Sysmon 13  2026-11-03 09:41:07.918
Image        C:\Users\lab\Desktop\wm-loader.exe
EventType    SetValue
TargetObject HKCU\Software\Microsoft\Windows\CurrentVersion\Run\WinUpdate
Details      "C:\Users\lab\AppData\Roaming\WinUpdate\wuauserv.exe"
User         LAB\lab

source sysmon operational log · sha256 not published

Interpretation. HKCU is writable by its owner, so persistence needs no elevation and no service creation. The value name WinUpdate and the target path agree with EV-002, which is what makes the pair corroborate persistence rather than leave it as two unrelated writes.

EV-004The guest opens one HTTPS connection to the stub server and posts a 176-byte bodyCORROBORATED
Sysmon 3   2026-11-03 09:41:09.006
Image       C:\Users\lab\AppData\Roaming\WinUpdate\wuauserv.exe
Destination 10.20.30.40:8443   Protocol tcp   Initiated true

zeek http.log
ts         2026-11-03T09:41:09.118
method     POST
host       10.20.30.40
uri        /api/v2/beacon
user_agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
request_body_len 176
status_code 200
resp_mime_types application/json

source sysmon operational log; zeek http.log on lab-sensor · sha256 not published

Interpretation. Two independent records agree that the copy, not the desktop binary, made the connection, which is what ties the file write to the execution. The user agent is ordinary Chrome; taken alone it is worthless as a signal, and it is included here to show why the rule below is built on the registry write instead.

EV-005TLS is a self-signed certificate with no SNI, and the client fingerprint is stable across both runsFACT
zeek ssl.log
server_name  -
subject      CN=c2.lab.internal
issuer       CN=c2.lab.internal
validation_status self signed certificate
ja3          6f0d8e1a9c3b4f27d05a8e6b1c9347d2
ja3s         2d1f7c0b8e4a9635cf17b0d2e6a85413
version      TLSv12   cipher TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

source zeek ssl.log · sha256 not published

Interpretation. Self-signed is expected on a lab stub and says nothing about the specimen's own validation: static analysis (EV-007) shows the client accepts any certificate, so this handshake would have completed against a spoofed server too. The JA3 value identifies the .NET HTTP stack rather than the specimen, so it is a weak, version-dependent pivot, not a detection.

EV-006Twenty beacons in thirty minutes: a fixed 90-second interval with no jitterCORROBORATED
$ zeek-cut ts uri < http.log | grep -c '/api/v2/beacon'
20

gaps between consecutive requests (seconds)
90.02  89.97  90.11  89.94  90.03  90.06  89.99  90.08  ...

source zeek http.log, 30-minute idle window, repeated in run 2 with the same result

Interpretation. The interval is exact to within one scheduler tick, which is unusual: most commodity implants jitter. That precision makes a periodicity rule viable against this specimen, and it is also a warning — a rebuild with jitter set to anything above zero would defeat it, which is why the rule below does not depend on timing.

EV-007Static analysis: the config blob decrypts to a URL, a sleep value and three permitted verbsFACT
dnSpy, Main():
  DecodeConfig()  base64 -> AES-256-CBC, key = SHA256("WM-Lab-2026"), IV embedded
  Persist()       File.Copy(self) then Registry.CurrentUser Run\WinUpdate
  Loop()          POST config.url, sleep config.sleep, dispatch cmd

decoded config (inert text):
{"url":"https://10.20.30.40:8443/api/v2/beacon",
 "sleep":90,"jitter":0,"max_retry":3,
 "commands":["noop","sleep","exit"]}

ServerCertificateCustomValidationCallback = (_, _, _, _) => true
No process creation, no download, no shell, no injection API in the assembly.

source dnSpy decompilation of the specimen, read after the run

Interpretation. The three-verb allow-list bounds what this specimen can do: it can idle, change its own sleep interval or exit. That is a property of this build, not of loaders generally, and the findings table says so rather than generalising from it. Certificate validation is disabled, so no network control that depends on a trusted certificate would have stopped the beacon.

Findings

FindingEvidenceConfidence
User-level persistence via HKCU\...\Run needs no elevation, and the write happens within a second of first execution.EV-002, EV-003CORROBORATED
The persisted copy is named after a Windows Update component and lives outside System32, where that name cannot legitimately exist.EV-002INFERENCE
The beacon interval is fixed at 90 seconds with zero jitter in the default build.EV-004, EV-006CORROBORATED
TLS certificate validation is disabled and the certificate is self-signed, so certificate trust is not a control here.EV-005, EV-007FACT
This build cannot spawn a process, download a file or inject into another process, so it says nothing about what a real loader does next; the claim stops at this specimen.EV-007FACT
Command and control traffic is HTTP over TLS to a hard-coded IP with an ordinary browser user agent; there is no domain to block.EV-004, EV-007FACT

Detection

The durable signal in this chain is the registry write: it survives a rebuild that changes the binary, the path, the URL and the interval. The rule targets the shape rather than the name, because a rebuild would rename the value and the copy.

title: Logon Run Key Points At A User-Writable Directory
id: 0b1c4f2a-7d3e-4a91-8c55-6e2f9d1a4b70
status: experimental
description: A logon-run value whose executable lives under AppData or ProgramData, the usual per-user persistence path for a loader.
references:
    - https://attack.mitre.org/techniques/T1547/001/
author: WMouton
date: 2026/11/04
tags:
    - attack.persistence
    - attack.t1547.001
logsource:
    category: registry_set
    product: windows
detection:
    selection_key:
        TargetObject|endswith:
            - '\Software\Microsoft\Windows\CurrentVersion\Run'
            - '\Software\Microsoft\Windows\CurrentVersion\Run\*'
            - '\Software\Microsoft\Windows\CurrentVersion\RunOnce\*'
    selection_path:
        Details|contains:
            - '\AppData\Roaming\'
            - '\AppData\Local\'
            - '\ProgramData\'
    condition: selection_key and selection_path
falsepositives:
    - Per-user installers and updaters that register themselves at logon (Teams, Slack, Zoom, Spotify)
    - Sign-in helpers and peripheral utilities installed per user
level: medium

The second, cheaper hunt is on the masquerade: a process whose image name is wuauserv.exe but whose path is not C:\Windows\System32. The same query works for any service name that never exists as an image, because those services all run inside svchost.exe. In Sysmon 1 terms, Image|endswith: '\wuauserv.exe' with a path under a user profile or ProgramData is worth an alert in its own right, and it needs no threat intelligence or hash list.

The table below is where those signals stop, which is the part a rule review should read first. If this rule graduates to the published detection set, it belongs there rather than duplicated here; this page is the reasoning behind it, not a second copy of the rule library.

SignalData sourceWhat it cannot see
Run-key value pointing into a user-writable directorySysmon 13, or the registry auditing equivalent in EDRPersistence through scheduled tasks, services, WMI subscriptions or startup folders; a loader that installs no persistence at all
Service-named image running outside System32Sysmon 1 / Security 4688A copy given an unremarkable, non-system name; every one of those is a FALSE negative that a rename produces
Fixed-interval beacon to one hostZeek conn or a proxy log, over a long windowJitter, sleeping hours and low-and-slow intervals; anything under the volume threshold of the query

Mitigation

what removes the behaviour, and what only raises the cost

Removing the behaviour. Application control (WDAC or AppLocker) that denies execution from %APPDATA%, %LOCALAPPDATA% and ProgramData for standard users stops the persisted copy from starting at all, which is a stronger position than alerting after it has. The rule above is the fallback for hosts where that policy is not yet in place.

Raising the cost. Egress filtering that denies direct-to-IP HTTPS from workstations gives a hard-coded address nowhere to go. Under that control this specimen would still run and persist; it would stop beaconing, and the persistence would be left for the host-side rule to find rather than for the network to see.

Validation and reproducibility

Everything below was run against the published records and the range logs, and re-run after the second execution. The counts are the check: if a rebuild changes the interval or the persistence method, they stop matching.

# sensor: beacon count and spacing over the 30-minute window
$ zeek-cut ts uri < http.log | grep -c '/api/v2/beacon'
20
$ zeek-cut ts < http.log | grep -B1 '/api/v2/beacon' | awk 'p{print $1-p} {p=$1}' | sort -u | head -3
89.94
90.02
90.11

# guest: the persistence write is present exactly once
PS> Get-WinEvent -LogName 'Microsoft-Windows-Sysmon/Operational' |
      Where-Object { $_.Id -eq 13 -and $_.Message -match 'WinUpdate' } |
      Measure-Object | Select-Object -ExpandProperty Count
1

# guest: the persisted copy exists and is unsigned
PS> Get-AuthenticodeSignature "$env:APPDATA\WinUpdate\wuauserv.exe" |
      Select-Object -ExpandProperty Status
NotSigned

The drift check is the count: a rebuild with jitter above zero keeps roughly the same number of connections but loses the constant spacing, and a build that switched to a scheduled task produces one registry event fewer and one task-creation event more. Both show up in the same three commands, which is what makes them usable on a specimen nobody can download.

Limitations and open questions

  • OPEN The specimen supports a jitter setting and has never been run with it above zero. Whether a jittered rebuild defeats the periodic query is untested, and the rule above is written not to depend on the answer.
  • DISPROVEN The first hypothesis was DNS tunnelling, because the IP was hard-coded and that usually implies name resolution somewhere. Zeek dns.log records four queries in the window, all A/AAAA for the range resolver and the update host, no TXT, no NULL. The hypothesis was wrong and is kept here so the conclusion is not read as "no covert channel was tested for".
  • BENIGN A per-user Teams install on the same guest writes an HKCU\...\Run value pointing at %LOCALAPPDATA%\Microsoft\Teams\current\Teams.exe and triggers the rule above. It is the first false positive and it cannot be resolved by the registry record alone: the distinction is that an installer wrote it, which is a different parent process and a different time of day.
  • OPEN No memory capture was taken while the config was decrypted, so the claim about the plaintext config rests on decompilation (EV-007) and not on a live record. A RAM acquisition would settle where the clear-text key lives at rest.
  • Two runs, one Windows build, one sensor, no EDR in the path. Behaviour that depends on the build, the OS version or a security product is outside what this evidence supports.

References

All malware research

Tags

#malware#windows#dotnet#persistence#detection

Related

shares a tag