PolinRider indicators of compromise

On this page
  1. The classification rule
  2. Keeping it current
  3. Self-matching

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