97be306fd21b5326e686d2eb14a929204b6df058
75 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
97be306fd2 |
release: bump version to v0.6.0
This release captures the session's reliability + accuracy work
on top of v0.5.0:
- Shared host fingerprint (core/host.{h,c}): kernel/distro/userns
gates / sudo + polkit versions, populated once at startup; every
module consults ctx->host instead of doing its own probes.
- Test harness (tests/test_detect.c, make test): 44 unit tests over
mocked host fingerprints, wired into CI as a non-root step.
- --auto upgrades: auto-enables --active, per-detect 15s timeout,
fork-isolated detect + exploit so a crashing module can't tear
down the dispatcher, per-module verdict table + scan summary.
- --dry-run flag (preview without firing; --i-know not required).
- Pinned mainline fix commits for the 3 ported modules
(dirtydecrypt / fragnesia / pack2theroot) — detect() is now
version-pinned with kernel_range tables, not precondition-only.
- New modules: dirtydecrypt (CVE-2026-31635), fragnesia
(CVE-2026-46300), pack2theroot (CVE-2026-41651).
- macOS dev build works for the first time (all Linux-only code
wrapped in #ifdef __linux__).
- docs/JSON_SCHEMA.md: stable consumer contract for --scan --json.
Version bump:
- SKELETONKEY_VERSION = '0.6.0' in skeletonkey.c
- README status line updated with the v0.6.0 changelog
- docs/JSON_SCHEMA.md example refreshed
v0.6.0
|
||
|
|
a9c8f7d8c6 |
tests: 5 happy-path VULNERABLE assertions (44 total)
Adds h_kernel_5_14_userns_ok fingerprint (vulnerable kernel + userns allowed) and uses it to assert the VULNERABLE branch is reached on the 5 netfilter-class modules whose detect() short-circuits there once both gates are satisfied: - nf_tables (CVE-2024-1086) -> VULNERABLE - cls_route4 (CVE-2022-2588) -> VULNERABLE - nft_set_uaf (CVE-2023-32233) -> VULNERABLE - nft_fwd_dup (CVE-2022-25636) -> VULNERABLE - nft_payload (CVE-2023-0179) -> VULNERABLE Combined with the earlier sudo_samedit and pwnkit vulnerable-version tests, this gives us positive-verdict coverage on 7 modules (was 2). The detect() logic that decides VULNERABLE when conditions match is now exercised, not just the precondition short-circuits. 39 -> 44 cases, all pass on Linux. |
||
|
|
150f16bc97 |
pwnkit + sudoedit_editor: ctx->host migration + 4 more tests (39 total)
pwnkit: migrate detect() to consult ctx->host->polkit_version with the same graceful-fallback pattern as the sudo modules. The version is populated once at startup by core/host.c (via pkexec --version); detect() skips the per-scan popen when the host fingerprint has the version. Falls back to the inline popen path when ctx->host is missing the version (degenerate test contexts). sudoedit_editor: already migrated; this commit adds direct test coverage. tests/test_detect.c expansion (35 → 39): - pwnkit: polkit_version='0.105' -> VULNERABLE (pre-0.121 fix) - pwnkit: polkit_version='0.121' -> OK (fix release) - sudoedit_editor: vuln sudo + no sudoers grant -> PRECOND_FAIL (documented behaviour: vulnerable version, but the dispatcher has no usable sudoedit grant on the host) - sudoedit_editor: fixed sudo (1.9.13p1) -> OK The sudoedit_editor 'vuln + no grant' case is the first test to exercise the second-level precondition gate AFTER the version check passes — proves the version-pinned detect logic AND the sudo -ln target-discovery short-circuit both work as intended. The h_vuln_sudo / h_fixed_sudo synthetic fingerprints gained the .polkit_version field alongside .sudo_version so a single fingerprint exercises both pwnkit and the sudo modules. Verification: 39/39 pass on Linux (docker gcc:latest + libglib2.0-dev + sudo, non-root user skeletonkeyci). macOS dev box still reports 'skipped — Linux-only' as designed. |
||
|
|
c63ee72aa1 |
docs: JSON output schema (consumer contract for --scan --json)
Adds docs/JSON_SCHEMA.md documenting the shape and stability promises
of the JSON document --scan --json emits on stdout. The schema is
already what the binary produces — this commit pins the contract so
fleet-scan / SIEM consumers can rely on it across releases.
What it covers:
- Top-level object: { version, modules } and field stability.
- Per-module entry: { name, cve, result } with type + stability.
- The 6-value result enum (OK / TEST_ERROR / VULNERABLE /
EXPLOIT_FAIL / PRECOND_FAIL / EXPLOIT_OK) and what each means
semantically.
- Process exit-code semantics for --scan (worst observed result
becomes the exit code — lets a SIEM treat the binary exit as a
single-host alert level).
- Bash + jq one-liners for the common fleet-roll-up patterns.
- A recommended Python consumer pattern with the forward-compat
guidance (ignore unknown fields, treat unknown result strings as
TEST_ERROR-equivalent).
- Explicit stability promises: which fields cannot change without
a major-version bump, what may be added in future minor
releases, what consumers MUST tolerate.
Verified against the live binary: --scan --json produces exactly
the documented shape (top-level keys {modules, version}; per-module
keys {cve, name, result}; result values come from the documented
enum). 31 modules / 30 unique CVEs at v0.5.0.
README's 'Sysadmins' audience row now links the schema doc:
'JSON output for CI gates ([schema](docs/JSON_SCHEMA.md))'.
|
||
|
|
86812b043d |
core/host: userspace version fingerprint (sudo, polkit)
The host fingerprint now captures sudo + polkit versions at startup
so userspace-LPE modules can consult a single source of truth
instead of each popen-ing the relevant binary themselves on every
scan. Pack2theroot already queries PackageKit version via D-Bus
in-module, so PackageKit stays there for now.
core/host.h:
- new fields: char sudo_version[64], char polkit_version[64].
Empty string when the tool isn't installed or version parse fails;
modules should treat that as PRECOND_FAIL.
- documented next to has_systemd / has_dbus_system in the struct.
core/host.c:
- new populate_userspace_versions(h) called from
skeletonkey_host_get() after the other populators.
- capture_first_line() helper runs a command via popen, grabs first
stdout line, strips newline. Best-effort: failure leaves dst empty.
- extract_version_after_prefix() pulls the version token after a
fixed prefix string ('Sudo version', 'pkexec version'), handling
the colon/space variants.
- skeletonkey_host_print_banner() gained a third line when either
version is non-empty:
[*] userspace: sudo=1.9.17p2 polkit=-
Module migration (graceful fallback pattern — modules still work
without ctx->host populated):
- sudo_samedit detect: if ctx->host->sudo_version is set, skip the
popen and synthesize a 'Sudo version <X>' line for the existing
parser. Falls back to the original find_sudo + popen path if the
host fingerprint didn't capture a version.
- sudoedit_editor detect: same pattern — host fingerprint sudo_version
takes precedence over the local get_sudo_version popen.
tests/test_detect.c additions (2 new cases, 33 → 35):
- h_vuln_sudo fingerprint (sudo_version='1.8.31', kernel 5.15) —
asserts sudo_samedit reports VULNERABLE via the host-provided
version string.
- h_fixed_sudo fingerprint (sudo_version='1.9.13p1', kernel 6.12) —
asserts sudo_samedit reports OK on a patched sudo.
This is the first test pair to cover the *vulnerable* path of a
module rather than just precondition gates — proves the
version-parsing logic itself, not only the short-circuits.
Verification: 35/35 pass on Linux. macOS banner shows
'userspace: sudo=1.9.17p2 polkit=-' as the dev box has Homebrew
sudo but no polkit.
|
||
|
|
0d87cbc71c |
copy_fail_family: bridge-level userns gate + 4 new tests (33 total)
The 4 dirty_frag siblings + the GCM variant all gate on unprivileged user-namespace creation (the XFRM-ESP / AF_RXRPC paths are unreachable without it). The inner DIRTYFAIL detect functions already check this, but the check happened deep inside the legacy code — invisible to the test harness, and the bridge wrappers would delegate first and only short-circuit afterwards. Move the check up to the bridge: a single cff_check_userns() helper inspects ctx->host->unprivileged_userns_allowed and returns PRECOND_FAIL (with a host-fingerprint-annotated message) BEFORE calling the inner detect. The inner check stays in place as belt- and-suspenders. copy_fail itself uses AF_ALG (no userns needed) and bypasses the gate — its inner detect still confirms the primitive empirically via the active probe. modules/copy_fail_family/skeletonkey_modules.c: - #include "../../core/host.h" alongside the existing includes. - new static cff_check_userns(modname, ctx) helper. - copy_fail_gcm_detect_wrap, dirty_frag_esp_detect_wrap, dirty_frag_esp6_detect_wrap, dirty_frag_rxrpc_detect_wrap all call cff_check_userns before delegating. - copy_fail_detect_wrap is intentionally untouched. tests/test_detect.c: 4 new EXPECT_DETECT cases assert that all 4 gated bridge wrappers return PRECOND_FAIL when unprivileged_userns_allowed=false, using the existing h_kernel_5_14_no_userns fingerprint. 29 → 33 tests, all pass on Linux. |
||
|
|
2b1e96336e |
core/host: in_range helper + 13-module migration + 12 more tests (29 total)
Three coordinated changes that build on the host_kernel_at_least
landed in
|
||
|
|
1571b88725 |
core/host: skeletonkey_host_kernel_at_least + 9 new detect() tests
core/host helper:
- Adds bool skeletonkey_host_kernel_at_least(h, M, m, p) — the
canonical 'kernel >= X.Y.Z' check. Replaces the manual
'v->major < X || (v->major == X && v->minor < Y)' pattern that
many modules use for their 'predates the bug' pre-check. Returns
false when h is NULL or h->kernel.major == 0 (degenerate cases),
true otherwise iff the host kernel sorts at or above the supplied
version.
- dirtydecrypt migrated as the demo: the 'kernel < 7.0 → predates'
pre-check now reads 'if (!host_kernel_at_least(ctx->host, 7, 0, 0))'.
Other modules still using the manual pattern continue to work
unchanged; migrating them is incremental polish.
tests/test_detect.c expansion (8 → 17 cases):
New fingerprints:
- h_kernel_4_4 — ancient (Linux 4.4 LTS); used for 'predates the
bug' on dirty_pipe.
- h_kernel_6_12 — recent (Linux 6.12 LTS); above every backport
threshold in the corpus — modules report OK via
the 'patched by mainline inheritance' branch of
kernel_range_is_patched.
- h_kernel_5_14_no_userns — vulnerable-era kernel (5.14.0, past
every relevant predates check while below every
backport entry) with unprivileged_userns_allowed
deliberately false; lets the userns gate fire
after the version check confirms vulnerable.
New tests (9):
- dirty_pipe + kernel 4.4 → OK (predates 5.8 introduction)
- dirty_pipe + kernel 6.12 → OK (above every backport)
- dirty_cow + kernel 6.12 → OK (above 4.9 fix)
- ptrace_traceme + kernel 6.12 → OK (above 5.1.17 fix)
- cgroup_release_agent + kernel 6.12 → OK (above 5.17 fix)
- nf_tables + vuln kernel + userns=false → PRECOND_FAIL
- fuse_legacy + vuln kernel + userns=false → PRECOND_FAIL
- cls_route4 + vuln kernel + userns=false → PRECOND_FAIL
- overlayfs_setuid + vuln kernel + userns=false → PRECOND_FAIL
Process note: initial 8th and 9th userns tests failed because the
chosen test kernel (5.10.0) tripped each module's predates check
(nf_tables bug introduced 5.14; overlayfs_setuid 5.11). Switched to
5.14.0, which is past every predates threshold AND below every
backport entry in this batch — the version verdict is now genuinely
'vulnerable' and the userns gate fires next. The bug-finding tests
caught a real-but-narrow modeling gap in the original picks.
Verification:
- Linux (docker gcc:latest, non-root user): 17/17 pass.
- macOS (local): builds clean, suite reports 'skipped — Linux-only'
as designed.
|
||
|
|
36814f272d |
modules: migrate remaining 22 modules to ctx->host fingerprint
Completes the host-fingerprint refactor that started in
|
||
|
|
d05a46c5c6 |
.gitignore: exclude skeletonkey-test build artifact
Mirrors the /skeletonkey rule. The test binary slipped into the prior commit; this removes it from tracking. Local binary on disk is kept (it's a build artifact). |
||
|
|
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. |