Skip to content

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 keyWhat it holdsBacking file
HKLM\SYSTEMControl sets: services, drivers, boot configuration%SystemRoot%\System32\config\SYSTEM
HKLM\SOFTWAREMachine-wide software settings, machine Run keys, task cache%SystemRoot%\System32\config\SOFTWARE
HKLM\SAM, HKLM\SECURITYLocal accounts, LSA secrets and policy (readable only by SYSTEM)config\SAM, config\SECURITY
HKU\.DEFAULTProfile used by LocalSystemconfig\DEFAULT
HKU\<SID>One loaded user profile%USERPROFILE%\NTUSER.DAT
HKU\<SID>_ClassesThat 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

TypeContentsWhere you meet it
REG_SZStringRun values, ObjectName of a service
REG_EXPAND_SZString with %VARIABLES% expanded when readImagePath, ServiceDll
REG_MULTI_SZList of stringsService dependencies, svchost groups
REG_DWORD / REG_QWORD32/64-bit integerStart, Type
REG_BINARYRaw bytesEncoded 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:

powershell
# 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 -a runs 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:

ValueMeaningWhat to check
ImagePathCommand line the SCM runs (or driver path)Location, quoting, arguments
Start0 boot, 1 system (drivers), 2 automatic, 3 manual, 4 disabledAutomatic services are the persistence ones
Type0x1 kernel driver, 0x2 file system driver, 0x10 own process, 0x20 shared processA new kernel driver is a much bigger finding
ObjectNameAccount: LocalSystem, NT AUTHORITY\LocalService, NT AUTHORITY\NetworkService, NT SERVICE\<name>, or a userPrivilege of whatever runs
Parameters\ServiceDllDLL loaded by svchost.exe for shared-process servicesPath of the real code
FailureActionsWhat to do when the service crashes, including running a commandA "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:

powershell
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 -AutoSize

Sorting 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, and Tasks\{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.

powershell
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:

LocationPathChange recorded by
Run / RunOnce keysHKLM and HKCU\Software\Microsoft\Windows\CurrentVersion\Run, RunOnce, plus WOW6432NodeSysmon 12/13/14; key last-write time
Services and driversHKLM\SYSTEM\CurrentControlSet\ServicesSystem 7045, Security 4697, Sysmon 12/13
Scheduled tasksSystem32\Tasks, Schedule\TaskCacheSecurity 4698–4702, TaskScheduler Operational 106/140/141, Sysmon 11/12/13
Startup folders%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup, %ProgramData%\...\StartUpSysmon 11 (file create)
Winlogon valuesHKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon Userinit, ShellSysmon 13
WMI subscriptionsroot\subscription filters, consumers, bindings; wbem\Repository offlineSysmon 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 .arn file; 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 -z does 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 autorunsc on 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:

FieldExample
LocationHKCU\Software\Microsoft\Windows\CurrentVersion\Run, value WindowsUpdateCheck
Data, exactly as storedC:\Users\lab\AppData\Roaming\x7q\upd.exe
ScopeWhich user (SID) or machine-wide; 32- or 64-bit view
Target fileSHA-256, signature status, size, file timestamps
TimingKey last-write time, creation events (7045, 4698, Sysmon 13)
Account and privilegeLocalSystem service, task running with highest privileges, user logon
Baseline statusAbsent 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.

  1. 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 --version
    text
    Python 3.14.7
  2. Create the sample export. regedit and reg export write UTF-16LE text with a byte-order mark, and store REG_EXPAND_SZ data as hex(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 -12
    text
    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 grep for SecurityHealth.exe would fail on this file twice over: the text is UTF-16, and the path itself is hex-encoded.

  3. Write the triage script. It decodes the export, collects Run/RunOnce values and service ImagePath/ServiceDll data, 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])
  4. Run it against the export:

    bash
    .venv/bin/python triage_reg.py sample-autoruns.reg
    text
    [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 spaces

    Read the three flagged lines differently. OneDrive installs per user under AppData\Local by design: one weak signal, explained by a known product. VendorSync is a hygiene finding for the vendor. WindowsUpdateCheck combines a user-writable, randomly named folder under Roaming with a name borrowed from Windows Update: that is the entry you would investigate first.

  5. Exercise the other heuristics. Save this as extra.reg in 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.reg
    text
    [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)
  6. 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 ServiceDll

    Run triage_reg.py on the exports and explain every flagged line.

  7. 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.arn on a clean snapshot. Later, on the same VM after installing any piece of software, open Autoruns again and use File → Compare with baseline.arn: new entries are highlighted. Check the Logon, Services and Scheduled Tasks tabs for the software you installed.

  8. 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; HKCU and HKCR are views, and on 64-bit Windows the 32-bit WOW6432Node view 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, ObjectName and, for svchost-hosted services, ServiceDll; installation leaves System 7045 and, if audited, Security 4697.
  • Scheduled tasks live both as XML in System32\Tasks and in the TaskCache registry 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.