Persistence Hunter¶
Persistence lets a program run again after a reboot, a sign-in, a scheduled trigger, or another event. An attacker can use normal Windows configuration for this purpose. Many legitimate programs use exactly the same mechanisms, so finding a persistence entry is a starting point for analysis.
juntei persistence-hunter is a native Rust port of
Hexastrike's PyrsistenceSniper, with 131 checks: the
130-check PyrsistenceSniper catalogue plus a native Start Menu COM check.
For an explanation of every mechanism, its evidence locations, and how to investigate a finding, see the 131-check persistence guide. The persistence acknowledgements describe the Python and PowerShell projects behind the catalogue.
Usage¶
Version: 1.1.0 (CODE BLUE Release)
Yamato Security (https://github.com/Yamato-Security/juntei - @SecurityYamato)
Usage:
juntei persistence-hunter -d <EVIDENCE> [OPTIONS]
Input:
-d, --directory <DIR> Evidence directory (a mounted Windows volume or a collection of hosts)
-f, --file <FILE> One registry hive (filesystem checks need a directory)
--no-logs Read dirty hives without replaying transaction logs
Filtering:
--profile <FILE> Replace the bundled allow/block profile with a YAML profile
--min-severity <MIN_SEVERITY> Minimum classification to include: info, low, medium, high [default: medium] [possible values: info, low, medium, high]
--check <ID> Run only these check or ATT&CK IDs (comma separated; repeatable) [alias: --technique]
General Options:
-h, --help Show the help menu
--list-checks Show every available check and its ATT&CK technique
Analysis:
--no-timeline Skip MFT and event-log timestamp correlation
--mft <FILE> MFT to use for timestamp correlation (one host only)
Performance:
-T, --threads <N> Worker threads (default: logical CPU count)
Output:
-o, --output <FILE> Save findings to a file
-t, --output-type <OUTPUT_TYPE> Output type: csv (default), json, jsonl; without -o, write data to stdout [possible values: csv, json, jsonl]
--coverage <FILE> Save per-check coverage and errors to CSV
-C, --clobber Overwrite output files
Display Settings:
-K, --no-color Disable color output
-q, --quiet Quiet mode: do not display the launch banner
First scan¶
The evidence can be a mounted Windows volume, a Velociraptor collection, or a directory containing
several collected computers. Computers are kept separate during analysis, and every result includes
Comp. Collected filename escaping is decoded when resolving Windows paths.
For one hive:
A single hive cannot supply checks that need another hive or filesystem evidence. Read the coverage report before treating an empty result as meaningful.
List the check IDs and their ATT&CK mappings:
juntei persistence-hunter --list-checks
juntei persistence-hunter -d evidence/ --check run_keys,windows_service_image_path -o focused.csv
Use the check IDs printed by --list-checks, or an ATT&CK ID such as T1547.001.
A parent ID such as T1547 selects its sub-techniques. --technique is an alias for --check.
Each check includes its technique, description, and reference links in the findings.
Parallel scans¶
The command uses a shared worker pool, defaulting to the number of logical CPUs. Set
-T, --threads <N> to choose the worker count, or -T 1 for a single-thread scan:
Evidence discovery, independent checks, payload metadata, and MFT/EVTX timeline sources run in parallel. A host's loaded registry hives are shared read-only, and cached metadata for the same payload is initialized once. Hosts are loaded one at a time to avoid multiplying hive memory across the collection. Findings, coverage, timestamp selection, and warning order remain the same at different thread counts.
auto-parse already runs parser processes in parallel, so it gives each persistence-hunter child
one worker. Its own -T option controls the number of concurrent parser processes.
Evidence to collect¶
| Source | Examples of checks |
|---|---|
SYSTEM and adjacent transaction logs |
Services, service recovery commands, LSA packages, boot settings, authentication providers |
SOFTWARE and adjacent transaction logs |
Run keys, Winlogon, IFEO, COM registrations, TaskCache, Office settings |
Each user's NTUSER.DAT and UsrClass.dat |
User autostarts, environment variables, COM overrides, Office settings |
SAM and SOFTWARE |
RID anomalies, hidden accounts, unusual local Administrators membership |
Windows/System32/Tasks |
Scheduled-task commands and COM handlers; comparison with TaskCache |
Per-user Start Menu .library-ms files and CLSID-named folders |
COM activation-trigger candidates, joined to matching user/machine class registrations |
| Startup folders, PowerShell profiles, Office templates, Terminal settings | Files and commands run by user or system triggers |
WMI OBJECTS.DATA |
Event consumer instances and filter-to-consumer bindings |
BITS qmgr.db or qmgr*.dat |
Recovered notification commands and owner SIDs |
$MFT and relevant EVTX files |
Independent timestamp correlation |
| Referenced executables, DLLs, and catalog files | SHA256 hashes and descriptive PE/certificate metadata |
Registry transaction logs are replayed when available. --no-logs reads the hives as collected;
an unreplayed dirty hive may omit recent changes and is reported in coverage. The current control
set is used for active-system checks. The DSRM backdoor check additionally examines
ControlSet001, ControlSet002, and CurrentControlSet when present; check its reported path
against SYSTEM\\Select before describing that setting as active. Other historical control sets
can be examined with the individual registry parser commands.
Hive reads share a 1 GiB per-computer input budget. Machine hives are attempted before user hives;
inputs or transaction logs that do not fit are named in partial coverage. Other readable hives and
file-backed checks continue. Only canonical hive locations or explicitly supplied exports establish
hive identity; a copy inside a user's temporary directory cannot replace that user's real hive.
The original system drive recorded in SOFTWARE is mapped to the captured system volume, so mounting
an original C: volume under a different evidence drive label does not discard payload metadata.
auto-parse also runs the hunter and saves persistence/PersistenceHunter.csv (or .json/.jsonl)
inside each computer directory, with PersistenceHunter-logs.txt beside it. Its findings also enter
the optional -j <FILE> SIEM timeline. Automatic parsing uses --min-severity info to retain the
complete configuration inventory, including entries matched by allow rules.
COM registrations and activation triggers¶
The scheduled_task_files check selects each action's principal using the task XML's
Actions Context and matching Principal id. A group principal is retained as a group;
it cannot establish which individual user's class registrations would be used.
COM-handler candidates are resolved separately in native and 32-bit class views. Only the
matching user's captured UsrClass.dat/NTUSER.DAT and machine SOFTWARE are considered.
SID principals are joined through captured ProfileList paths. Account names, including
HOST\user and DOMAIN\user, prefer a matching user.DOMAIN profile over a plain
user profile. Local host aliases and .\user use captured host names for suffix matching;
an exact suffix takes precedence over other host aliases. The plain profile is considered
only when no qualified profile matches. Every such match is explicitly labeled a
folder-name heuristic in Description; it does not prove account ownership or domain membership. Ambiguous matches
remain unresolved and are reported in both the finding description and coverage; ambiguity
among qualified profiles does not trigger a fallback to the plain profile. This does not depend
on Volatile Environment, which is normally memory-only and is not saved with registry hives. A duplicate user CLSID
shadows the machine CLSID's children. Group principals use machine COM candidates and retain
that limitation in the finding description; a normal group principal does not itself make
coverage partial or consume the error-summary entries.
The resolver follows InprocServer32 and LocalServer32, uses managed CodeBase paths
(including version subkeys), and joins AppID LocalService to the collected SYSTEM service
ImagePath. AppID executable-name associations are retained, but a basename does not establish
an executable's full path. Local file: CodeBase URLs are decoded; remote URLs are recorded
without accessing them. Missing registrations retain the CLSID with
ResolutionStatus: unresolved_com_class, empty path/hash fields, and unknown Exists.
The start_menu_com check reads per-user Start Menu .library-ms XML URLs and collects
CLSID-named folders, including empty directories. It reports the trigger path, CLSID, candidate
server, and registry provenance. A file-only acquisition cannot preserve an empty directory,
and missing user hives prevent a complete per-user registration lookup. Symbolic links are
not followed. These are configuration candidates: architecture, elevation, UAC, policy and
activation context can affect whether a registration is actually used. Corroborate execution
with process, module-load or other independent evidence.
Classification and profiles¶
The default minimum classification is medium. To inspect the full inventory, including
entries matched by the bundled allowlist:
Use all severities when reviewing incomplete collections
A broad allow rule can partially match an attacker's command even when its signer is missing
or different. Upstream classifies partial matches as LOW, which the manual command's default
medium threshold hides. This can affect malicious scheduled tasks under Windows directories.
Use --min-severity info for a complete investigation and review PolicyReason, SignerStatus,
the command, and source evidence together. auto-parse already retains these entries.
| Classification | Meaning |
|---|---|
HIGH |
A configured block rule fully matched |
MEDIUM |
No allow or block rule matched |
LOW |
An allow rule's hard conditions matched, but signer metadata did not match or was unavailable |
INFO |
An allow rule fully matched |
These are policy classifications, not measured probabilities of compromise. A default MEDIUM
entry is not proof of malware. INFO does not establish that a file has a valid signature or that
an attacker did not abuse it. A partial allow match is below the default threshold, so include
--min-severity low or info when reviewing sparse collections or tuning the allowlist.
Supply a YAML profile with --profile profile.yaml. It replaces the bundled profile. Global
and per-check rules within the selected file are combined; any fully matched block rule wins.
For example:
allow: []
block:
- reason: Example known-bad command fragment
value_matches: 'example-malicious-loader\.exe'
checks:
windows_service_image_path:
enabled: true
allow:
- reason: Organization's expected service path
value_matches: '^C:\\Program Files\\ExampleCorp\\Agent\\agent\.exe$'
signer: ExampleCorp
Supported rule conditions are path_matches, value_matches, hash (64 hexadecimal SHA256
digits), not_lolbin, and signer (case-insensitive substring). All stated hard conditions must
match. Signer metadata is the soft condition described above. Regexes use Rust's regular-expression
syntax: lookarounds and backreferences are not supported. Unknown check IDs, invalid expressions,
and unknown profile fields produce an error rather than silently weakening the policy.
Output fields¶
CSV is the default file format. Use -t json for readable JSON objects or -t jsonl for compact,
one-object-per-line output. Without -o, an explicit -t writes only data to stdout; coverage
warnings go to stderr. Without either option, Juntei prints a summary and up to 30 candidates.
| Fields | How to interpret them |
|---|---|
Comp, CheckID, Technique, MitreID |
Which computer and persistence mechanism the row describes |
Path, Value |
The registry location, file, task, or recovered record and its configured content |
Description, References |
Why the mechanism matters and where to read more |
AccessGained |
The mechanism's usual scope (USER or SYSTEM; BITS can also identify LOCAL_SERVICE/NETWORK_SERVICE); not proof that a specific command achieved that privilege |
Severity, PolicyReason |
Profile classification, matching policy explanation, and any partial-match or unverified-signer qualification |
SourceFile, RecordStatus |
Exact collected source, its path starting with the input as given (./HOST/... from auto-parse), and allocation/replay/recovery context; follow this back to original evidence |
KeyLastWrite |
The registry key's timestamp. It does not date an individual value |
LastChange, ChangeSource, ChangeEvidence |
Independently correlated change time, source proof, and whether correlation succeeded |
ResolvedPath, ResolutionStatus, Exists |
Payload path and offline resolution result; uncollected evidence is unknown, not false |
SHA256 |
SHA256 of the collected payload bytes, when readable |
Signer, SignerStatus |
Descriptive embedded certificate/catalog metadata and its availability; certificate trust and signature validity are not verified |
IsLolbin, Launcher |
Whether the configured launcher or payload is in the bundled LOLBAS list, and the launcher used for a proxied payload |
IsBuiltin, IsInOS-Directory |
Context for common Windows commands and system directories; neither field establishes legitimacy |
A rundll32.exe command can name a DLL payload. Juntei preserves the launcher while resolving and
hashing the payload. A reference outside the collected computer, a missing drive, an ambiguous
case-insensitive match, or an unresolved environment variable is not substituted with a file from
the analyst's machine.
Timestamps and limitations¶
The hunter does not use the analyst VM's copied-file modification times as attack timestamps.
Where available, it correlates registry writes with Sysmon event 13, task installation with
Security 4698 or Task Scheduler 106, service installation with System 7045, and file-backed
configuration with original $MFT timestamps. The provenance appears in ChangeSource.
An event can date a configuration change without proving that the configured payload ran.
Local System event 6011 can prove an earlier computer name. The hunter follows verified rename
chains anchored to the collected computer, preserving its current Comp value while recording
the original event computer and rename-record proof in ChangeSource. Unproven foreign names,
archived logs, forwarded logs, and another Windows installation are not accepted through this rule.
Captured ComputerName/ActiveComputerName, TCP/IP Hostname/NV Hostname, and client-info hostname/FQDN supply host aliases, covering DNS/NetBIOS differences and pending renames. With no captured identity, a single name in the primary logs can support dating with explicit unverified-identity provenance and a coverage warning. Multiple unverified names are not attributed to the host. Rejected Computer names are reported as a correlation coverage limitation; their channels are still marked collected.
Use --mft <FILE> to select an externally collected MFT for a single computer. Use --no-timeline
to skip timestamp correlation. NOT_RUN, NOT_APPLICABLE, NO_ARTIFACT, NO_MATCH, and REJECTED
explain an empty LastChange; an empty field is not evidence that the entry is harmless.
WMI and BITS recovery is bounded carving rather than a complete database transaction parser. Recovered strings or bindings can include old/deleted data; these records must be corroborated. WMI class schemas alone are not counted as subscriptions. A TaskCache entry without a collected task XML file is a discrepancy for review; a triage collection may simply have omitted that file. Catalog and PE metadata are unverified and may be absent from a collection. Accessibility-file replacement checks depend on suitable reference signature metadata; missing catalog evidence must not cause every accessibility executable to be flagged.
Coverage is recorded for every check, including checks with zero findings. complete means the
applicable collected source was read; it does not prove that the collection is complete or that
the computer has no other persistence. partial, not_collected, failed, and disabled explain
the limits. Keep --coverage Coverage.csv and the parser log with your findings.
The port uses Juntei's CSV/JSON/JSONL formats. It does not call the upstream Python program, query online enrichment services, or reproduce the upstream HTML/XLSX UI.