{"id":51002115,"url":"https://github.com/yeet-src/poolnarc","last_synced_at":"2026-06-20T15:33:13.437Z","repository":{"id":361894597,"uuid":"1256306861","full_name":"yeet-src/poolnarc","owner":"yeet-src","description":"Behavioral hidden-cryptominer detector for Linux in eBPF — flags processes talking to mining-pool ports while spoofing kernel-thread names. No signatures, no agent, no cloud. CO-RE portable.","archived":false,"fork":false,"pushed_at":"2026-06-01T17:17:43.000Z","size":1637,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-01T19:08:03.373Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://yeet.cx","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yeet-src.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":"audit.js","citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-01T16:48:00.000Z","updated_at":"2026-06-01T17:22:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yeet-src/poolnarc","commit_stats":null,"previous_names":["yeet-src/poolnarc"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/yeet-src/poolnarc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fpoolnarc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fpoolnarc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fpoolnarc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fpoolnarc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yeet-src","download_url":"https://codeload.github.com/yeet-src/poolnarc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fpoolnarc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34576043,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-20T15:33:13.375Z","updated_at":"2026-06-20T15:33:13.431Z","avatar_url":"https://github.com/yeet-src.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# poolnarc\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://yeet.cx\"\u003e\u003cimg src=\"https://img.shields.io/badge/built%20with-yeet.cx-FF5C00\" alt=\"built with yeet.cx\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/eBPF-CO--RE-1E90FF\" alt=\"eBPF CO-RE\"\u003e\n  \u003ca href=\"#requirements\"\u003e\u003cimg src=\"https://img.shields.io/badge/platform-Linux%20%E2%89%A5%205.5-555\" alt=\"platform Linux \u003e= 5.5\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://discord.gg/vQcyYccY9\"\u003e\u003cimg src=\"https://img.shields.io/badge/Discord-join-5865F2?logo=discord\u0026logoColor=white\" alt=\"Discord\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n**Behavioral detector for hidden cryptominers on Linux.** Runs a scan, prints a verdict — no signature DB, no agent, no cloud.\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"assets/poolnarc.gif\" alt=\"poolnarc demo\" width=\"820\"\u003e\u003c/p\u003e\n\npoolnarc watches every outbound TCP connection at the kernel boundary with eBPF, matches destinations against a database of known mining-pool ports, and flags processes whose names lie about what they are. Built on [yeet](https://yeet.cx). Questions and ideas welcome in the [Discord](https://discord.gg/vQcyYccY9).\n\n## Quickstart\n\npoolnarc is an eBPF program, and [yeet](https://yeet.cx) is the runner for it: yeet compiles the program (with `clang`/`bpftool`), loads it with the kernel capabilities it needs, and streams its output — so there's nothing to set up by hand.\n\nInstall yeet once:\n\n```sh\ncurl -fsSL https://yeet.cx | sh\n```\n\nThen run poolnarc straight from GitHub. No clone, no build step — yeet fetches and compiles it for you:\n\n```sh\nyeet run github:yeet-src/poolnarc -- --audit\n```\n\nEverything after `--` is passed to poolnarc itself. `--audit` runs a one-shot scan and prints a verdict; drop it for the live dashboard. On a clean host you'll see:\n\n```\nVERDICT: NO MINING ACTIVITY DETECTED\n```\n\n\u003e The examples below are written as `yeet run main.js`, which assumes a local clone (see [Build from a clone](#build-from-a-clone)). To run without cloning, use `github:yeet-src/poolnarc` anywhere you see `main.js`.\n\n## Use cases\n\n- **Incident triage** — \"is this box mining right now?\" One command, one scan, a yes/no verdict. Nothing lands on the host beyond yeet.\n- **Fleet monitoring** — cron the JSON audit (`--audit --json`) across servers and alert on any non-clean verdict.\n- **Golden-image / CI checks** — scan a freshly provisioned image before it ships to catch a compromised base layer.\n- **Chasing unexplained CPU** — a pegged core with no obvious owner is the classic cryptojacking tell; poolnarc names the process and the pool.\n- **Confirming a suspected compromise** — verify cryptojacking on a live host without pulling it offline or touching the malware on disk.\n\n## Why behavioral, not signatures\n\nMost \"detect cryptominer\" tools hash files against a YARA or ClamAV database. They lag behind whatever's actually being deployed.\n\npoolnarc asks two behavioral questions instead:\n\n1. **Does the process talk to a known mining-pool port?** Stratum on 14444 for Monero, 2020 for Ethereum, the NiceHash range, and about 30 others — all in `render.js`.\n2. **Is the process lying about its identity?** Real kernel threads can't open outbound TCP. If something named `kworker/u4:2` is sending bytes to a Stratum port, it's malware spoofing `comm` via `prctl(PR_SET_NAME)`.\n\nA miner with a brand-new SHA256 still trips this. The behavior is what's invariant.\n\n## Audit mode\n\nOne-shot scan that runs for a fixed window, prints a verdict, and exits. Outputs to stdout, pipe-friendly.\n\n```sh\nyeet run main.js -- --audit                          # 60s scan\nyeet run main.js -- --audit --duration 90            # longer window\nyeet run main.js -- --audit --json | tee out.json    # machine-readable\n```\n\nSample clean output:\n\n```\n════════════════════════════════════════════════════════════════\n  poolnarc audit · behavioral hidden-cryptominer scan\n════════════════════════════════════════════════════════════════\n\n  Scan started: 2026-05-31T14:18:01.234Z\n  Duration:     1m 0s\n\n── Connections observed ────────────────────────────────────────\n  TCP events seen:          847\n  Connections opened:       42\n  Distinct destinations:    18\n  Total bytes ↑/↓:          12MB / 38MB\n\n── Mining pool detection ───────────────────────────────────────\n  High-confidence mining ports:  0\n  Likely Stratum ports:          0\n  Overall:                       ✓ NONE\n\n── Comm-name mimicry detection ─────────────────────────────────\n  Kernel-thread name spoofing:   0\n  System-daemon name spoofing:   0\n  Overall:                       ✓ NO MIMICRY OBSERVED\n\n════════════════════════════════════════════════════════════════\nVERDICT: NO MINING ACTIVITY DETECTED\n════════════════════════════════════════════════════════════════\n```\n\n## Live mode\n\nFor watching activity in real time. Repaints every 200ms.\n\n```sh\nyeet run main.js\n```\n\nThe dashboard:\n\n```\n ▌ POOLNARC · crypto-mining traffic detector ────────────────────────────────────────────────────────────────────\n● LIVE 00:24   3 conn   ▲180KB/s ▼42KB/s   ⛏ 96% mining   ⚠ 1 CRITICAL · 0 susp\n\n  ⚠ HIDDEN MINER ALERTS · process names spoofing kernel threads / daemons ──────────────────────────────────────\n  CRITICAL  pid 8821 kworker/u4:2     → Ethereum Stratum         1.2MB↑ 340KB↓        12s\n\n  MINING ACTIVITY · ⛏ confirmed  ⛏! kernel-thread mimicry  ⛏? daemon mimicry ────────────────────────────────────\n   ⛏! kworker/u4:2     pid 8821    160KB/s   38KB/s    1 pool   12s\n   ⛏  xmrig            pid 4231    18KB/s    4KB/s     1 pool   24s\n\n  POOLS · sorted by current bandwidth · 2.4MB↑ 720KB↓ mining bytes total ────────────────────────────────────────\n   ⛏ 142.93.124.5:2020         Ethereum Stratum    ▲160KB/s ▼38KB/s   1 miner\n   ⛏ 65.21.198.20:14444        Monero Stratum      ▲18KB/s  ▼4KB/s    1 miner\n\n  CONNECTION FEED · opens and closes, newest first ─────────────────────────────────────────────────────────────\n   00:24  ⛏ ● OPEN  10.0.0.12:51932          → 142.93.124.5:2020    pid 8821 kworker/u4:2\n   00:24  ⛏ ● OPEN  10.0.0.12:51931          → 65.21.198.20:14444   pid 4231 xmrig\n─────────────────────────────────────────────────────────────────────────────────────────────────────────────────\n```\n\nHIDDEN MINER ALERTS appears only when there's something to show. Two tiers: CRITICAL when `comm` matches a kernel-thread prefix (kworker, ksoftirqd, swapper, ...), suspicious when it matches a daemon prefix (systemd, dbus, cron, ...).\n\nAnonymize identifying details before sharing a screenshot:\n\n```sh\nyeet run main.js -- --anonymize\n```\n\n## Verify it works\n\n`tests/simulate_attack.sh` launches a fake mining pool on `127.0.0.1:14444` plus a Python process that renames itself to `kworker/u4:2` and pumps Stratum-shaped traffic at the pool. Same `prctl` syscall the Kinsing family uses.\n\n```sh\n# shell 1\nyeet run main.js -- --audit --duration 20\n\n# shell 2\n./tests/simulate_attack.sh\n```\n\nAfter 20 seconds the audit prints:\n\n```\nVERDICT: CRITICAL — hidden cryptominer detected\n  1 process(es) talking to mining pools while spoofing kernel-thread names.\n```\n\nCtrl-C the simulator when you're done. You've verified the detection without touching real malware.\n\n## Evasion paths\n\nThe detector is not magic. These are real ways to defeat it, and you should know them:\n\n- Custom mining pools on non-standard ports. The port DB covers public-pool defaults. A private pool on port 443 looks like HTTPS. The fix is pairing poolnarc with allowlist-based egress filtering.\n- Non-spoofed process names. A miner that calls itself `nginx-worker` doesn't trip mimicry detection. It still shows up in MINING ACTIVITY, just not promoted to CRITICAL.\n- Stratum-over-TLS on port 443. Same problem as above. TLS doesn't matter for the port-based classification; the port choice does.\n- Long-lived connections that started before the scan. poolnarc sees them only when they next transition state (or move bytes). The BYTES path catches active traffic, but a miner with a long-idle socket waiting for the next block won't trigger an OPEN event mid-scan.\n\n## Real-world incidents\n\n- [Kinsing](https://en.wikipedia.org/wiki/Kinsing). Active 2020-present. Spreads through misconfigured Docker / Redis / SaltStack. Renames itself to look like a kernel thread.\n- TeamTNT. 2020-2022 campaigns against cloud Linux. XMRig disguised as system processes.\n- Sysrv-hello. Go mining worm. Same masquerade pattern.\n\nCommon pattern: outbound to public pools + comm camouflage. That's what poolnarc targets.\n\n## Under the hood\n\nThree BPF programs feed one ring buffer:\n\n| program           | hook                          | does what                                           |\n|-------------------|-------------------------------|------------------------------------------------------|\n| `on_set_state`    | `tp_btf/inet_sock_set_state`  | track at ESTABLISHED, reap at CLOSE                  |\n| `on_sendmsg`      | `fentry/tcp_sendmsg`          | tx bytes; pid fixup in app context                   |\n| `on_cleanup_rbuf` | `fentry/tcp_cleanup_rbuf`     | rx bytes; pid fixup in app context                   |\n\nOne HASH map (`conns`, keyed by sock pointer) holds per-conn state and cumulative byte counts. One RINGBUF (256 KiB) carries OPEN, BYTES (delta every 64 KiB), and CLOSE to JS. Uses libbpf BTF relocations; no fixed offsets; CO-RE.\n\nMining intelligence (port DB, mimicry detection, alert ranking, verdict logic) is all JavaScript. Adding a pool port is a one-line patch to `render.js`.\n\n```\nmain.js         entry. dispatches live vs audit, BPF bind + subscribe\nstate.js        connection model, aggregators, mimicry detection\naudit.js        one-shot scan + report (human + JSON)\nrender.js       ANSI, formatters, mining pool port database\ndashboard.js    panels and layout for live mode\n```\n\n## Requirements\n\n- Linux ≥ 5.5 (for `fentry` and `tp_btf`). Debian 13, Ubuntu 22.04+, Fedora 36+, recent Arch.\n- Kernel BTF (`CONFIG_DEBUG_INFO_BTF=y`), default on the above.\n- `CAP_BPF` + `CAP_PERFMON`. yeet handles this.\n- `clang` and `bpftool` for the BPF object. `yeet run` invokes them on first launch.\n\n## Build from a clone\n\nTo build from a local checkout instead:\n\n```sh\ngit clone https://github.com/yeet-src/poolnarc\ncd poolnarc\nmake\nyeet run main.js\n```\n\n`make clean` removes `bin/`. `make distclean` also removes `include/vmlinux.h`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fpoolnarc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyeet-src%2Fpoolnarc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fpoolnarc/lists"}