Lesson 5.3 · Windows Internals for Analysts· 40 min
The Registry, Services and Scheduled Tasks
How to read the Windows registry, services and scheduled tasks during triage and incident response, live and offline, and review autostart entries.
Objectives
- Describe the registry's hives, root keys, value types and WOW64 view, and locate each hive file on disk
- Read registry keys live with reg query and PowerShell, and offline from collected hives with Registry Explorer and RegRipper
- Review services and scheduled tasks: where they are defined, which fields matter, and which events record their creation
- Audit the common autostart locations with Autoruns, a saved baseline and the telemetry that records changes to each
- Report an autostart finding with enough detail for others to hunt for it and remove it
Processes, Threads and DLLs described what is running now. This lesson is about what Windows has been told to run later. Code that survives a reboot has to be written down somewhere the operating system reads at start-up or logon, and almost all of those places are in three configuration stores: the registry, the service database and the Task Scheduler. Malware that wants to stay needs them, and so do legitimate software, installers and administrators, which is why reading them is a triage skill before it is a malware skill.
In Behavioural Monitoring you diffed registry and Autoruns snapshots taken before and after a run. That works in a lab you control. On a real host in an incident there is no "before" snapshot, so you have to recognise what does not belong by reading the stores themselves, live or from a disk image.
How the registry is organised
The registry is a hierarchical database. Keys are like folders and hold subkeys and values; each value has a name, a type and data. Every key also carries a last-write timestamp, updated whenever the key or one of its values changes. Values have no timestamp of their own, so the key's time is the closest you get to "when was this Run entry added?".
Hives and root keys
On disk the registry is split into hive files. The root keys you see in
regedit are views over those hives:
| Root key | What it holds | Backing file |
|---|---|---|
HKLM\SYSTEM | Control sets: services, drivers, boot configuration | %SystemRoot%\System32\config\SYSTEM |
HKLM\SOFTWARE | Machine-wide software settings, machine Run keys, task cache | %SystemRoot%\System32\config\SOFTWARE |
HKLM\SAM, HKLM\SECURITY | Local accounts, LSA secrets and policy (readable only by SYSTEM) | config\SAM, config\SECURITY |
HKU\.DEFAULT | Profile used by LocalSystem | config\DEFAULT |
HKU\<SID> | One loaded user profile | %USERPROFILE%\NTUSER.DAT |
HKU\<SID>_Classes | That user's COM and file-association settings | %LOCALAPPDATA%\Microsoft\Windows\UsrClass.dat |
Three root keys are links, not stores. HKCU points at the HKU\<SID>
of whoever is asking, so the same reg query HKCU\... returns different
results for different users, and a service running as SYSTEM sees
HKU\S-1-5-18. HKCR is a merged view of HKLM\SOFTWARE\Classes and
the user's classes key, with the user's entries winning. HKCC is a
shortcut into the current hardware profile.
In HKLM\SYSTEM, CurrentControlSet exists only on a running system; it is a
link to one of the numbered ControlSet00n keys. In an offline SYSTEM hive,
read the Select\Current value to know which control set was in use.
Next to each hive file sit transaction logs (.LOG1, .LOG2). A hive
copied from a running or crashed system may be "dirty": recent changes are
still in the logs. Offline tools that replay the logs show you the latest
state; tools that do not can miss exactly the entry you are looking for.
Value types
| Type | Contents | Where you meet it |
|---|---|---|
REG_SZ | String | Run values, ObjectName of a service |
REG_EXPAND_SZ | String with %VARIABLES% expanded when read | ImagePath, ServiceDll |
REG_MULTI_SZ | List of strings | Service dependencies, svchost groups |
REG_DWORD / REG_QWORD | 32/64-bit integer | Start, Type |
REG_BINARY | Raw bytes | Encoded configuration, sometimes whole payloads |
A large REG_BINARY or oddly long string is worth a second look: fileless
families keep encrypted configuration, even code, in values.
The WOW64 view
On 64-bit Windows, 32-bit programs see a redirected HKLM\SOFTWARE: their
reads and writes go to HKLM\SOFTWARE\WOW6432Node. A 32-bit sample that sets
a machine-wide Run value therefore lands under
HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run, and an
analyst who only checks the 64-bit path misses it. HKCU\Software is not
redirected. reg.exe takes /reg:64 or /reg:32 to pick the view explicitly;
Autoruns and offline parsers show both.
Reading the registry
Live
For triage, prefer commands whose output you can paste into your notes over
browsing in regedit:
# One key, all values
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
# The 32-bit view of the same key
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /reg:32
# Recursive, e.g. every value under a suspicious vendor key
reg query "HKCU\Software\SomeVendor" /s
# PowerShell exposes keys as a drive
Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
# Other users: HKCU only shows yours, so walk HKU for loaded profiles
Get-ChildItem Registry::HKEY_USERS | ForEach-Object {
Get-ItemProperty "Registry::$($_.Name)\Software\Microsoft\Windows\CurrentVersion\Run" -ErrorAction SilentlyContinue
}Only profiles of users currently logged on (or with running processes) are
loaded under HKU. Everyone else's NTUSER.DAT is a file on disk, which is
one reason offline analysis matters even on a live host.
To preserve evidence rather than just read it, reg save HKLM\SYSTEM C:\ir\SYSTEM.hiv writes a hive file from an elevated prompt, and reg export
writes a text .reg file. Collection frameworks such as KAPE or Velociraptor
copy the locked hive files together with their transaction logs.
Offline
For collected hives or a mounted disk image, use tools that parse the file format directly instead of loading hives into your own registry:
- Registry Explorer (Eric Zimmerman) opens hive files, replays dirty transaction logs, shows key last-write times and deleted keys recovered from free space, and ships bookmarks for common artefacts. Its command-line twin, RECmd, runs batch files of keys to extract across many hives.
- RegRipper runs plugins that know where specific artefacts live and
prints them as a report.
rip.exe -r NTUSER.DAT -aruns every plugin for that hive type; plugin names vary between releases, so list them with-l.
Parsing the file also defeats tricks that fool the Win32 API, such as
null-byte registry key hiding: a
value name that regedit cannot open is still an ordinary cell in the hive.
Tip: Never load evidence hives into your own workstation's registry with File → Load Hive as a first step. It needs a writable file, may modify it, and ties your analysis to your own machine. Work on a hashed copy with a parser.
Services
A service is a program started and supervised by the Service Control
Manager (SCM), which runs as services.exe. Services start before anyone
logs on, restart on failure if configured to, and usually run as a privileged
account, which is why service persistence
is attractive to attackers who already have administrative rights.
Each service and driver is a subkey of HKLM\SYSTEM\CurrentControlSet\Services:
| Value | Meaning | What to check |
|---|---|---|
ImagePath | Command line the SCM runs (or driver path) | Location, quoting, arguments |
Start | 0 boot, 1 system (drivers), 2 automatic, 3 manual, 4 disabled | Automatic services are the persistence ones |
Type | 0x1 kernel driver, 0x2 file system driver, 0x10 own process, 0x20 shared process | A new kernel driver is a much bigger finding |
ObjectName | Account: LocalSystem, NT AUTHORITY\LocalService, NT AUTHORITY\NetworkService, NT SERVICE\<name>, or a user | Privilege of whatever runs |
Parameters\ServiceDll | DLL loaded by svchost.exe for shared-process services | Path of the real code |
FailureActions | What to do when the service crashes, including running a command | A "restart" action that runs something else |
Many Windows services are DLLs hosted by svchost.exe. Their ImagePath is
%SystemRoot%\system32\svchost.exe -k <group>, which looks identical for
dozens of services; the code that actually runs is named by ServiceDll, and
the groups are listed under
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost. A service whose
ImagePath looks normal but whose ServiceDll points outside System32 is a
classic hiding place.
Review services with commands that show the fields above:
sc.exe qc Dnscache # config: binary path, start type, account
sc.exe qdescription Dnscache
Get-CimInstance Win32_Service |
Select-Object Name, StartMode, State, StartName, PathName |
Sort-Object PathName | Format-Table -AutoSizeSorting by PathName makes the odd ones stand out. Watch also for unquoted paths with spaces:
C:\Program Files\Vendor Tools\sync agent\syncsvc.exe without quotes makes
Windows try C:\Program.exe and C:\Program Files\Vendor.exe first. That is
a vendor bug and a privilege-escalation opening, not proof of compromise, but
it belongs in your report.
When a service is installed, the SCM writes System event 7045 ("A service
was installed in the system") with the name, image path, start type and
account. If Audit Security System Extension is enabled, the Security log
also gets 4697 with the subject who installed it. Sysmon 12/13 record the
registry writes under Services if your configuration watches that path.
Scheduled tasks
The Task Scheduler (the Schedule service, hosted in svchost.exe) runs
actions on triggers: at logon, at boot, on a timer, on an event, when idle.
Tasks can run as SYSTEM, as a user, or with highest privileges, and are
the persistence mechanism many administrators, installers and attackers
reach for first (scheduled task persistence).
A registered task exists in two places:
- An XML definition in
C:\Windows\System32\Tasks\, one file per task, with folders mirroring the task path. It holds triggers, actions, the principal and the registration date and author. - A registry cache under
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache:Tree\<path>maps the name to a GUID, andTasks\{GUID}stores the actions, triggers and hashes the service uses.
The two should agree. Attackers have hidden tasks from schtasks and the GUI
by deleting the SD (security descriptor) value under the task's Tree key;
the task still runs. Offline, compare the XML folder, Tree and Tasks
against each other.
schtasks /query /fo LIST /v
Get-ScheduledTask | Where-Object TaskPath -notlike '\Microsoft\*' |
Select-Object TaskPath, TaskName, State,
@{n='Action'; e={$_.Actions.Execute + ' ' + $_.Actions.Arguments}},
@{n='RunAs'; e={$_.Principal.UserId}}Task creation leaves several records. The
Microsoft-Windows-TaskScheduler/Operational log records registration
(106), updates (140), deletion (141) and each run (200 action started,
201 completed); confirm that it is enabled on your fleet, since it has been off
by default on some Windows versions. With Audit Other Object Access Events
enabled, the Security log records 4698 (created, with the full task
XML), 4699 (deleted), 4700/4701 (enabled/disabled) and 4702 (updated). Sysmon
11 catches the XML file appearing under System32\Tasks, and 12/13 the
TaskCache keys, where the configuration covers them.
Where to look: a defender's autostart table
A full catalogue of persistence techniques is the job of Persistence Mechanisms. For triage, start with the locations below, and know which telemetry would have recorded a change:
| Location | Path | Change recorded by |
|---|---|---|
| Run / RunOnce keys | HKLM and HKCU\Software\Microsoft\Windows\CurrentVersion\Run, RunOnce, plus WOW6432Node | Sysmon 12/13/14; key last-write time |
| Services and drivers | HKLM\SYSTEM\CurrentControlSet\Services | System 7045, Security 4697, Sysmon 12/13 |
| Scheduled tasks | System32\Tasks, Schedule\TaskCache | Security 4698–4702, TaskScheduler Operational 106/140/141, Sysmon 11/12/13 |
| Startup folders | %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup, %ProgramData%\...\StartUp | Sysmon 11 (file create) |
| Winlogon values | HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon Userinit, Shell | Sysmon 13 |
| WMI subscriptions | root\subscription filters, consumers, bindings; wbem\Repository offline | Sysmon 19/20/21, WMI-Activity Operational 5861 |
For Winlogon, the expected data is short and well known: Userinit is
C:\Windows\system32\userinit.exe, and Shell is explorer.exe. Anything
appended is a finding. For WMI, query live with
Get-CimInstance -Namespace root\subscription -ClassName __EventFilter (and
CommandLineEventConsumer, ActiveScriptEventConsumer,
__FilterToConsumerBinding); a clean workstation has very few.
Autoruns covers the table
Autoruns enumerates all of these locations and many more (Explorer extensions, browser helpers, codecs, LSA providers, print monitors, Image File Execution Options), resolves each entry to a file, and can verify its signature. Three settings make it a triage tool rather than a wall of text:
- Options → Hide Microsoft Entries (and Hide Windows Entries) hides signed operating-system entries. Turn on Verify Code Signatures first, or the filter trusts the file's claimed company name.
- File → Save writes an
.arnfile; File → Compare against a saved one highlights new entries in green. - File → Analyze Offline System points Autoruns at a mounted image's
Windows directory and a user profile;
autorunsc -zdoes the same from the command line.
Its console version produces CSV for scripting and fleet collection:
autorunsc64 -accepteula -a * -c -h -s -m (all categories, CSV, hashes,
signature check, hide Microsoft-signed entries).
Tip: Autoruns' VirusTotal check sends file hashes to a third party. That is usually acceptable; submitting unknown files is not, during an incident. Check your policy before enabling it.
Baselining and diffing
A single host's autostart list is hard to judge on its own. Two comparisons make it easier:
- Against a baseline. Run
autorunscon your gold image and after major updates, keep the CSV, and diff new collections against it. Anything absent from the image needs an owner: a software deployment, an administrator, or nobody. - Across the fleet. Collect the same CSV from many hosts and count how often each entry (location, name, path, hash) appears. An entry on 4,000 machines is probably your software; one on two machines is where to start. This "least frequency of occurrence" review works equally well for services and tasks.
Then build a timeline: key last-write times, task registration dates, 7045 and 4698 events and the target binary's file timestamps should line up. When they do not, ask why; timestamps can be forged, and legitimate activity rewrites some keys.
What to put in a report
For each autostart finding, record enough for someone else to find it on another host and remove it cleanly:
| Field | Example |
|---|---|
| Location | HKCU\Software\Microsoft\Windows\CurrentVersion\Run, value WindowsUpdateCheck |
| Data, exactly as stored | C:\Users\lab\AppData\Roaming\x7q\upd.exe |
| Scope | Which user (SID) or machine-wide; 32- or 64-bit view |
| Target file | SHA-256, signature status, size, file timestamps |
| Timing | Key last-write time, creation events (7045, 4698, Sysmon 13) |
| Account and privilege | LocalSystem service, task running with highest privileges, user logon |
| Baseline status | Absent from gold image; seen on N hosts |
Normalise user-specific paths (%APPDATA%\x7q\upd.exe) for hunting, but keep
the literal value in the evidence. The triage report
structure from Module 3 has a section for host indicators; autostart entries
go there. Removal guidance should name both the entry and the file, since
removing one often leaves the other to be restored.
Lab: triage an autostart export
You will build a small .reg export containing Run values and service keys,
most of them ordinary, and a Python script that flags entries by simple
heuristics. Nothing in this lab creates autostart entries on any system: the
"registry" is a text file.
-
Set up a working folder and virtual environment (only the standard library is needed):
bash mkdir m5r && cd m5r python3 -m venv .venv .venv/bin/python --versiontext Python 3.14.7 -
Create the sample export.
regeditandreg exportwrite UTF-16LE text with a byte-order mark, and storeREG_EXPAND_SZdata ashex(2):bytes rather than readable text. This generator reproduces both, so the parser has to cope with what real exports look like:python # make_sample.py - writes sample-autoruns.reg the way regedit exports it (UTF-16LE with BOM). # REG_EXPAND_SZ data is exported as hex(2): little-endian UTF-16 bytes plus a terminating NUL. def hex2(s): b = (s + "\0").encode("utf-16-le") return "hex(2):" + ",".join(f"{x:02x}" for x in b) def wrap(line, width=78): # regedit wraps long hex data with a trailing backslash and two-space indent out, cur = [], line while len(cur) > width: cut = cur.rfind(",", 0, width) + 1 out.append(cur[:cut] + "\\") cur = " " + cur[cut:] out.append(cur) return "\r\n".join(out) R = r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" U = r"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" S = r"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services" lines = ["Windows Registry Editor Version 5.00", "", f"[{R}]", wrap('"SecurityHealth"=' + hex2(r"%windir%\system32\SecurityHealthSystray.exe")), r'"VendorTray"="\"C:\\Program Files\\Vendor Tools\\tray.exe\" /minimized"', "", f"[{U}]", r'"OneDrive"="\"C:\\Users\\lab\\AppData\\Local\\Microsoft\\OneDrive\\OneDrive.exe\" /background"', r'"WindowsUpdateCheck"="C:\\Users\\lab\\AppData\\Roaming\\x7q\\upd.exe"', "", f"[{S}\\Dnscache]", wrap('"ImagePath"=' + hex2(r"%SystemRoot%\system32\svchost.exe -k NetworkService -p")), '"Start"=dword:00000002', '"Type"=dword:00000020', r'"ObjectName"="NT AUTHORITY\\NetworkService"', "", f"[{S}\\Dnscache\\Parameters]", wrap('"ServiceDll"=' + hex2(r"%SystemRoot%\System32\dnsrslvr.dll")), "", f"[{S}\\Spooler]", wrap('"ImagePath"=' + hex2(r"%SystemRoot%\System32\spoolsv.exe")), '"Start"=dword:00000002', '"Type"=dword:00000010', '"ObjectName"="LocalSystem"', "", f"[{S}\\AcmeBackup]", r'"ImagePath"="\"C:\\Program Files\\Acme\\Backup\\acmebk.exe\" -service"', '"Start"=dword:00000003', '"Type"=dword:00000010', '"ObjectName"="LocalSystem"', "", f"[{S}\\VendorSync]", r'"ImagePath"="C:\\Program Files\\Vendor Tools\\sync agent\\syncsvc.exe"', '"Start"=dword:00000002', '"Type"=dword:00000010', '"ObjectName"="LocalSystem"', "", ] with open("sample-autoruns.reg", "wb") as f: f.write(("\ufeff" + "\r\n".join(lines) + "\r\n").encode("utf-16-le"))bash .venv/bin/python make_sample.py file sample-autoruns.reg iconv -f UTF-16 -t UTF-8 sample-autoruns.reg | tr -d '\r' | head -12text sample-autoruns.reg: Windows Registry little-endian text (Win2K or above) Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run] "SecurityHealth"=hex(2):25,00,77,00,69,00,6e,00,64,00,69,00,72,00,25,00,5c,00,\ 73,00,79,00,73,00,74,00,65,00,6d,00,33,00,32,00,5c,00,53,00,65,00,63,00,75,\ 00,72,00,69,00,74,00,79,00,48,00,65,00,61,00,6c,00,74,00,68,00,53,00,79,00,\ 73,00,74,00,72,00,61,00,79,00,2e,00,65,00,78,00,65,00,00,00 "VendorTray"="\"C:\\Program Files\\Vendor Tools\\tray.exe\" /minimized" [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run] "OneDrive"="\"C:\\Users\\lab\\AppData\\Local\\Microsoft\\OneDrive\\OneDrive.exe\" /background" "WindowsUpdateCheck"="C:\\Users\\lab\\AppData\\Roaming\\x7q\\upd.exe"Note that a plain
grepforSecurityHealth.exewould fail on this file twice over: the text is UTF-16, and the path itself is hex-encoded. -
Write the triage script. It decodes the export, collects Run/RunOnce values and service
ImagePath/ServiceDlldata, and applies five heuristics: user-writable location, interpreter or proxy binary, unexpected extension, unquoted path with spaces, and a vendor-sounding name pointing outside the Windows and Program Files directories.python # triage_reg.py - flag autostart entries in a regedit .reg export by simple heuristics. import re, sys USER_WRITABLE = re.compile(r"\\users\\|%appdata%|%localappdata%|%temp%|\\programdata\\|\\windows\\temp\\", re.I) INTERPRETERS = {"cmd.exe", "powershell.exe", "pwsh.exe", "wscript.exe", "cscript.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe"} EXPECTED_EXT = {".exe", ".dll", ".sys"} TRUSTED_DIRS = re.compile(r"^(%systemroot%|%windir%|c:\\windows|c:\\program files)", re.I) VENDOR_WORDS = re.compile(r"windows|microsoft|update|defender", re.I) def read_reg(path): raw = open(path, "rb").read() text = raw.decode("utf-16") if raw[:2] in (b"\xff\xfe", b"\xfe\xff") else raw.decode("utf-8-sig") return re.sub(r"\\\r?\n\s*", "", text) # join wrapped hex lines def decode(data): if data.startswith('"'): return data[1:-1].replace('\\"', '"').replace("\\\\", "\\"), "REG_SZ" if data.startswith("dword:"): return int(data[6:], 16), "REG_DWORD" if data.startswith("hex(2):"): b = bytes(int(x, 16) for x in data[7:].split(",")) return b.decode("utf-16-le").rstrip("\0"), "REG_EXPAND_SZ" return data, "OTHER" def parse(text): keys, key = {}, None for line in text.splitlines(): if line.startswith("[") and line.endswith("]"): key = keys.setdefault(line[1:-1], {}) elif key is not None and (m := re.match(r'^"((?:[^"\\]|\\.)*)"=(.*)$', line)): key[m.group(1)] = decode(m.group(2)) return keys def split_command(cmd): """Return (executable path, was it quoted?) the way CreateProcess would read it.""" cmd = cmd.strip() if cmd.startswith('"'): return cmd[1:cmd.index('"', 1)], True m = re.match(r"^(.+?\.(exe|dll|sys|bat|cmd|ps1|vbs|js|hta|scr|com))\b", cmd, re.I) return (m.group(1) if m else cmd.split(" ")[0]), False def check(name, command): exe, quoted = split_command(command) base = exe.lower().rsplit("\\", 1)[-1] ext = "." + base.rsplit(".", 1)[-1] if "." in base else "" flags = [] if USER_WRITABLE.search(command): # whole line: catches scripts passed to interpreters flags.append("user-writable path") if base in INTERPRETERS: flags.append(f"interpreter/proxy ({base})") elif ext not in EXPECTED_EXT: flags.append(f"unusual extension ({ext or 'none'})") if not quoted and " " in exe: flags.append("unquoted path with spaces") if VENDOR_WORDS.search(name) and not TRUSTED_DIRS.match(exe): flags.append("vendor-like name outside Windows/Program Files") return exe, flags START = {0: "boot", 1: "system", 2: "auto", 3: "demand", 4: "disabled"} HIVE = {"HKEY_LOCAL_MACHINE": "HKLM", "HKEY_CURRENT_USER": "HKCU", "HKEY_USERS": "HKU"} def entries(keys): """Yield (location, name, command, details, [paths to check]).""" for key, values in keys.items(): hive, _, rest = key.partition("\\") last = rest.rsplit("\\", 1)[-1] if last.lower() in ("run", "runonce"): for name, (data, _) in values.items(): yield f"{HIVE.get(hive, hive)} {last}", name, data, "", [data] elif "\\services\\" in key.lower() and "ImagePath" in values: image = values["ImagePath"][0] start = START.get(values.get("Start", (None,))[0], "?") details = f"start={start} account={values.get('ObjectName', ('?',))[0]}" checks = [image] dll = keys.get(key + "\\Parameters", {}).get("ServiceDll") if dll: details += f" ServiceDll={dll[0]}" checks.append(dll[0]) yield "Service", last, image, details, checks def main(path): for where, name, command, details, checks in entries(parse(read_reg(path))): flags = [f for c in checks for f in check(name, c)[1]] level = "HIGH" if len(flags) >= 2 else "LOW " if flags else "ok " print(f"[{level}] {where} {name}: {command}") if details: print(f" {details}") for f in flags: print(f" - {f}") if __name__ == "__main__": main(sys.argv[1]) -
Run it against the export:
bash .venv/bin/python triage_reg.py sample-autoruns.regtext [ok ] HKLM Run SecurityHealth: %windir%\system32\SecurityHealthSystray.exe [ok ] HKLM Run VendorTray: "C:\Program Files\Vendor Tools\tray.exe" /minimized [LOW ] HKCU Run OneDrive: "C:\Users\lab\AppData\Local\Microsoft\OneDrive\OneDrive.exe" /background - user-writable path [HIGH] HKCU Run WindowsUpdateCheck: C:\Users\lab\AppData\Roaming\x7q\upd.exe - user-writable path - vendor-like name outside Windows/Program Files [ok ] Service Dnscache: %SystemRoot%\system32\svchost.exe -k NetworkService -p start=auto account=NT AUTHORITY\NetworkService ServiceDll=%SystemRoot%\System32\dnsrslvr.dll [ok ] Service Spooler: %SystemRoot%\System32\spoolsv.exe start=auto account=LocalSystem [ok ] Service AcmeBackup: "C:\Program Files\Acme\Backup\acmebk.exe" -service start=demand account=LocalSystem [LOW ] Service VendorSync: C:\Program Files\Vendor Tools\sync agent\syncsvc.exe start=auto account=LocalSystem - unquoted path with spacesRead the three flagged lines differently. OneDrive installs per user under
AppData\Localby design: one weak signal, explained by a known product.VendorSyncis a hygiene finding for the vendor.WindowsUpdateCheckcombines a user-writable, randomly named folder underRoamingwith a name borrowed from Windows Update: that is the entry you would investigate first. -
Exercise the other heuristics. Save this as
extra.regin plain UTF-8 (the script accepts either encoding) and run it:text Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce] "Cleanup"="wscript.exe //B C:\\ProgramData\\q\\a.js" "Helper"="C:\\Users\\lab\\AppData\\Local\\Temp\\h.dat"bash .venv/bin/python triage_reg.py extra.regtext [HIGH] HKCU RunOnce Cleanup: wscript.exe //B C:\ProgramData\q\a.js - user-writable path - interpreter/proxy (wscript.exe) [HIGH] HKCU RunOnce Helper: C:\Users\lab\AppData\Local\Temp\h.dat - user-writable path - unusual extension (.dat) -
Do the same on Windows with
reg. In an analysis VM, export the keys you want to review and query them directly:powershell reg export "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" C:\ir\hkcu-run.reg /y reg export "HKLM\SYSTEM\CurrentControlSet\Services" C:\ir\services.reg /y reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /reg:32 reg query "HKLM\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters" /v ServiceDllRun
triage_reg.pyon the exports and explain every flagged line. -
Do it with Autoruns. Run Autoruns as administrator. Enable Options → Verify Code Signatures, then Hide Microsoft Entries and Hide Windows Entries, and press F5. Save the result as
baseline.arnon a clean snapshot. Later, on the same VM after installing any piece of software, open Autoruns again and use File → Compare withbaseline.arn: new entries are highlighted. Check the Logon, Services and Scheduled Tasks tabs for the software you installed. -
Cross-check the telemetry. For the software you installed, look for System 7045 if it added a service, and the TaskScheduler Operational log (event 106) if it registered a task. Note which of them you would have missed with your current audit policy.
Questions to answer: Why does the script need to decode hex(2) data
before any path check can work, and what would a grep on the raw export have
missed? Which heuristic would fire on a legitimate Chrome or Teams per-user
install, and how would a fleet-wide frequency count help you dismiss it? How
would you extend the script to read the WOW6432Node Run key and scheduled
task actions? For the WindowsUpdateCheck entry, list the evidence you would
collect next and the events that could date its creation.
Key takeaways
- The registry is a set of hive files;
HKCUandHKCRare views, and on 64-bit Windows the 32-bitWOW6432Nodeview must be checked separately. - Key last-write times are your registry timestamps; values have none. Offline parsers that replay transaction logs show the latest state and can see what the Win32 API hides.
- For services, read
ImagePath,Start,Type,ObjectNameand, for svchost-hosted services,ServiceDll; installation leaves System 7045 and, if audited, Security 4697. - Scheduled tasks live both as XML in
System32\Tasksand in theTaskCacheregistry keys; creation is logged in Security 4698 and the TaskScheduler Operational log when those are enabled. - Autoruns with signature verification, Microsoft entries hidden and a saved baseline turns an autostart review into a short list; fleet-wide frequency counts shorten it further.
- Report autostart findings with the exact location, data, scope, target file hash, timing and baseline status, so others can hunt and remove them.