PolinRider indicators of compromise
One source of truth, in ioc/.
Every script reads these files at runtime, so adding an indicator there updates
the organization scanner, the account scanner and all three local checks at once.
| File | Meaning | Effect on verdict |
|---|---|---|
strong.txt |
Confirmed PolinRider artifacts | INFECTED |
strong-patterns.txt |
Patterns over file contents where a part rotates (extended regex, with a fixed part of 3 or more characters, no ^ or $) |
INFECTED (ADR-0052) |
bad-packages.txt |
Malicious package names from the campaign and from related malware | INFECTED |
filenames.txt |
Paths that are indicators on their own (extended regex) | INFECTED |
filenames-review.txt |
Paths worth a look that legitimate projects can also have (extended regex) | review only (ADR-0048) |
ssh-keys.txt |
SSH keys that related malware adds to ~/.ssh/authorized_keys: a key’s comment or the key itself |
INFECTED, removed by hand (ADR-0049) |
weak.txt |
Strings legitimate projects also use | review only |
network.txt |
Hosts and IP addresses of the campaign and of related malware | INFECTED if a live connection matches |
implant-paths.txt |
Install, persistence and working paths of the related MicrosoftSystem64 implant |
INFECTED if the path exists |
implant-names.txt |
Implant binary and process names | INFECTED on an exact process-name match |
hashes.txt |
SHA-256 of known files of the campaign and of related malware: MicrosoftSystem64 binaries, and small files such as a poisoned tasks.json or a fake font |
INFECTED on a hash match, whatever the file is named. Files of 10 to 300 MB are hashed, and every tasks.json, font file and build config at any size (ADR-0045) |
“Related malware” is MicrosoftSystem64 and the npm packages that install
it: malware from the same DPRK-linked group, not part of PolinRider itself.
What PolinRider is
explains the link and its sources.
Format: one entry per line. Lines starting with # and blank lines are ignored.
strong.txt and weak.txt are matched as fixed strings (grep -F), so no
regex escaping is needed. bad-packages.txt holds names, compared whole: a
package.json, package-lock.json, yarn.lock or pnpm-lock.yaml is read
for the packages it installs, and anywhere else a name counts only where it
stands on its own, so sass-format is not found inside sass-formatter and a
description is not a dependency. A manifest that cannot be read is matched as
text and reported for review. The CI scanner, ci/scan-workspace.sh, does the
same. strong.txt and
bad-packages.txt are also matched against a copy of each file with its
\uXXXX, \u{...} and \xNN escapes decoded, because one loader family is
written entirely in them. Add an indicator as it reads once decoded, never in
escapes. filenames.txt is extended regex matched against the full path.
The classification rule
Put an indicator in strong.txt only if a match means infection with no
plausible alternative. If a legitimate project could contain the same string,
it belongs in weak.txt.
A false positive in strong.txt costs every user of this repository a panic. A
miss in weak.txt costs one line of review output. When the two are close,
choose weak.txt.
Keeping it current
The campaign rotates its signatures, so an indicator set that matched in September will not necessarily match in December. The set is reviewed weekly against the published sources, and every review is recorded, including the weeks where nothing changed, in the indicator reviews.
A scanner that has quietly stopped matching is worse than no scanner, because it returns a clean result somebody believes. The review log is how you check that has not happened.
Self-matching
Any file that detects PolinRider contains PolinRider strings by definition. Your own scanners, this repository, and CI workflows built from it will be flagged by a grep-based scan. That is expected, and the triage filter separates those matches by path.
Last updated:
Edit this page on GitHub