main
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5d48a7b0b5 |
release v0.7.1: arm64-static binary + per-module arch_support
Two additions on top of v0.7.0:
1. skeletonkey-arm64-static is now published alongside the existing
x86_64-static binary. Built native-arm64 in Alpine via GitHub's
ubuntu-24.04-arm runner pool (free for public repos as of 2024).
install.sh auto-picks it based on 'uname -m'; SKELETONKEY_DYNAMIC=1
fetches the dynamic build instead. Works on Raspberry Pi 4+, Apple
Silicon Linux VMs, AWS Graviton, Oracle Ampere, Hetzner ARM, etc.
.github/workflows/release.yml refactor: the previous single
build-static-x86_64 job becomes a build-static matrix with two
entries (x86_64-static on ubuntu-latest, arm64-static on
ubuntu-24.04-arm). Both share the same Alpine container + build
recipe.
2. .arch_support field on struct skeletonkey_module — honest per-module
labeling of which architectures the exploit() body has been verified
on. Three categories:
'any' (4 modules): pwnkit, sudo_samedit, sudoedit_editor,
pack2theroot. Purely userspace; arch-independent.
'x86_64' (1 module): entrybleed. KPTI prefetchnta side-channel;
x86-only by physics. Already source-gated (returns
PRECOND_FAIL on non-x86_64).
'x86_64+unverified-arm64' (26 modules): kernel exploitation
code. The bug class is generic but the exploit primitives
(msg_msg sprays, finisher chain, struct offsets) haven't been
confirmed on arm64. detect() still works (just reads ctx->host);
only the --exploit path is in question.
--list now has an ARCH column (any / x64 / x64?) and the footer
prints 'N arch-independent (any)'.
--module-info prints 'arch support: <value>'.
--scan --json adds 'arch_support' to each module record.
This is the honest 'arm64 works for detection on every module +
exploitation on 4 of them today; the rest await empirical arm64
sweep' framing — not pretending the kernel exploits already work
there, but not blocking the arm64 binary on that either. arm64
users get the full triage workflow + a handful of userspace exploits
out of the box, plus a clear roadmap for the rest.
Future work to promote modules from 'x86_64+unverified-arm64' to
'any': add an arm64 Vagrant box (generic/debian12-arm64 etc.) to
tools/verify-vm/ and run a verification sweep on Apple Silicon /
ARM Linux hardware.
|
||
|
|
8ab49f36f6 |
detection rules: complete sigma/yara/falco coverage across the corpus
Three parallel research agents drafted 49 detection rules grounded in
each module's source + existing .opsec_notes string + existing .detect_auditd
counterpart. A one-shot tools/inject_rules.py wrote them into the
right files and replaced the .detect_<format> = NULL placeholders.
Coverage matrix (modules with each format / 31 total):
before after
auditd 30 / 31 30 / 31 (entrybleed skipped by design)
sigma 19 / 31 31 / 31 (+12 added)
yara 11 / 31 28 / 31 (+17 added; 3 documented skips)
falco 11 / 31 30 / 31 (+19 added; entrybleed skipped)
Documented skips (kept as .detect_<format> = NULL with comment):
- entrybleed: yara + falco + auditd. Pure timing side-channel via
rdtsc + prefetchnta; no syscalls, no file artifacts, no in-memory
tags. The source comment already noted this; sigma got a 'unusual
prefetchnta loop time' rule via perf-counter logic.
- ptrace_traceme: yara. Pure in-memory race; no on-disk artifacts
or persistent strings to match. Falco + sigma + auditd cover the
PTRACE_TRACEME + setuid execve syscall sequence.
- sudo_samedit: yara. Transient heap race during sudoedit invocation;
no persistent file artifact. Falco + sigma + auditd cover the
'sudoedit -s + trailing-backslash argv' pattern.
Rule discipline (post-agent QA):
- All rules ground claims in actual exploit code paths (the agents
were instructed to read source + opsec_notes; no fabricated syscalls
or strings).
- Two falco rules were narrowed by the agent to fire only when
proc.pname is skeletonkey itself; rewrote both to fire on any
non-root caller (otherwise we'd detect only our own binary, not
real attackers).
- Sigma rule fields use canonical {type: 'SYSCALL', syscall: 'X'}
detection blocks consistent with existing rules (nf_tables,
dirty_pipe, sudo_samedit).
- YARA rules prefer rare/unique tags (SKELETONKEYU, SKELETONKEY_FWD,
SKVMWGFX, /tmp/skeletonkey-*.log) over common bytes — minimizes
false positives.
- Every rule tagged with attack.privilege_escalation + cve.YYYY.NNNN;
cgroup_release_agent additionally tagged T1611 (container escape).
skeletonkey.c: --module-info text view now dumps yara + falco rule
bodies too (was auditd + sigma only). All 4 formats visible per module.
Verification:
- macOS local: clean build, 33 kernel_range tests pass.
- Linux (docker gcc:latest): 33 + 54 = 87 passes, 0 fails.
- --module-info nf_tables / af_unix_gc / etc.: 'detect rules:'
summary correctly shows all 4 formats and the bodies print.
|
||
|
|
39ce4dff09 |
modules: per-module OPSEC notes — telemetry footprint per exploit
Adds .opsec_notes to every module's struct skeletonkey_module
(31 entries across 26 module files). One paragraph per exploit
describing the runtime footprint a defender/SOC would see:
- file artifacts created/modified (exact paths from source)
- syscall observables (the unshare / socket / setsockopt /
splice / msgsnd patterns the embedded detection rules look for)
- dmesg signatures (silent on success vs KASAN oops on miss)
- network activity (loopback-only vs none)
- persistence side-effects (/etc/passwd modification, dropped
setuid binaries, backdoors)
- cleanup behaviour (callback present? what it restores?)
Each note is grounded in the module's source code + its existing
auditd/sigma/yara/falco detection rules — the OPSEC notes are
literally the inverse of those rules (the rules describe what to
look for; the notes describe what the exploit triggers).
Three intelligence agents researched the modules in parallel,
reading source + MODULE.md, then their proposals were embedded
verbatim via tools/inject_opsec.py (one-shot script, not retained).
Where surfaced:
- --module-info <name>: '--- opsec notes ---' section between
detect-rules summary and the embedded auditd/sigma rule bodies.
- --module-info / --scan --json: 'opsec_notes' top-level string.
Audience uses:
- Red team: see what footprint each exploit leaves so they pick
chains that match the host's telemetry posture.
- Blue team: the notes mirror the existing detection rules from the
attacker side — easy diff to find gaps in their SIEM coverage.
- Researchers: per-exploit footprint catalog for technique analysis.
copy_fail_family gets one shared note across all 5 register entries
(copy_fail, copy_fail_gcm, dirty_frag_esp, dirty_frag_esp6,
dirty_frag_rxrpc) since they share exploit infrastructure.
Verification:
- macOS local: clean build, --module-info nf_tables shows full
opsec section + CWE + ATT&CK + KEV row from previous commit.
- Linux (docker gcc:latest): 33 + 54 = 87 passes, 0 fails.
Next: --explain mode (uses these notes + the triage metadata to
render a single 'why is this verdict, what would patch fix it, and
what would the SOC see' page per module).
|
||
|
|
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
|
||
|
|
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)'.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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 🔵 → 🟢. |
||
|
|
3eeee01f06 |
Phase 7: overlayfs CVE-2021-3493 module (Ubuntu userns LPE) — detect-only
10th module. Ubuntu-specific userns + overlayfs LPE that injects file
capabilities cross-namespace.
- modules/overlayfs_cve_2021_3493/iamroot_modules.{c,h}:
- is_ubuntu() — parses /etc/os-release for ID=ubuntu or
ID_LIKE=ubuntu. Non-Ubuntu hosts get IAMROOT_OK immediately (the
bug is specific to Ubuntu's modified overlayfs).
- unprivileged_userns_clone gate — sysctl=0 → PRECOND_FAIL
- Active probe (--active): forks a child that enters userns +
mountns and attempts the overlayfs mount inside /tmp. Mount
success on Ubuntu = VULNERABLE. Mount denied = patched / AppArmor
block. Child-isolated so parent's namespace state is untouched.
- Version fallback: kernel < 5.13 = vulnerable-by-inference for
Ubuntu kernels; recommend --active for confirmation.
- Exploit: detect-only stub. Reference vsh's exploit-cve-2021-3493
for full version (mount overlayfs in userns, drop binary with
cap_setuid+ep into upper layer, re-exec outside ns).
- Embedded auditd rules: mount(overlay) syscall + security.capability
xattr writes (the exploit's two-step footprint).
Verified end-to-end on kctf-mgr (Debian):
iamroot --scan → 'not Ubuntu — bug is Ubuntu-specific' → IAMROOT_OK
Module count: 10. Active-probe pattern now applies to dirty_pipe,
entrybleed, and overlayfs (and copy_fail_family via existing
dirtyfail_active_probes global). Detect quality across the corpus
materially improved this session.
|