Breaking change. Tool name, binary name, function/type names,
constant names, env vars, header guards, file paths, and GitHub
repo URL all rebrand IAMROOT → SKELETONKEY.
Changes:
- All "IAMROOT" → "SKELETONKEY" (constants, env vars, enum
values, docs, comments)
- All "iamroot" → "skeletonkey" (functions, types, paths, CLI)
- iamroot.c → skeletonkey.c
- modules/*/iamroot_modules.{c,h} → modules/*/skeletonkey_modules.{c,h}
- tools/iamroot-fleet-scan.sh → tools/skeletonkey-fleet-scan.sh
- Binary "iamroot" → "skeletonkey"
- GitHub URL KaraZajac/IAMROOT → KaraZajac/SKELETONKEY
- .gitignore now expects build output named "skeletonkey"
- /tmp/iamroot-* tmpfiles → /tmp/skeletonkey-*
- Env vars IAMROOT_MODPROBE_PATH etc. → SKELETONKEY_*
New ASCII skeleton-key banner (horizontal key icon + ANSI Shadow
SKELETONKEY block letters) replaces the IAMROOT banner in
skeletonkey.c and README.md.
VERSION: 0.3.1 → 0.4.0 (breaking).
Build clean on Debian 6.12.86. `skeletonkey --version` → 0.4.0.
All 24 modules still register; no functional code changes — pure
rename + banner refresh.
6.0 KiB
SKELETONKEY for defenders
SKELETONKEY is dual-use: the same binary that runs exploits also ships the detection rules to spot them. This document is for the blue team.
TL;DR
# 1. Detect what you're vulnerable to (no system modification)
sudo skeletonkey --scan --json | jq .
# 2. Deploy detection rules covering every bundled CVE
sudo skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
sudo systemctl restart auditd
# 3. (Optional) Apply pre-patch mitigations for vulnerable families
sudo skeletonkey --mitigate copy_fail # or whatever module reports VULNERABLE
# 4. Watch
sudo ausearch -k skeletonkey-copy-fail -ts recent
sudo ausearch -k skeletonkey-dirty-pipe -ts recent
sudo ausearch -k skeletonkey-pwnkit -ts recent
Why a single tool for offense and defense
Public LPE PoCs ship without detection rules. Public detection rules ship without test corpora. The gap means defenders deploy rules they never validate against a real exploit, and attackers iterate against defenders who haven't tuned thresholds. SKELETONKEY closes that loop:
- Each module ships an exploit AND the detection rules that catch it.
- Every CVE in
CVES.mdhas a row in the rule corpus. - New CVEs we add ship both halves — there's no "rule lag" between an exploit landing in the bundle and the rule being available.
- Detection-rule tests live in CI alongside exploit tests (Phase 4 followup).
Operations cheat sheet
Inventory what's bundled
skeletonkey --list
Prints every registered module with CVE, family, and one-line summary.
Run all detectors
skeletonkey --scan # human-readable
skeletonkey --scan --json # one JSON object → SIEM ingest
skeletonkey --scan --json | jq '.modules[] | select(.result == "VULNERABLE")'
Result codes per module:
| Result | Meaning | Exit code |
|---|---|---|
OK |
Not vulnerable (patched, immune, or N/A) | 0 |
VULNERABLE |
Detect confirmed vulnerable | 2 |
PRECOND_FAIL |
Preconditions missing (module/feature not installed) | 4 |
TEST_ERROR |
Probe could not run (permissions, missing tools, etc.) | 1 |
skeletonkey --scan returns the WORST result code across all modules.
Use this in CI to fail builds that produce vulnerable images.
Deploy detection rules
# auditd (most environments)
sudo skeletonkey --detect-rules --format=auditd \
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
sudo augenrules --load # or systemctl restart auditd
# Sigma (for SIEMs that ingest sigma)
skeletonkey --detect-rules --format=sigma > /etc/falco/skeletonkey.sigma.yml
# YARA / Falco — placeholders for future modules; currently empty
skeletonkey --detect-rules --format=yara
skeletonkey --detect-rules --format=falco
Rules are emitted in registry order, deduplicated by string-pointer:
family-shared rule sets emit once with a "see family rules above"
marker on siblings (no duplicate -w /etc/passwd lines hitting your
auditd config).
Audit keys to watch
| Key | Modules | What it catches |
|---|---|---|
skeletonkey-copy-fail |
copy_fail, copy_fail_gcm, dirty_frag_esp{,6}, dirty_frag_rxrpc | Writes to passwd/shadow/sudoers/su |
skeletonkey-copy-fail-afalg |
copy_fail family | AF_ALG socket creation (kernel crypto API used by exploit) |
skeletonkey-copy-fail-xfrm |
copy_fail family | xfrm setsockopt (Dirty Frag ESP variants) |
skeletonkey-dirty-pipe |
dirty_pipe | Same target files; complements copy-fail watches |
skeletonkey-dirty-pipe-splice |
dirty_pipe | splice() syscalls (the bug's primitive) |
skeletonkey-pwnkit |
pwnkit | pkexec watch |
skeletonkey-pwnkit-execve |
pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 |
Search:
sudo ausearch -k skeletonkey-copy-fail -ts today
sudo ausearch -k skeletonkey-pwnkit -ts today
Mitigate (pre-patch)
For families with mitigations available, --mitigate <name> applies
distro-portable workarounds:
# Currently: copy_fail_family — blacklists algif_aead/esp4/esp6/rxrpc,
# sets kernel.apparmor_restrict_unprivileged_userns=1, drops caches.
sudo skeletonkey --mitigate copy_fail
# Revert mitigation (e.g., before applying the real kernel patch)
sudo skeletonkey --cleanup copy_fail
Modules without --mitigate (dirty_pipe, entrybleed, pwnkit) report
that the only real fix is upgrading the affected component. We don't
ship a half-baked mitigation when the real one is a package update.
Fleet scanning
The --scan --json output is one-line-per-host friendly:
# scan a host list via ssh
for h in $(cat fleet.txt); do
ssh $h sudo skeletonkey --scan --json | jq --arg h "$h" '. + {host: $h}'
done | jq -s . > fleet-scan-$(date +%F).json
# group by vulnerability
jq '.[] | {host, vulns: .modules | map(select(.result == "VULNERABLE")) | map(.cve)}' \
fleet-scan-*.json
For very large fleets, deploy the binary as a one-shot under a remote shell tool (Ansible/SaltStack/Fabric/etc.) and aggregate JSON output into your SIEM. Each scan is a few seconds of CPU and no system modification.
Known false positives
| Rule | False-positive shape |
|---|---|
skeletonkey-copy-fail-afalg |
strongSwan and IPsec daemons use AF_ALG legitimately — scope with -F auid= to exclude service accounts |
skeletonkey-dirty-pipe-splice |
nginx, HAProxy, kTLS use splice() heavily — scope with -F gid!=33 -F gid!=99 for those service accounts |
skeletonkey-pwnkit-execve |
gnome-software, polkit's own dispatcher legitimately exec pkexec — scope by parent process if you can correlate |
The shipped rules are starting points. Tune per environment.
Submitting new detections
If you find a detection signature for a CVE we already bundle, file an
issue. We'll integrate the rule into the relevant module's
detect_* field and ship it on the next release. New CVEs accept
contributions per docs/ARCHITECTURE.md's "adding a new CVE" flow —
each new module ships its own detection rules from day one.