Running locally
This guide is for two audiences: you, running the tracker on your own machine day to day, and anyone else who wants to stand up their own copy and populate it with their own samples.
These files live in
docs/as Markdown today. They become the published docs section of the Hugo site once it ships, so they are written to work both rendered on the site and read raw on GitHub.
What the pipeline does
samples/ --ingest--> data/c2db.json --enrich--> whois cache + DNS
|
Hugo site --> GitHub Pages
- ingest walks
samples/, identifies the malware family, extracts the C2 configuration, and records one row per (family, indicator, sample) indata/c2db.jsonwith first/last-seen dates. - enrich queries RDAP (the JSON successor to WHOIS) and DNS for every
unique indicator, caching results under
data/whois/so repeated runs are instant and registry rate limits are respected. - refresh is the daily maintenance pass: re-resolve every domain and re-query Whois for entries older than 7 days. This is what catches “domain changed registrar”, “C2 went dark”, “IP reassigned to another network” โ the changes that are themselves intelligence.
- The Hugo site (separate step) renders
data/c2db.json+ the whois cache into the public tracker.
Setup
Requirements: Python 3.10+ and Git. Windows, macOS and Linux all work.
git clone https://github.com/<your-user>/C2-Tracker.git
cd C2-Tracker
pip install -r requirements.txt
No other services, databases or API keys are required for the core loop. RDAP and DNS are queried directly; everything else is local files.
First seen dates come from MalwareBazaar (when the sample was
first seen in the wild) or from manual backfill, never from the day you
ran the ingest. The Bazaar lookup needs a free abuse.ch API key; you can
also set dates yourself from any source you trust
(python -m tracker backfill <sha256> --first-seen YYYY-MM-DD):
-
Create an account at https://bazaar.abuse.ch/login/ and copy your API key from https://bazaar.abuse.ch/api/.
-
Set it as an environment variable before ingesting:
# PowerShell (persistent, per-user) [Environment]::SetEnvironmentVariable("BAZAAR_AUTH_KEY", "your-key", "User") # Git Bash (current session) export BAZAAR_AUTH_KEY=your-key
Without the key the tracker still works โ dates fall back to the ingest
day, and you can fix them later with python -m tracker backfill-seen.
Do not commit samples. The
samples/directory is git-ignored and must stay that way โ pushing malware to a public repository gets it flagged and banned. Only extracted data ever leaves your machine.
Daily workflow
# 1. drop new samples into samples/ (APK today; PE/ELF decoders land in phase 2)
python -m tracker ingest
# 2. fetch whois + DNS for anything new or stale
python -m tracker enrich
# 3. look at what you have
python -m tracker stats
ingest is safe to re-run: already-known (family, indicator, sample)
combinations are recognised and merged, not duplicated.
The daily refresh (automatic)
python -m tracker refresh is designed to be scheduled:
- Windows: registered as C2Tracker Daily Refresh (08:17 daily) via
Task Scheduler. Check with
schtasks /query /tn "C2Tracker Daily Refresh". - Linux/macOS: add a cron entry, e.g. at 08:17 daily โ
17 8 * * * cd /path/to/c2tracker && python -m tracker refresh --quiet >> refresh.log 2>&1
Data layout
| Path | Contents | Committed? |
|---|---|---|
data/c2db.json |
one record per family/indicator/sample, with enrichment | yes |
data/whois/*.json |
cached RDAP per unique indicator | yes |
samples/ |
your malware samples | never |
Committing the whois cache is deliberate: the published site (and any CI run) renders entirely from committed files and never needs network access to registries.
Adding a decoder
Decoders live in tracker/decoders/<platform>/. A decoder is a class with
two methods:
class MyFamilyDecoder(AndroidDecoder):
family = "MyFamily"
def match(self, apk) -> bool:
# cheap family identification (package name, marker strings)
def extract(self, apk) -> ExtractedConfig:
# return ExtractedConfig(family=..., c2=["host:port"], ports=[...], extras={...})
Register it with one line in the platform’s __init__.py
(ANDROID_DECODERS.append(MyFamilyDecoder())). Windows (RATDecoders-style)
and IoT (ELF/Mirai-family) slots exist as stubs with the same interface.
Troubleshooting
no decoder matchedโ the sample is a valid APK but no family’s matcher accepted it. Repacked variants often change package names; widen thematch()accordingly (and send the hash upstream).- Whois shows only an error key โ the registry RDAP server was unreachable or rate-limited; the cache keeps the last good data and the next refresh retries.
- Domain doesn’t resolve โ many C2s are dead by the time you analyse them. That is recorded (the domain stops resolving), which is expected, not a failure.