main
115 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ea1744e6f0 |
tests: detect() unit harness with mocked ctx->host
Adds tests/test_detect.c — a standalone harness that constructs synthetic struct skeletonkey_host fingerprints (vulnerable / patched / specific-gate-closed) and asserts each migrated module's detect() returns the expected verdict. First real test coverage for the corpus; catches regressions in the host-fingerprint-consuming logic. Initial coverage — 8 deterministic cases across the 4 modules that already consume ctx->host: - dirtydecrypt: 3 cases verifying 'kernel < 7.0 -> predates the bug' short-circuit on synthetic 6.12 / 6.14 / 6.8 hosts. - fragnesia: unprivileged_userns_allowed=false -> PRECOND_FAIL. - pack2theroot: is_debian_family=false -> PRECOND_FAIL. - pack2theroot: has_dbus_system=false -> PRECOND_FAIL. - overlayfs: distro=debian / distro=fedora -> 'not Ubuntu' -> OK. Coverage grows automatically as more modules migrate to ctx->host (task #12 below adds them). Each new module that consults the host fingerprint can have its precondition gates tested with a one-line EXPECT_DETECT call against a pre-built fingerprint. Wiring: - Makefile: new MODULE_OBJS var consolidates the module .o list so both the main binary and the test binary can share it without duplication. New TEST_BIN := skeletonkey-test target. 'make test' builds and runs the suite. - .github/workflows/build.yml: install libglib2.0-dev + pkg-config so pack2theroot builds with GLib in CI (was previously stub-compiling). New 'tests — detect() unit suite' step runs 'make test' as a non-root user so modules' 'already root' gates don't short-circuit before the synthetic host checks fire. - Test harness compiles cross-platform but assertions are #ifdef __linux__ guarded (on non-Linux all module detect() bodies stub-out to PRECOND_FAIL, making assertions tautological); macOS dev build reports 'skipped'. Module change: - pack2theroot p2tr_detect now consults ctx->host->is_root (with a geteuid() fallback when ctx->host is null) instead of calling geteuid() directly. Production behaviour is identical (host->is_root is populated from geteuid() at startup); tests can now construct non-root fingerprints regardless of the test process's actual euid. Exposed a real consistency issue worth fixing. Verified in docker as non-root: 8/8 pass on Linux. macOS reports 'skipped' as designed. |
||
|
|
c00c3b463a |
dispatcher: per-detect timeout + exploit() fork-isolation
Two reliability improvements that make --auto survive any misbehaving
module: a 15s timeout on detect() so a hung probe can't stall the
scan, and fork-isolation around exploit/mitigate/cleanup so a
crashing callback doesn't take down --auto's fallback path.
Detect timeout:
- New SKELETONKEY_DETECT_TIMEOUT_SECS = 15.
- run_detect_isolated() forked child now calls alarm(15); if detect()
hangs, SIGALRM kills the child. Parent observes WIFSIGNALED with
signal SIGALRM and reports 'detect() timed out (signal 14)' in the
verdict table.
- cmd_auto distinguishes timeout vs other crash in the scan-summary
callout: separate n_timeout counter and dedicated [!] line.
Exploit fork-isolation:
- New run_callback_isolated() wraps exploit() / mitigate() / cleanup()
in a forked child. Two crash-safety properties:
* A SIGSEGV/SIGILL in the callback is contained; --auto continues
to the next-safest candidate via its existing fallback list.
* The dispatcher itself can't be killed by a misbehaving exploit.
- Result-code communication is via a one-byte pipe with FD_CLOEXEC on
the write end:
* Callback returns normally -> child writes result byte, _exit;
parent reads it; trusted result.
* Callback execve()s a target -> FD_CLOEXEC closes the write end
during the exec transition;
parent's read() gets EOF; we treat
exec-then-exit as EXPLOIT_OK
regardless of the shell's exit
code (we DID land code execution).
* Callback crashes -> WIFSIGNALED true; report the
signal and propagate EXPLOIT_FAIL.
- cmd_auto: exploit() crash now logged distinctly ('[!] X exploit
crashed (signal N) — dispatcher recovered'). Exec-path is
surfaced too ('[*] X exploit transferred to spawned target — ...').
- cmd_one: same wrapping, same crash/exec reporting for the
--exploit/--mitigate/--cleanup single-module paths.
Both platforms build clean. Verified containment behavior on Linux
in docker: entrybleed's prefetchnta SIGILL still reports cleanly as
'detect() crashed (signal 4) — continuing' and the scan finishes
through all 31 modules to the summary + pick step.
|
||
|
|
4f30d00a1c |
core/host: shared host fingerprint refactor
Adds core/host.{h,c} — a single struct skeletonkey_host populated once
at startup and handed to every module callback via ctx->host. Replaces
the per-detect uname / /etc/os-release / sysctl / userns-fork-probe
calls scattered across the corpus with O(1) cached lookups, and gives
the dispatcher one consistent view of the host.
What's in the fingerprint:
- Identity: kernel_version (parsed from uname.release), arch (machine),
nodename, distro_id / distro_version_id / distro_pretty (parsed once
from /etc/os-release).
- Process state: euid, real_uid (defeats userns illusion via
/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 (file-existence checks once).
- Capability gates (Linux): unprivileged_userns_allowed (live
fork+unshare probe), apparmor_restrict_userns,
unprivileged_bpf_disabled, kpti_enabled, kernel_lockdown_active,
selinux_enforcing, yama_ptrace_restricted.
- System services: has_systemd, has_dbus_system.
Wiring:
- core/module.h forward-declares struct skeletonkey_host and adds the
pointer to skeletonkey_ctx. Modules opt-in by including
../../core/host.h.
- core/host.c is fully POD (no heap pointers) — uses a single file-
static instance, returns a stable pointer on every call. Lazily
populated on first skeletonkey_host_get().
- skeletonkey.c calls skeletonkey_host_get() at main() entry, stores
in ctx.host before any register_*() runs.
- cmd_auto's bespoke distro-fingerprint code (was an inline
read_os_release helper) is replaced with skeletonkey_host_print_banner(),
which emits a two-line banner of identity + capability gates.
Migrations:
- dirtydecrypt: kernel_version_current() -> ctx->host->kernel.
- fragnesia: removed local fg_userns_allowed() fork-probe in favour of
ctx->host->unprivileged_userns_allowed (no per-scan fork). Also
pulls kernel from ctx->host. The PRECOND_FAIL message now notes
whether AppArmor restriction is on.
- pack2theroot: access('/etc/debian_version') -> ctx->host->is_debian_family;
also short-circuits when ctx->host->has_dbus_system is false (saves
the GLib g_bus_get_sync attempt on systems without system D-Bus).
- overlayfs: replaced the inline is_ubuntu() /etc/os-release parser
with ctx->host->distro_id comparison. Local helper preserved for
symmetry / standalone builds.
Documentation: docs/ARCHITECTURE.md gains a 'Host fingerprint'
section describing the struct, the opt-in include pattern, and
example detect() usage. ROADMAP --auto accuracy log notes the
landing and flags remaining modules as an incremental follow-up.
Build verification:
- macOS (local): make clean && make -> Mach-O x86_64, 31 modules,
banner prints with distro=?/? (no /etc/os-release).
- Linux (docker gcc:latest + libglib2.0-dev): make clean && make ->
ELF 64-bit, 31 modules. Banner prints with kernel + distro=debian/13
+ 7 capability gates. dirtydecrypt correctly says 'predates the
rxgk code added in 7.0'; fragnesia PRECOND_FAILs with
'(host fingerprint)' annotation; pack2theroot PRECOND_FAILs on
no-DBus; overlayfs reports 'not Ubuntu (distro=debian)'.
|
||
|
|
3e6e0d869b |
skeletonkey: add --dry-run flag
Preview-only mode for --auto / --exploit / --mitigate / --cleanup.
Walks the full scan (with active probes, fork isolation, verdict
table — everything the real --auto does) and prints what would be
launched, without ever calling the exploit/mitigate/cleanup callback.
Wiring:
- struct skeletonkey_ctx gains a 'dry_run' field (core/module.h).
- Long option --dry-run, getopt case 10.
- cmd_auto: after picking the safest, if dry_run, print
[*] auto: --dry-run: would launch `--exploit <NAME> --i-know`; not firing.
plus the remaining ranked candidates, then return 0.
- cmd_one (used for --exploit/--mitigate/--cleanup) shorts on dry_run
with [*] <module>: --dry-run: would run --<op>; not firing.
UX: --auto --dry-run does NOT require --i-know (nothing fires). The
refusal message for bare --auto now points to --dry-run for the
preview path:
[-] --auto requires --i-know (or --dry-run for a preview that never fires).
ROADMAP --auto accuracy section updated with the dry-run + the
version-pinned detect work from the previous commit.
Smoke-tested locally on macOS: scanning runs, verdicts print, the
'would launch' line fires, exit 0.
|
||
|
|
a26f471ecf |
dirtydecrypt + fragnesia: pin CVE fix commits, version-based detect()
Both modules' detect() was precondition-only because we didn't know the
mainline fix commits at port time. Debian's security tracker now
provides them — pinning here turns detect() into a proper version-
based verdict (still with --active for empirical override).
dirtydecrypt (CVE-2026-31635):
- Fix commit a2567217ade970ecc458144b6be469bc015b23e5 in mainline 7.0
('rxrpc: fix oversized RESPONSE authenticator length check').
- Debian tracker confirms older stable branches (5.10 / 6.1 / 6.12) as
<not-affected, vulnerable code not present>: the rxgk RESPONSE-
handling code was added in 7.0.
- kernel_range table: { {7, 0, 0} }
- detect() pre-checks 'kernel < 7.0 -> SKELETONKEY_OK (predates)' then
consults the table. With --active, the /tmp sentinel probe overrides
empirically (catches pre-fix 7.0-rc kernels the version check
reports as patched).
fragnesia (CVE-2026-46300):
- Fix in mainline 7.0.9 per Debian tracker ('linux unstable: 7.0.9-1
fixed'). Older Debian-stable branches (bullseye 5.10 / bookworm 6.1
/ trixie 6.12) are still marked vulnerable as of 2026-05-22 - no
backports yet.
- kernel_range table: { {7, 0, 9} }
- detect() keeps the userns + carrier preconditions, then consults
the table: 7.0.9+ -> OK; older branches without an explicit backport
entry -> VULNERABLE (version-only). --active confirms empirically.
- Table is intentionally minimal so distros that DO backport in the
future flow into 'patched' once their branch lands an entry; until
then, the conservative VULNERABLE verdict on unfixed branches is
correct.
Other changes:
- module struct .kernel_range strings updated from 'fix commit not
yet pinned' to the actual pinned-version prose.
- module_safety_rank bumped 86 -> 87 for both modules (version-pinned
detect is now real; still below the verified copy_fail family at
88 so --auto prefers verified modules when both apply).
- Both modules now #include core/kernel_range.h inside their
#ifdef __linux__ block.
- MODULE.md verification-status sections rewritten: detect() is now
version-pinned; only the exploit body remains unverified.
- CVES.md note + inventory rows updated: dropped the 'precondition-
only' language for the pair; all three ported modules now have
pinned fix references.
- README ⚪ tier description + module list aligned to the new state.
Both detect()s smoke-tested in docker gcc:latest on kernel 6.12.76-
linuxkit: dirtydecrypt correctly reports OK ('predates the rxgk code
added in 7.0'); fragnesia + pack2theroot correctly report
PRECOND_FAIL (no userns / no D-Bus in container). Local macOS + Linux
builds both clean.
|
||
|
|
cdb8f5e8f9 |
all modules: wrap Linux-only code in #ifdef __linux__ — full macOS build works
Every kernel-LPE module that uses Linux-only headers (splice, posix_fadvise,
linux/netlink.h, sys/ptrace.h, etc.) now follows the same #ifdef __linux__
pattern the new modules already used: Linux body in the ifdef, stub
detect/exploit/cleanup returning SKELETONKEY_PRECOND_FAIL on non-Linux,
platform-neutral rule strings + module struct + register fn left outside.
14 modules wrapped:
dirty_pipe (already done above), af_packet, af_packet2,
cgroup_release_agent, cls_route4, dirty_cow, fuse_legacy,
netfilter_xtcompat, nf_tables, nft_fwd_dup, nft_payload,
overlayfs, overlayfs_setuid, ptrace_traceme.
Several modules previously had ad-hoc partial stubs (af_packet2 faked
SIOCSIFFLAGS/MAP_LOCKED, netfilter_xtcompat faked sysv-msg syscalls,
the nft_* modules had 3 partial __linux__ islands each, fuse_legacy /
nf_tables had inner-only ifdef blocks) — all replaced with the uniform
outer-wrap shape from dirty_pipe / dirtydecrypt / fragnesia / pack2theroot.
Where a module includes core/kernel_range.h, core/finisher.h, or
core/offsets.h, those are now inside the ifdef block as well — silences
clangd's "unused-includes" LSP warning on macOS while keeping them
present for the real Linux build.
No exploit logic, constant, struct, shellcode byte, or rule string was
modified — only include placement and ifdef markers.
Build verification:
macOS (local): make clean && make → Mach-O x86_64, 31 modules
registered, --scan reports each Linux-only module as
"Linux-only module — not applicable here".
Linux (docker gcc:latest + libglib2.0-dev): make clean && make →
ELF 64-bit, 31 modules. Exploit code paths unchanged.
|
||
|
|
9a4cc91619 |
pack2theroot (CVE-2026-41651) + --auto accuracy work
Adds the third ported module — Pack2TheRoot, a userspace PackageKit
D-Bus TOCTOU LPE — and spends real effort hardening --auto so its
detect step gives an accurate, robust verdict before deploying.
pack2theroot (CVE-2026-41651):
- Ported from the public Vozec PoC
(github.com/Vozec/CVE-2026-41651). Original disclosure by the
Deutsche Telekom security team.
- Two back-to-back InstallFiles D-Bus calls (SIMULATE then NONE)
overwrite the cached transaction flags between polkit auth and
dispatch. GLib priority ordering makes the overwrite deterministic,
not a timing race; postinst of the malicious .deb drops a SUID bash
in /tmp.
- detect() reads PackageKit's VersionMajor/Minor/Micro directly over
D-Bus and compares against the pinned fix release 1.3.5 (commit
76cfb675). This is a high-confidence verdict, not precondition-only.
- Debian-family only (PoC builds its own .deb in pure C; ar/ustar/
gzip-stored inline). Cleanup removes /tmp .debs + best-effort
unlinks /tmp/.suid_bash + sudo -n dpkg -r the staging packages.
- Adds an optional GLib/GIO build dependency. The top-level Makefile
autodetects via `pkg-config gio-2.0`; when absent the module
compiles as a stub returning PRECOND_FAIL.
- Embedded auditd + sigma rules cover the file-side footprint
(/tmp/.suid_bash, /tmp/.pk-*.deb, non-root dpkg/apt execve).
--auto accuracy improvements:
- Auto-enables --active before the scan. Per-module sentinel probes
(page-cache /tmp files, fork-isolated namespace mounts) turn
version-only checks into definitive verdicts, so silent distro
backports don't fool the scan and --auto won't pick blind on
TEST_ERROR.
- Per-module verdict printing — every module's result is shown
(VULNERABLE / patched / precondition / indeterminate), not just
VULNERABLE rows. Operator sees the full picture.
- Scan-end summary line: "N vulnerable, M patched/n.a., K
precondition-fail, L indeterminate" with a separate callout when
modules crashed.
- Distro fingerprint added to the auto banner (ID + VERSION_ID from
/etc/os-release alongside kernel/arch).
- Fork-isolated detect() — each detector runs in a child process so
a SIGILL/SIGSEGV in one module's probe is contained and the scan
continues. Surfaced live while testing: entrybleed's prefetchnta
KASLR sweep SIGILLs on emulated CPUs (linuxkit on darwin); without
isolation the whole --auto died at module 7 of 31. With isolation
the scan reports "detect() crashed (signal 4) — continuing" and
finishes cleanly.
module_safety_rank additions:
- pack2theroot: 95 (userspace D-Bus TOCTOU; dpkg + /tmp SUID footprint
— clean but heavier than pwnkit's gconv-modules-only path).
- dirtydecrypt / fragnesia: 86 (page-cache writes; one step below the
verified copy_fail/dirty_frag family at 88 to prefer verified
modules when both apply).
Docs:
- README badge / tagline / tier table / ⚪ block / example output /
v0.5.0 status — all updated to "28 verified + 3 ported".
- CVES.md counts line, the ported-modules note (now calling out
pack2theroot's high-confidence detect vs. precondition-only for
the page-cache pair), inventory row, operations table row.
- ROADMAP Phase 7+: pack2theroot moved out of carry-overs into the
"landed (ported, pending VM verification)" group; added a new
"--auto accuracy work" subsection documenting the dispatcher
hardening landed in this commit.
- docs/index.html: scanning-count example bumped to 31, status line
updated to mention 3 ported modules.
Build verification: full `make clean && make` in `docker gcc:latest`
with libglib2.0-dev installed: links into a 31-module skeletonkey
ELF (413KB), `--list` shows all modules including pack2theroot,
`--detect-rules --format=auditd` emits the new pack2theroot section,
`--auto --i-know --no-shell` exercises the new banner + active
probes + verdict table + fork isolation + scan summary end-to-end.
Only build warning is the pre-existing
`-Wunterminated-string-initialization` in dirty_pipe (not introduced
here).
|
||
|
|
ac557b67d0 |
review pass: fidelity + credits + count consistency for ported modules
Three-agent rigorous review of the dirtydecrypt + fragnesia ports plus
repo-wide doc consistency, followed by a full Linux build verification.
dirtydecrypt (NOTICE + detection rules):
- NOTICE.md: removed an unsupported "Zellic co-founder" detail and a
fabricated disclosure-date narrative; tightened phrasing of the
Zellic + V12 credit; noted that upstream poc.c carries no
author/license header of its own.
- Embedded auditd + sigma rules and detect/sigma.yml broadened to
cover every binary in dd_targets[] (added /usr/bin/mount,
/usr/bin/passwd, /usr/bin/chsh) and added the b32 splice rule, so
the embedded ruleset matches the on-disk reference and the carrier
list the exploit actually targets.
- Exploit primitive verified byte-for-byte against the V12 PoC
(tiny_elf[] identical, all rxgk/XDR/fire/pagecache_write logic
token-identical). docker gcc:latest compile of the Linux path:
COMPILE_OK, zero warnings.
fragnesia: review found no defects. Exploit primitive byte-identical
to the V12 PoC (shell_elf[] 192 bytes identical, AF_ALG GCM keystream
table + userns/netns/XFRM + receiver/sender/run_trigger_pair all
faithful). The deliberate omissions (ANSI TUI, CLI arg parsing) drop
nothing exploit-critical. docker gcc:latest compile: COMPILE_OK; full
project build links into a working skeletonkey ELF and --list shows
the module registered correctly.
Repo docs (README.md / CVES.md / ROADMAP.md):
- Chose to keep "28 verified" as the headline; the two ported
modules are represented as a separate clearly-labelled tier
("ported-but-unverified") that is explicitly excluded from the
28-module verified counts. README + CVES.md + ROADMAP.md now tell
one consistent story.
- Filled a pre-existing documentation gap: sudo_samedit, sequoia,
sudoedit_editor, vmwgfx were registered + built but absent from
CVES.md's inventory + operations tables. Added rows synthesized
from each module's .cve / .summary / .kernel_range fields.
- ROADMAP Phase 8 "7 🟡 PRIMITIVE modules" → "14"; added a "Landed
since v0.1.0" group; moved vmwgfx out of the stale carry-overs.
docs site (docs/index.html):
- Stat box "28 / total modules" → "28 / verified modules" (the 14+14
breakdown now sums to the headline consistently).
- Terminal example "scanning 28 modules" → "scanning 30 modules"
(was factually wrong — the binary literally prints module_count()
which is 30).
- Status line: updated to mention the 2 ported-but-unverified
modules and mirror the README phrasing.
- docs/LAUNCH.md left as a dated v0.5.0 launch snapshot.
Build verification: `docker run gcc:latest make clean && make` —
links into a 30-module skeletonkey ELF on Linux. macOS dev box still
hits the pre-existing dirty_pipe header gap; unchanged.
.gitignore: added /skeletonkey to exclude the top-level build
artifact (the existing modules/*/skeletonkey only covered per-module
binaries; the root one was getting picked up by `git add -A`).
|
||
|
|
a8c8d5ef1f |
modules: add dirtydecrypt (CVE-2026-31635) + fragnesia (CVE-2026-46300)
Two new page-cache-write LPE modules, both ported from the public V12 security PoCs (github.com/v12-security/pocs): - dirtydecrypt (CVE-2026-31635): rxgk missing-COW in-place decrypt. rxgk_decrypt_skb() decrypts spliced page-cache pages before the HMAC check, corrupting the page cache of a read-only file. Sibling of Copy Fail / Dirty Frag in the rxrpc subsystem. - fragnesia (CVE-2026-46300): XFRM ESP-in-TCP skb_try_coalesce() loses the SHARED_FRAG marker, so the ESP-in-TCP receive path decrypts page-cache pages in place. A latent bug exposed by the Dirty Frag fix (f4c50a4034e6). Retires the old _stubs/fragnesia_TBD stub. Both wrap the PoC exploit primitive in the skeletonkey_module interface: detect/exploit/cleanup, an --active /tmp sentinel probe, --no-shell support, and embedded auditd + sigma rules. The exploit body runs in a forked child so the PoC's exit()/die() paths cannot tear down the dispatcher. The fragnesia port drops the upstream PoC's ANSI TUI (incompatible with a shared dispatcher); the exploit mechanism is reproduced faithfully. Linux-only code is guarded with #ifdef __linux__ so the modules still compile on non-Linux dev boxes. VERIFICATION: ported, NOT yet validated end-to-end on a vulnerable-kernel VM. The CVE fix commits are not pinned, so detect() is precondition-only (PRECOND_FAIL / TEST_ERROR, never a blind VULNERABLE) and --auto will not fire them unless --active confirms. macOS stub-path compiles verified locally; the Linux exploit-path build is covered by CI (build.yml, ubuntu) only. See each MODULE.md. Wiring: core/registry.h, skeletonkey.c, Makefile, CVES.md, ROADMAP.md. |
||
|
|
3b287f84f0 |
copy_fail_family: skip DIRTYFAIL typed prompt under --i-know
The vendored DIRTYFAIL exploits call typed_confirm("DIRTYFAIL"), which
reads stdin interactively. SKELETONKEY already gates --exploit/--auto
behind --i-know, so the prompt is redundant and deadlocks non-interactive
runs like `skeletonkey --auto --i-know`.
Add a dirtyfail_assume_yes flag, forwarded from skeletonkey_ctx.authorized
by the bridge layer's apply_ctx(). When set, typed_confirm() auto-satisfies
its gate and logs that it did so.
The YES_BREAK_SSH self-lockout guard is exempt — it protects the
operator's own access rather than gating authorization, so it still
requires an interactive answer.
Standalone DIRTYFAIL builds are unchanged: the flag defaults false.
|
||
|
|
33f81aeb69 |
site: revert CVE table → pill grid
The sortable table was denser but lost the visual scan-ability of the color-coded pill grid. Restoring the pill view: two grouped sections (🟢 / 🟡) each showing every module name as a pill. Drops the table-sort JS (~25 lines) and the .cve-table CSS block. |
||
|
|
5be3c46719 |
CONTRIBUTING: fix stale IAMROOT_EXPLOIT_OK → SKELETONKEY_EXPLOIT_OK
Two references missed during the IAMROOT → SKELETONKEY rename in v0.4.0. The enum value in core/module.h is SKELETONKEY_EXPLOIT_OK. |
||
|
|
58fb2e0951 |
site: simplify nav + add sortable CVE chart
nav: removed Releases / CVEs / Defenders links — kept only a
right-aligned GitHub link with the Octocat SVG icon.
index.html: replaced pill-grid corpus view with a proper sortable
table — Year, CVE, Bug, Module, Tier columns. Click headers to
sort. Defaults to Year descending. 28 rows covering 2016 → 2026.
style.css: added .nav-github (border-pill style) + table styles
(sortable headers with arrow indicators, hover rows, mobile-
responsive font-size + overflow-x scroll).
JS for sort is ~25 lines vanilla — no library.
|
||
|
|
2904fa159c |
site: GitHub Pages landing page
Single-page static site under /docs/, served by GitHub Pages from
the main branch /docs source.
docs/index.html: hero with one-liner + copy button, why-this-exists,
corpus stats + module pills (14 🟢 + 14 🟡), audience cards
(red/blue/sysadmin/CTF), terminal-shape worked example,
verified-vs-claimed bar, quickstart commands, status, footer.
docs/style.css: dark theme matching GitHub's color palette
(#0d1117 bg, #c9d1d9 text). System sans for prose, ui-monospace
for code. Mobile-responsive with grid breakpoints. No JS framework,
no external fonts, no analytics.
docs/.nojekyll: disable Jekyll so the static HTML is served
verbatim and the existing /docs/*.md files stay as raw markdown
(viewable via GitHub UI, not the Pages site).
|
||
|
|
2873133852 |
README: polish — accurate counts, audience table, corpus glance
Module counts were stale: 13 🟢 + 11 🟡 → corrected to 14 🟢 + 14 🟡 (sudoedit_editor is new 🟢; sudo_samedit + sequoia + vmwgfx are new 🟡 from the v0.5.0 batch). Added 'Who it's for' table — red team / sysadmin / blue team / CTF each get a row. Added 'Corpus at a glance' section with explicit module lists per tier, replacing the prose paragraph that buried the names. Tightened Quickstart — removed duplicate one-liner block, single canonical command set. Worked example switched from fictional dirty_pipe to the actual --auto output shape (pwnkit pick on a vulnerable Ubuntu 5.15). Honest 'Status' framing — acknowledges no empirical end-to-end validation yet, calls it the next roadmap item. Replaces the aspirational 'CI-tested across a distro matrix' claim. Added 'How it works' (was 'Architecture' + 'Build & run' merged into a clearer flow) and 'The verified-vs-claimed bar' section explaining why most modules ship without per-kernel offsets. |
||
|
|
95135213e5 |
launch: README polish + CONTRIBUTING + LAUNCH.md
README.md: badges (release / license / module-count / platform),
sharpened hero stating value prop in one sentence, audience
framing for red team / sysadmin / blue team.
CONTRIBUTING.md (new): what we accept (offsets, modules, detection
rules, bug reports) and what we don't (untested EXPLOIT_OK,
fabricated offsets, 0days, undisclosed CVEs).
docs/LAUNCH.md (new): ~600-word HN/blog launch post. Copy-paste
ready. Explains the verified-vs-claimed bar + --auto + the
operator-populated offset table approach.
GitHub repo description + 11 topics set via gh repo edit so the
repo is discoverable in topic searches (linux-security,
privilege-escalation, cve, redteam, blueteam, etc.).
|
||
|
|
0fbe1b058f |
v0.5.0: --auto mode + sysadmin one-liner
skeletonkey.c: new --auto subcommand. Scans every module's detect(),
filters to VULNERABLE, ranks by safety (structural > page-cache >
userspace > kernel-primitive > race), runs the safest exploit.
Requires --i-know. If the safest fails, suggests next candidates.
README.md: 'One-command root' Quickstart section showing
curl … install.sh | sh && skeletonkey --auto --i-know
— the sysadmin/red-team one-liner.
Status: bumped 0.4.5 → 0.5.0; corpus 24 → 28 modules (4 new in
parallel batch: sudo_samedit, sequoia, sudoedit_editor, vmwgfx).
v0.5.0
|
||
|
|
e13edd0cfd |
modules: add sudo_samedit + sequoia + sudoedit_editor + vmwgfx
sudo_samedit (CVE-2021-3156): Qualys Baron Samedit, userspace heap
overflow in sudoedit -s. Version-range detect; Qualys-style trigger
fork+verify (no per-distro offsets shipped — EXPLOIT_FAIL honest).
sequoia (CVE-2021-33909): Qualys size_t→int wrap in seq_buf_alloc.
Userns reach + 5000-level nested tree + bind-mount amplification +
/proc/self/mountinfo read triggers stack-OOB write. No JIT-spray.
sudoedit_editor (CVE-2023-22809): Synacktiv EDITOR/VISUAL '--' argv
escape. Structural exploit — no offsets. Helper-via-sudoedit
appends 'skel::0:0:' line to /etc/passwd, su to root.
vmwgfx (CVE-2023-2008): DRM buffer-object OOB write in VMware guests.
Detect requires DMI VMware + /dev/dri/cardN vmwgfx driver.
All four refuse cleanly on kctf-mgr (patched 6.12.86 / sudo 1.9.16p2).
|
||
|
|
5a73565e0e |
scaffold: 4 new module dirs (sudo_samedit, sequoia, sudoedit_editor, vmwgfx)
Stubs returning PRECOND_FAIL. Parallel agents fill in real detect/exploit. |
||
|
|
324b539d65 | README: bump Status to v0.4.5 | ||
|
|
e668c3301f |
banner: drop ASCII art, plain text only
Replace the skeleton-key ASCII art with a single-line text banner. Bump 0.4.4 → 0.4.5.v0.4.5 |
||
|
|
347a9af832 |
banner: give the bit actual teeth
Previous staircase pattern was just trailing decoration — not real key teeth. Redesigned the bit as a hanging rectangle with two clearly-projecting notch-teeth on its right edge (the part that engages a lock's wards). Switched to box-drawing chars for the bit since they make sharper notches than 8/b/d glyphs; bow stays ornate-ASCII style. Bump 0.4.3 → 0.4.4.v0.4.4 |
||
|
|
023289a03a |
banner: artwork is the focal point — plain SKELETONKEY text below
Previous banner had a SKELETONKEY block-letter art that competed with the skeleton-key drawing for visual attention. Simplified: the key art is now the focal point, and SKELETONKEY is rendered as plain spaced text below the drawing. Slight refinement to the key art: bow is a bit larger (888 instead of 88) to feel more substantial. Bit/teeth pattern unchanged. Bump 0.4.2 → 0.4.3.v0.4.3 |
||
|
|
e7ced5db7c |
banner: more detailed ornate skeleton key
The v0.4.1 box-drawing key was minimalist — round bow, line shaft,
small bit. Replaced with a more detailed ornate skeleton-key
silhouette in the classic ASCII-art-of-keys tradition:
- Round bow with internal "hole" rendered via stylized 8/b/d/'
pattern (suggests the decorative loop you'd grip)
- Long shaft running right across the banner
- Bit at the end with a staircase notch pattern (the iconic
"key-tooth" descent showing the wards that engage the lock)
Same height as the previous banner. SKELETONKEY block letters
below unchanged.
Bump 0.4.1 → 0.4.2.
v0.4.2
|
||
|
|
b5188b7818 |
banner: redesign skeleton key ASCII art
Replace the previous "circle + shaft + curl" silhouette (which read
more like a magnifying glass) with a proper skeleton-key anatomy:
- BOW: round decorative loop with center hole (the part you hold)
- SHAFT: long horizontal rod (= the body of the key)
- BIT: notched tooth hanging down from the shaft end (the part
that engages the lock — the iconic key-tooth profile)
Same change in skeletonkey.c BANNER and README.md.
Bump 0.4.0 → 0.4.1.
v0.4.1
|
||
|
|
9593d90385 |
rename: IAMROOT → SKELETONKEY across the entire project
Breaking change. Tool name, binary name, function/type names,
constant names, env vars, header guards, file paths, and GitHub
repo URL all rebrand IAMROOT → SKELETONKEY.
Changes:
- All "IAMROOT" → "SKELETONKEY" (constants, env vars, enum
values, docs, comments)
- All "iamroot" → "skeletonkey" (functions, types, paths, CLI)
- iamroot.c → skeletonkey.c
- modules/*/iamroot_modules.{c,h} → modules/*/skeletonkey_modules.{c,h}
- tools/iamroot-fleet-scan.sh → tools/skeletonkey-fleet-scan.sh
- Binary "iamroot" → "skeletonkey"
- GitHub URL KaraZajac/IAMROOT → KaraZajac/SKELETONKEY
- .gitignore now expects build output named "skeletonkey"
- /tmp/iamroot-* tmpfiles → /tmp/skeletonkey-*
- Env vars IAMROOT_MODPROBE_PATH etc. → SKELETONKEY_*
New ASCII skeleton-key banner (horizontal key icon + ANSI Shadow
SKELETONKEY block letters) replaces the IAMROOT banner in
skeletonkey.c and README.md.
VERSION: 0.3.1 → 0.4.0 (breaking).
Build clean on Debian 6.12.86. `skeletonkey --version` → 0.4.0.
All 24 modules still register; no functional code changes — pure
rename + banner refresh.
v0.4.0
|
||
|
|
9d88b475c1 |
v0.3.1: --dump-offsets tool + NOTICE.md per module
The README has been claiming "each module credits the original CVE
reporter and PoC author in its NOTICE.md" since v0.1.0, but only
copy_fail_family actually shipped one. Fixed.
modules/<name>/NOTICE.md (×19 new + 1 existing): per-module
research credit covering CVE ID, discoverer, original advisory
URL where public, upstream fix commit, IAMROOT's role.
iamroot.c: new --dump-offsets subcommand. Resolves kernel offsets
via the existing core/offsets.c four-source chain (env →
/proc/kallsyms → /boot/System.map → embedded table), then emits
a ready-to-paste C struct entry for kernel_table[]. Run once
as root on a target kernel build; upstream via PR. Eliminates
fabricating offsets — every shipped entry traces back to a
`iamroot --dump-offsets` invocation on a real kernel.
docs/OFFSETS.md: documents the --dump-offsets workflow.
CVES.md: notes the NOTICE.md convention + offset dump tool.
iamroot.c: bump IAMROOT_VERSION 0.3.0 → 0.3.1.
v0.3.1
|
||
|
|
1bcfdd0c9f |
release: v0.3.0 — 4 new CVE modules (24 total)
iamroot.c: bump IAMROOT_VERSION 0.2.0 → 0.3.0
CVES.md: add inventory entries for nft_set_uaf, af_unix_gc,
nft_fwd_dup, nft_payload; extend operations table;
bump counts (🟢 13 · 🟡 11 · 🔵 0 · ⚪ 1).
README.md: update Status to 24 modules, list all 11 🟡 modules.
Module families now spanning:
- copy_fail_family (page-cache write)
- nf_tables (4 modules: nf_tables, nft_set_uaf, nft_fwd_dup, nft_payload)
- af_packet (2 modules: af_packet, af_packet2)
- overlayfs (2 modules: overlayfs CVE-2021-3493, overlayfs_setuid)
- af_unix (new in v0.3.0)
- plus 10 single-CVE families
v0.3.0
|
||
|
|
5a808e3583 |
modules: 4 new CVE modules — nft_set_uaf + af_unix_gc + nft_fwd_dup + nft_payload
Each module: detect with branch-backport ranges + userns reach +
hand-rolled trigger + msg_msg cross-cache groom + slabinfo witness
+ /tmp/iamroot-<name>.log breadcrumb + auditd rules + --full-chain
finisher (FALLBACK depth, sentinel-arbitrated).
nft_set_uaf (CVE-2023-32233, +1033): anonymous-set UAF
(Sondej+Krysiuk). 5.1 → 6.4. nfnetlink batch:
NEWTABLE → NEWCHAIN → NEWSET(ANON|EVAL) →
NEWRULE(lookup) → DELSET → DELRULE; cg-512 spray.
af_unix_gc (CVE-2023-4622, +813): GC race UAF (Lin Ma). ~2.0 → 6.5
— widest range of any module. Two-thread race driver
(SCM_RIGHTS cycle vs unix_gc trigger) + kmalloc-512
spray. No userns needed.
nft_fwd_dup (CVE-2022-25636, +1024): nft_fwd_dup_netdev_offload
heap OOB (Aaron Adams). 5.4 → 5.17. NFT_CHAIN_HW_OFFLOAD
chain + 16 immediates + fwd to overrun action.entries[].
nft_payload (CVE-2023-0179, +1136): set-id memory corruption
(Davide Ornaghi). 5.4 → 6.2. NFTA_SET_DESC variable
element + NFTA_SET_ELEM_EXPRESSIONS with payload-set
whose verdict.code drives the regs->data[] OOB.
All 4 honor verified-vs-claimed: trigger fires, primitive grooms, no
fabricated offsets. EXPLOIT_OK only via empirical setuid-bash sentinel.
Build clean on Debian 6.12.86; all 4 refuse cleanly on both default
and --full-chain paths via the existing patched-kernel detect gate.
|
||
|
|
6a0a7d8718 |
scaffold: 4 new module dirs + registry/Makefile wiring (stubs)
Pre-scaffolding for the next batch (CVE-2023-32233, CVE-2023-4622, CVE-2022-25636, CVE-2023-0179). Each module ships as a 21-line stub returning PRECOND_FAIL; parallel agents fill in the real detect/exploit/--full-chain implementations. This commit keeps registry.h / iamroot.c / Makefile in one place so the 4 parallel agents don't collide on shared-file edits — they each own a single iamroot_modules.c. Build clean on Debian 6.12.86; --list shows all 24 modules including the 4 new stubs. |
||
|
|
e2a3d6e94f |
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.v0.2.0 |
||
|
|
c1d1910a90 |
modules: wire --full-chain root-pop into all 7 🟡 PRIMITIVE modules
Each module now exposes an opt-in full-chain root-pop via --full-chain:
default --exploit behavior is unchanged (primitive-only, returns
EXPLOIT_FAIL). With --full-chain, after primitive lands, modules call
iamroot_finisher_modprobe_path() via a module-specific arb_write_fn
that re-uses the same trigger + slab groom to write a userspace
payload path into modprobe_path[], then exec a setuid bash dropped
by the kernel-invoked modprobe.
netfilter_xtcompat (+239): msg_msg m_list_next stride-seed FALLBACK
af_packet (+316): sk_buff data-pointer stride-seed FALLBACK
af_packet2 (+156): tp_reserve underflow + skb spray, LAST RESORT
nf_tables (+275): forged pipapo_elem with kaddr value-ptr
(Notselwyn offset 0x10), FALLBACK
cls_route4 (+251): msg_msg refill of UAF'd filter, FALLBACK
fuse_legacy (+291): m_ts overflow + MSG_COPY sanity gate,
FALLBACK (one of two modules with a real
post-write sanity check)
stackrot (+233): race-driver budget extended 3s → 30s when
--full-chain; honest <1% race-win/run
All seven honor verified-vs-claimed: arb_write_fn returns 0 for
"trigger structurally fired"; the shared finisher's setuid-bash
sentinel poll is the empirical arbiter. EXPLOIT_OK only when the
sentinel materializes within 3s of the modprobe_path trigger.
Build clean on Debian 6.12.86 (kctf-mgr); all 7 modules refuse
cleanly on both default and --full-chain paths via the existing
patched-kernel detect gate (short-circuits before the new branch).
|
||
|
|
125ce8a08b |
core: add shared finisher + offset resolver + --full-chain flag
Adds the infrastructure the 7 🟡 PRIMITIVE modules can wire into for
full-chain root pops.
core/offsets.{c,h}: four-source kernel-symbol resolution chain
1. env vars (IAMROOT_MODPROBE_PATH, IAMROOT_INIT_TASK, …)
2. /proc/kallsyms (only useful when kptr_restrict=0 or root)
3. /boot/System.map-$(uname -r) (world-readable on some distros)
4. embedded table keyed by uname-r glob (entries are
relative-to-_text, applied on top of an EntryBleed kbase leak;
seeded empty in v0.2.0 — schema-only — to honor the
no-fabricated-offsets rule).
core/finisher.{c,h}: shared root-pop helpers given a module's
arb-write primitive.
Pattern A (modprobe_path):
write payload script /tmp/iamroot-mp-<pid>.sh, arb-write
modprobe_path ← that path, execve unknown-format trigger,
wait for /tmp/iamroot-pwn-<pid> sentinel + setuid bash copy,
spawn root shell.
Pattern B (cred uid): stub — needs arb-READ too; modules use
Pattern A unless they have read+write.
On offset-resolution failure: prints a verbose how-to-populate
diagnostic and returns EXPLOIT_FAIL honestly.
core/module.h: + bool full_chain in iamroot_ctx
iamroot.c: + --full-chain flag (longopt 7, sets ctx.full_chain)
+ help text describing primitive-only-by-default + the
opt-in to attempt the full chain.
Makefile: add core/offsets.o + core/finisher.o to CORE_SRCS.
Build clean on Debian 6.12.86; --help renders the new flag.
|
||
|
|
3a5105c84c |
README: clarify iamroot runs unprivileged + add non-root → root demo
The whole point of an LPE tool is going from unprivileged to root,
but the Quickstart was leading with `sudo iamroot --scan`. Fix:
- Drop sudo from --scan / --audit / --exploit / --detect-rules.
These work without root (--scan reads /proc + /etc; --audit
walks the FS via stat; --exploit IS the privilege escalation;
--detect-rules emits to stdout).
- Keep sudo only where it's actually needed: --mitigate (writes
/etc/modprobe.d + sysctl) and tee'ing rule files into
/etc/audit/rules.d/.
- Add a worked example showing `id` as uid=1000, then
`iamroot --exploit dirty_pipe --i-know`, then `id` as uid=0.
- Fix the Build & run section's `sudo ./iamroot` too.
|
||
|
|
a564571e88 |
ci: add libc6-dev-arm64-cross for aarch64 cross-build
The v0.1.0 tag's arm64 job failed with fatal error: bits/wordsize.h: No such file or directory because gcc-aarch64-linux-gnu alone doesn't pull in the cross libc headers on Ubuntu 24.04 runners. Add libc6-dev-arm64-cross + linux-libc-dev-arm64-cross so the cross-toolchain has its sysroot.v0.1.0 |
||
|
|
dce158e33a |
release: v0.1.0 — 20-module corpus, 13 root-pop + 7 primitive
iamroot.c: bump IAMROOT_VERSION from 0.1.0-phase1 → 0.1.0
README.md: replace "bootstrap phase" status with v0.1.0 corpus
breakdown (13🟢 / 7🟡 across 2016→2026 timeline)
CVES.md: redefine 🟡 to mean "primitive fires + groom + witness,
stops short of cred-overwrite chain — refuses to claim
root unless empirically demonstrated"; flip 7 entries
from 🔵 → 🟡; add the two missing 🟢 entries
(cgroup_release_agent, overlayfs_setuid); extend the
operations matrix from 7 → 20 rows.
ROADMAP.md: mark all Phase-7 items landed; add Phase 8 covering
full-chain promotions (nf_tables / xtcompat / af_packet
prioritized — each has a public reference exploit;
IAMROOT's no-fabricated-offsets rule means each needs
an env-var offset table or System.map auto-resolve).
Build clean on Debian 6.12.86; iamroot --version reports 0.1.0.
|
||
|
|
3015e71ea3 |
modules: port final 2 detect-only modules (xtcompat + stackrot)
netfilter_xtcompat (CVE-2021-22555): +597 LoC — Option B
Andy Nguyen's IPT_SO_SET_REPLACE 4-byte OOB write trigger;
msg_msg kmalloc-2k spray + sk_buff sidecar; MSG_COPY witness
+ slabinfo delta. No leak→modprobe_path chain (per-kernel
offsets refused), honest EXPLOIT_FAIL with continuation
roadmap.
stackrot (CVE-2023-3269): +619 LoC — Option C
Two-thread race driver (MAP_GROWSDOWN + mremap rotation vs
fork+fault) with cpu pinning + 3s budget; kmalloc-192 spray
for anon_vma/anon_vma_chain; race-iteration + signal
breadcrumb to /tmp/iamroot-stackrot.log. 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.
Both refuse cleanly on Debian 6.12.86 (kctf-mgr); build clean.
This closes out the detect-only → LPE port across the corpus.
All 22 registered modules now either fire a real primitive or
refuse honestly per the verified-vs-claimed bar.
|
||
|
|
498bb36404 |
modules: port 5 detect-only modules to trigger+groom (Option B)
Converts the 5 remaining detect-only network/fs LPE modules to fire
the actual kernel primitive on a vulnerable host, with honest
EXPLOIT_FAIL return values since none ship the per-kernel cred-overwrite
finisher.
af_packet (CVE-2017-7308): +444 LoC — TPACKET_V3 int-overflow
+ skb spray + best-effort cred race
af_packet2 (CVE-2020-14386): +446 LoC — tp_reserve underflow
+ sendmmsg skb spray
cls_route4 (CVE-2022-2588): +410 LoC — route4 dangling-filter UAF
+ msg_msg 1k spray + classify drive
fuse_legacy (CVE-2022-0185): +420 LoC — fsconfig 4k OOB write
+ msg_msg cross-cache groom
nf_tables (CVE-2024-1086): +613 LoC — hand-rolled nfnetlink batch
builder + NFT_GOTO/DROP double-free
+ msg_msg groom skeleton
All five share:
- userns+netns reach (unshare(CLONE_NEWUSER|CLONE_NEWNET))
- Detect-refuse-on-patched re-call from exploit()
- geteuid()==0 short-circuit
- Honest EXPLOIT_FAIL with continuation roadmap comments
- macOS dev-build stubs via #ifdef __linux__ where needed
Build verified clean on Debian 6.12.86 (kctf-mgr). All five refuse on
the patched kernel.
|
||
|
|
4e9741ef1f |
Add overlayfs_setuid CVE-2023-0386 — FULL working exploit
Distro-agnostic overlayfs LPE — complements Ubuntu-specific CVE-2021-3493.
Same overlayfs family.
The bug: overlayfs copy_up preserves setuid bits even when the
unprivileged user triggering copy-up wouldn't normally have CAP_FSETID.
Exploit:
1. unshare(USER|NS), uid_map self → root in userns
2. Find a setuid binary on host (/usr/bin/su, sudo, passwd auto-pick)
3. mount overlayfs with the binary's dirname as lower
4. chown(merged/<binary>, 0, 0) — triggers copy-up; THE BUG: setuid
bit persists in upper-layer copy despite our unprivileged context
5. Open + truncate + replace upper-layer content with our payload
(a compiled C binary that setresuid(0,0,0) + execle /bin/sh -p)
6. exec upper-layer binary — runs as root via persistent setuid bit
- kernel_range: 5.11 ≤ K < 6.3, backports 5.15.110 / 6.1.27 / 6.2.13
- Detect refuses on patched / missing setuid carrier / userns denied
- Cleanup: rm -rf /tmp/iamroot-ovlsu-*
- Auditd: mount(overlay) + chown/fchown chain — shared with
CVE-2021-3493 module via the family-level 'iamroot-overlayfs' key
- Compiles payload via target's gcc/cc (fallback dynamic if no -static)
Verified on Debian 6.12.86 (patched): detect reports OK; exploit
refuses cleanly. Module count = 20.
Coverage by year now (only 2018 gap remaining):
2016: dirty_cow 🟢
2017: af_packet 🔵
2019: ptrace_traceme 🟢
2020: af_packet2 🔵
2021: pwnkit, overlayfs, netfilter_xtcompat 🟢/🟢/🔵
2022: dirty_pipe, cls_route4, fuse_legacy,
cgroup_release_agent 🟢/🔵/🔵/🟢
2023: entrybleed, stackrot, overlayfs_setuid 🟢/🔵/🟢
2024: nf_tables 🔵
2026: copy_fail family (×5) 🟢🟢🟢🟢🟢
16 of 20 modules have FULL working exploits (🟢).
|
||
|
|
6eab6d3f70 |
Add cgroup_release_agent CVE-2022-0492 — FULL working exploit
Universal container-escape LPE. Doesn't need msg_msg cross-cache groom, no arch-specific shellcode, no version-specific offsets — bug is structural (priv check in wrong namespace). Mechanism: 1. unshare(CLONE_NEWUSER | CLONE_NEWNS) → become 'root' in userns 2. write uid_map/gid_map (deny setgroups first) 3. mount cgroup v1 (rdma controller; memory fallback) 4. mkdir /<mnt>/iamroot subgroup 5. write payload-path → release_agent (in mount root) 6. write '1' → notify_on_release (in subgroup) 7. write our pid → cgroup.procs (in subgroup) 8. exit → cgroup empties → kernel exec's payload as INIT-ns uid=0 9. Payload drops /tmp/iamroot-cgroup-sh with setuid root 10. Parent polls for the setuid-shell appearance + exec's it -p - kernel_range: K < 5.17 mainline, backports across 4.9 / 4.14 / 4.19 / 5.4 / 5.10 / 5.15 / 5.16 LTS branches. - Detect probes user_ns+mount_ns clone via fork-isolated child. - Cleanup removes /tmp/iamroot-cgroup-* + umount the workspace. - Auditd: flag unshare + mount(cgroup) + /sys/fs/cgroup writes from non-root. Sigma rule for unshare+cgroup-mount chain. Path buffers oversized to silence GCC -Wformat-truncation noise (cgdir 384, ra_path 384, nor_path/cgproc_path 512). Verified on Debian 6.12.86 (patched): detect reports OK; exploit refuses cleanly. Module count = 19. |
||
|
|
7387ffd3bd |
Add stackrot (CVE-2023-3269) + af_packet2 (CVE-2020-14386) modules
Two more for 'THE tool' coverage breadth. stackrot CVE-2023-3269 (Ruihan Li, Jul 2023): - maple-tree VMA-split UAF — kernel R/W via use-after-RCU - **Different bug class than the netfilter-heavy 2022-2024 modules** (mm-class, broadens corpus shape) - kernel_range: 6.1 ≤ K < 6.4-rc4, backports: 6.1.37 / 6.3.10 / mainline 6.4 - Pre-6.1 immune (no maple tree); 6.5+ patched - Affects 6.1 LTS still widely deployed - ~1000-line public PoC deferred for port af_packet2 CVE-2020-14386 (Or Cohen, Sep 2020): - AF_PACKET tpacket_rcv VLAN integer underflow → heap OOB - Sibling of CVE-2017-7308; same subsystem, different code path - kernel_range: 4.6 ≤ K, backports across 4.9 / 4.14 / 4.19 / 5.4 / 5.7 / 5.8 - Family-shared 'iamroot-af-packet' audit key (one ausearch covers both CVEs from one rule deployment) Era coverage now (1 gap year remaining: 2018): 2016: dirty_cow 🟢 2017: af_packet 🔵 2019: ptrace_traceme 🟢 2020: af_packet2 🔵 2021: pwnkit, overlayfs, netfilter_xtcompat 🟢/🟢/🔵 2022: dirty_pipe, cls_route4, fuse_legacy 🟢/🔵/🔵 2023: entrybleed, stackrot 🟢/🔵 2024: nf_tables 🔵 2026: copy_fail family (×5) 🟢 18 modules total. Build clean. Scan on Debian 6.12.86: 13 OK / 5 VULN. |
||
|
|
b24934156a |
Install ergonomics: GitHub release workflow + install.sh + README quickstart
For 'people should say just use iamroot' framing, the install gate is the single biggest discoverability bottleneck. This commit makes it: curl -sSL https://github.com/KaraZajac/IAMROOT/releases/latest/download/install.sh | sh .github/workflows/release.yml: - Triggers on semver tag push (v*.*.*) + manual dispatch. - Matrix build for x86_64 (gcc) and arm64 (aarch64-linux-gnu-gcc cross). - Per-arch sha256sum alongside the binary. - Auto-generates release notes pointing at CVES.md / ROADMAP.md and including the install one-liner with the version-specific URL. - Publishes via softprops/action-gh-release@v2. install.sh (also uploaded as a release artifact, so the curl|sh above is stable): - Detects arch (x86_64 / aarch64 → arm64). - Pulls iamroot-<arch> + iamroot-<arch>.sha256 from the requested version (default: latest). - Verifies sha256 via sha256sum or shasum -a 256. - Installs to /usr/local/bin/iamroot (or $IAMROOT_PREFIX). Uses sudo iff /usr/local/bin isn't already writable. - Prints quickstart hints + ethics pointer at the end. - Env knobs: IAMROOT_VERSION, IAMROOT_PREFIX, IAMROOT_REPO. README.md gains a 'Quickstart' section at the top with the four canonical commands: install, --scan, --audit, --detect-rules, fleet-scan. Lands the 'curl|bash and go' UX as the first thing visitors see. |
||
|
|
541aac6993 |
Phase 7: ptrace_traceme CVE-2019-13272 — port FULL jannh-style exploit
Convert ptrace_traceme from 🔵 → 🟢. Real working PoC following Jann Horn's Project Zero issue #1903 technique. Mechanism: 1. fork() — child becomes our traced target via PTRACE_TRACEME 2. child sleeps 500ms (lets parent execve start) 3. parent execve's setuid binary (pkexec / su / passwd / sudo — auto-selected via find_setuid_target()) 4. Kernel elevates parent's creds to root but the stale ptrace_link from step 1 isn't invalidated (the bug) 5. child PTRACE_ATTACH's to the now-privileged parent 6. child PTRACE_POKETEXT's x86_64 shellcode at parent's RIP 7. child PTRACE_DETACH — parent runs shellcode: setuid(0); setgid(0); execve('/bin/sh', ...) → root shell Implementation notes: - x86_64-only (shellcode is arch-specific). ARM/other arch returns IAMROOT_PRECOND_FAIL gracefully. - Shellcode is the canonical 33-byte setuid(0)+execve('/bin/sh') inline asm sequence. - Setuid binary selection: pkexec preferred (almost universal), then su/sudo/passwd as fallbacks. Refuses if none available. - Auto-refuses on patched kernels (re-runs detect() at start). - No cleanup applies — exploit replaces our process image on success. Verified on Debian 6.12.86 (patched): iamroot --exploit ptrace_traceme --i-know → detect() says patched → refuses cleanly. Correct. CVES.md: ptrace_traceme 🔵 → 🟢. 5 detect-only modules remain (cls_route4, nf_tables, netfilter_xtcompat, af_packet, fuse_legacy). Each is 200-400 line msg_msg/sk_buff cross-cache groom — substantial individual commits. Next push or strategic pivot per session priorities. |
||
|
|
b6dd1e0482 |
Add --audit command: system-hygiene scan (setuid/world-writable/caps/sudo)
Beyond per-CVE detect (--scan), --audit answers 'is this box generally
exposed to privesc?' — the sysadmin-persona question. Distinguishes
IAMROOT from CVE-only tools (linux-exploit-suggester) and broad-enum
tools (linPEAS): focused on the LPE-exposure surface specifically.
Four scan categories:
- setuid: walks common bin dirs via find(1) -perm -4000. Annotates
notable items: pkexec (Pwnkit history), fusermount3 (userns LPE
history), sudo/su/passwd (expected, verify-integrity), snap-confine
(Ubuntu snap escape history).
- world_writable: find /etc -perm -0002. Anything here = config edit
by unprivileged user. Should be empty on a healthy box.
- capability: getcap -r over bin dirs. Flags cap_setuid+ep /
cap_setgid+ep / cap_dac_override+ep / cap_sys_admin+ep specifically
as 'privesc-equivalent if attacker-writable'.
- sudo NOPASSWD: grep /etc/sudoers + /etc/sudoers.d. Many legit
service-account uses; flagged for operator review.
Output: human-readable table by default; --audit --json emits a single
JSON object with {audit: [findings...], summary: {category: count, ...}}.
Side-effect-free — read-only filesystem walks via popen(find/getcap/grep).
Fixed strncpy truncation warnings — switched to snprintf for path/note
copies into the finding struct.
iamroot.c MODE_AUDIT enum + --audit longopt + getopt 'A' + dispatcher
case. Usage block updated.
Verified end-to-end on Debian kctf-mgr:
iamroot --audit → 13 setuid binaries inventoried, 0 of the
other categories. pkexec correctly annotated.
iamroot --audit --json → summary object suitable for SIEM ingest.
|
||
|
|
a52f5a657f |
Phase 7: af_packet (CVE-2017-7308) + FUSE legacy (CVE-2022-0185)
Two more famous LPEs broadening 'THE tool' coverage: af_packet CVE-2017-7308 (Andrey Konovalov, Mar 2017): - AF_PACKET TPACKET_V3 ring setup integer overflow → heap write-where - Fills 2017 coverage gap - kernel_range: 3.18.49 / 4.4.57 / 4.9.18 / 4.10.6 / mainline 4.11+ - Needs CAP_NET_RAW via user_ns clone - Famous as the canonical 'userns + AF_PACKET → root' research-era LPE fuse_legacy CVE-2022-0185 (William Liu / Crusaders-of-Rust, Jan 2022): - legacy_parse_param fsconfig heap OOB → cross-cache UAF → root - **Container-escape angle** — relevant to rootless docker/podman/snap (the system admin persona's nightmare) - kernel_range: 5.4.171 / 5.10.91 / 5.15.14 / 5.16.2 / mainline 5.17+ - Needs user_ns + mount_ns to reach legacy_load() code path - Originally reported as FUSE-specific but actually applies to any fs-mount path from userns (cgroup2, etc.) Both detect-only initially; full exploits in follow-ups. Coverage by year now: 2016: dirty_cow 🟢 2017: af_packet 🔵 2019: ptrace_traceme 🔵 2021: pwnkit, overlayfs, netfilter_xtcompat 🟢/🟢/🔵 2022: dirty_pipe, cls_route4, fuse_legacy 🟢/🔵/🔵 2023: entrybleed 🟢 2024: nf_tables 🔵 2026: copy_fail family (×5) 🟢 16 modules total. Build clean. Scan on kctf-mgr: 11 OK / 5 VULNERABLE. |
||
|
|
102b117d4e |
Phase 7: PTRACE_TRACEME (CVE-2019-13272) + xt_compat (CVE-2021-22555)
Two famous 2017-2020-era LPEs to broaden 'THE tool for folks' coverage. Both detect-only initially; exploit ports as follow-ups. ptrace_traceme (CVE-2019-13272 — jannh @ Google P0, Jun 2019): - Famous because works on default-config systems with no user_ns required — locked-down environments were still vulnerable. - kernel_range thresholds: 4.4.182 / 4.9.182 / 4.14.131 / 4.19.58 / 5.0.20 / 5.1.17 / mainline 5.2+ - Exploit shape (deferred): fork → child PTRACE_TRACEME → parent execve setuid binary → child ptrace-injects shellcode → root. - Auditd: flag PTRACE_TRACEME (request 0) — false positives via gdb/strace; tune by exclusion. netfilter_xtcompat (CVE-2021-22555 — Andy Nguyen @ Google P0): - Bug existed since 2.6.19 (2006) — 15 years of latent vuln. Famous for that age + default-config reachability via unprivileged_userns. - kernel_range thresholds: 4.4.266 / 4.9.266 / 4.14.230 / 4.19.185 / 5.4.110 / 5.10.27 / 5.11.10 / mainline 5.12+ - detect() probes user_ns+net_ns clone; locked-down → PRECOND_FAIL. - Exploit shape (deferred): heap massage via msg_msg + sk_buff cross- cache groom → kernel R/W → cred or modprobe_path overwrite. ~400 lines port from Andy's public exploit.c. - Auditd: unshare + iptables-style setsockopt + msgsnd — combined, the canonical exploit footprint. Both wired into iamroot.c, core/registry.h, Makefile. CVES.md rows added with detailed status. Coverage by year now: 2016: dirty_cow 🟢 2019: ptrace_traceme 🔵 2021: pwnkit, overlayfs, netfilter_xtcompat 🟢/🟢/🔵 2022: dirty_pipe, cls_route4 🟢/🔵 2023: entrybleed 🟢 2024: nf_tables 🔵 2026: copy_fail family (×5) 🟢 Module count: 14. Build clean (no warnings). |
||
|
|
e2fcc6a9e0 |
Phase 7: overlayfs CVE-2021-3493 — port FULL exploit (vsh-style)
Convert overlayfs from 🔵 → 🟢: full vsh-style userns + overlayfs + file-capability injection exploit. Sequence: 1. mkdtemp workdir; gcc-compile a minimal payload that setresuid(0,0,0) + execle(/bin/sh, -p) 2. fork child; child unshares(CLONE_NEWUSER | CLONE_NEWNS), writes /proc/self/{setgroups,uid_map,gid_map} mapping outer uid to userns-root 3. child mounts overlayfs with lower/upper/work layout 4. child copies payload binary into merged/payload — this writes to host's upper/payload via the overlay 5. child writes security.capability xattr with VFS_CAP_REVISION_2 blob granting cap_setuid+ep on merged/payload — the BUG persists this xattr to the host fs entry 6. child exits; parent verifies xattr via getxattr on upper/payload 7. parent execve's upper/payload from outside userns → has cap_setuid effective → setuid(0) → /bin/sh -p with uid=0 - libcap-less setcap: build VFS_CAP_REVISION_2 blob in-place (cap_setuid bit 7, cap_setgid bit 6, effective flag set in magic_etc), write via setxattr(security.capability). - which_gcc() fallback to /usr/bin/cc, /bin/gcc, etc.; tries -static first, falls back to dynamic link if static unavailable. - Re-runs detect() to refuse on patched / non-Ubuntu hosts. - Cleanup on failure: rmdir/unlink the workdir tree. - Removed unused write_uid_gid_map() helper (logic now inline in child since we self-write the maps post-unshare). Verified end-to-end on Debian kctf-mgr: iamroot --exploit overlayfs --i-know → 'not Ubuntu — bug is Ubuntu-specific' → 'refusing'. Correct. Path buffers oversized vs. mkdtemp template to silence GCC -Wformat-truncation noise. CVES.md: overlayfs 🔵 → 🟢. |
||
|
|
cb39cc5119 |
Phase 7: Dirty COW (CVE-2016-5195) FULL module — old-systems coverage
The iconic 2016 LPE. Fills the 10-year coverage gap (now spanning
2016 → 2026): RHEL 6/7, Ubuntu 14.04, Ubuntu 16.04, embedded boxes,
IoT — many still in production with kernels predating the 4.9 fix.
- modules/dirty_cow_cve_2016_5195/iamroot_modules.{c,h}:
- kernel_range: backport thresholds for 2.6 / 3.2 / 3.10 / 3.12 /
3.16 / 3.18 / 4.4 / 4.7 / 4.8 / mainline 4.9
- dirty_cow_write(): Phil-Oester-style two-thread race
- mmap /etc/passwd MAP_PRIVATE (writes go COW)
- writer thread: pwrite to /proc/self/mem at COW page offset
- madviser thread: madvise(MADV_DONTNEED) to drop COW copy
- poll-read /etc/passwd via separate fd to check if payload landed
- 3-second timeout (race usually wins in ms on vulnerable kernels)
- dirty_cow_exploit(): getpwuid → find_passwd_uid_field → race
→ execlp(su)
- dirty_cow_cleanup(): POSIX_FADV_DONTNEED + drop_caches
- Auditd rule: /proc/self/mem writes + madvise MADV_DONTNEED
- Sigma rule: non-root /proc/self/mem open → high
- Makefile: -lpthread added to LDFLAGS for the binary link.
- iamroot.c + core/registry.h wired.
- CVES.md row added with detailed status; legend updated.
Verified end-to-end on kctf-mgr (6.12.86 — patched):
iamroot --scan → 'dirty_cow: kernel is patched' (OK)
iamroot --exploit dirty_cow --i-know
→ 'detect() says not vulnerable; refusing'
Module count = 12.
|
||
|
|
3ad1446489 |
Add cls_route4 CVE-2022-2588 module (detect-only)
11th module. net/sched cls_route4 handle-zero dead UAF — discovered
by kylebot Aug 2022, fixed mainline 5.20 (commit 9efd23297cca).
Bug existed since 2.6.39 → very wide attack surface.
- modules/cls_route4_cve_2022_2588/iamroot_modules.{c,h}:
- kernel_range thresholds: 5.4.213 / 5.10.143 / 5.15.69 / 5.18.18 /
5.19.7 / mainline 5.20+
- can_unshare_userns() probes user_ns+net_ns clone availability
(the exploit's CAP_NET_ADMIN-in-userns gate)
- cls_route4_module_available() checks /proc/modules
- Reports VULNERABLE if kernel in range AND user_ns allowed;
PRECOND_FAIL if user_ns denied; OK if patched.
- Exploit stub returns IAMROOT_PRECOND_FAIL with reference to
kylebot's public PoC.
- Auditd rule: tc-style sendto syscalls (rough; legit traffic
shaping will trip — tune by user).
iamroot.c + Makefile + core/registry.h wired. CVES.md row added.
Verified on kctf-mgr (6.12.86): module reports OK, total module
count = 11.
|
||
|
|
fe33400f94 |
JSON polish: --list --json + --module-info + json_escape helper
- iamroot.c: cmd_list() takes ctx and switches on ctx->json — emits
one JSON object {version, modules:[{name, cve, family, kernel_range,
summary, has:{detect, exploit, mitigate, cleanup, auditd, sigma,
yara, falco}}, ...]}. The 'has' object lets a SIEM query module
capability without parsing the rule body.
- New cmd_module_info(name, ctx): full per-module detail.
Human-readable by default; --json switches to JSON with embedded
rule-text bodies for auditd/sigma/yara/falco.
- emit_module_json() helper shared between --list --json and
--module-info --json with include_rules toggle.
- json_escape() helper: minimal-but-safe escaping (quote, backslash,
newline, CR, tab; drops other control bytes). Sufficient for our
module-defined strings which are ASCII-only by convention.
- enum mode gains MODE_MODULE_INFO; longopts gains --module-info=<name>
taking required argument; getopt_long short string 'I:'.
- Usage block updated.
Verified end-to-end on kctf-mgr:
iamroot --list --json | jq '.modules[0]' → module metadata object
iamroot --module-info entrybleed → human pretty-print
iamroot --module-info entrybleed --json → JSON with embedded sigma
|