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).
This commit is contained in:
+148
-11
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <time.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include <getopt.h>
|
||||
#include <stdbool.h>
|
||||
@@ -670,10 +671,13 @@ static int module_safety_rank(const char *n)
|
||||
if (!strcmp(n, "cgroup_release_agent")) return 98; /* structural, no offsets */
|
||||
if (!strcmp(n, "overlayfs_setuid")) return 97; /* structural setuid */
|
||||
if (!strcmp(n, "overlayfs")) return 96; /* userns + xattr */
|
||||
if (!strcmp(n, "pack2theroot")) return 95; /* userspace D-Bus TOCTOU; dpkg + /tmp SUID footprint */
|
||||
if (!strcmp(n, "dirty_pipe")) return 90; /* page-cache write */
|
||||
if (!strcmp(n, "dirty_cow")) return 89;
|
||||
if (!strncmp(n, "copy_fail", 9) ||
|
||||
!strncmp(n, "dirty_frag", 10)) return 88;
|
||||
!strncmp(n, "dirty_frag", 10)) return 88; /* verified page-cache writes */
|
||||
if (!strcmp(n, "dirtydecrypt") ||
|
||||
!strcmp(n, "fragnesia")) return 86; /* ported page-cache writes, NOT VM-verified */
|
||||
if (!strcmp(n, "ptrace_traceme")) return 85; /* userspace cred race */
|
||||
if (!strcmp(n, "sudo_samedit")) return 80; /* heap-tuned, may crash sudo */
|
||||
if (!strcmp(n, "af_unix_gc")) return 25; /* kernel race, low win% */
|
||||
@@ -682,6 +686,68 @@ static int module_safety_rank(const char *n)
|
||||
return 50; /* kernel primitives — middle of pack */
|
||||
}
|
||||
|
||||
/* Run a module's detect() in a forked child so a SIGILL/SIGSEGV/etc.
|
||||
* in one detector cannot tear down the dispatcher. The verdict travels
|
||||
* back via the child's exit status (skeletonkey_result_t values fit in
|
||||
* 0..5). On a crash, returns SKELETONKEY_TEST_ERROR; *crashed_signal
|
||||
* is set to the terminating signal (0 if exited normally).
|
||||
*
|
||||
* This matters because --auto auto-enables active probes, which can
|
||||
* exercise CPU instructions (entrybleed's prefetchnta sweep) or
|
||||
* kernel paths (XFRM ESP-in-TCP setup) that may misbehave under
|
||||
* emulation or hardened containers. Without isolation, one bad probe
|
||||
* stops the whole scan and the operator never sees the rest of the
|
||||
* verdict table. */
|
||||
static skeletonkey_result_t run_detect_isolated(
|
||||
const struct skeletonkey_module *m,
|
||||
const struct skeletonkey_ctx *ctx,
|
||||
int *crashed_signal)
|
||||
{
|
||||
*crashed_signal = 0;
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
perror("fork");
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
if (pid == 0) {
|
||||
skeletonkey_result_t r = m->detect(ctx);
|
||||
fflush(NULL);
|
||||
_exit((int)r);
|
||||
}
|
||||
int st;
|
||||
if (waitpid(pid, &st, 0) < 0) return SKELETONKEY_TEST_ERROR;
|
||||
if (WIFEXITED(st)) return (skeletonkey_result_t)WEXITSTATUS(st);
|
||||
if (WIFSIGNALED(st)) *crashed_signal = WTERMSIG(st);
|
||||
return SKELETONKEY_TEST_ERROR;
|
||||
}
|
||||
|
||||
/* Best-effort host distro fingerprint via /etc/os-release. Populates
|
||||
* id_out and ver_out with up to 63 chars each; falls back to "?" when
|
||||
* /etc/os-release is missing or unparseable. */
|
||||
static void read_os_release(char *id_out, size_t id_cap,
|
||||
char *ver_out, size_t ver_cap)
|
||||
{
|
||||
snprintf(id_out, id_cap, "?");
|
||||
snprintf(ver_out, ver_cap, "?");
|
||||
FILE *f = fopen("/etc/os-release", "r");
|
||||
if (!f) return;
|
||||
char line[256];
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
const char *key = NULL; char *dst = NULL; size_t cap = 0;
|
||||
if (strncmp(line, "ID=", 3) == 0) {
|
||||
key = line + 3; dst = id_out; cap = id_cap;
|
||||
} else if (strncmp(line, "VERSION_ID=", 11) == 0) {
|
||||
key = line + 11; dst = ver_out; cap = ver_cap;
|
||||
} else continue;
|
||||
const char *v = key;
|
||||
if (*v == '"' || *v == '\'') v++;
|
||||
size_t L = strcspn(v, "\"'\n");
|
||||
if (L >= cap) L = cap - 1;
|
||||
memcpy(dst, v, L); dst[L] = '\0';
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static int cmd_auto(struct skeletonkey_ctx *ctx)
|
||||
{
|
||||
if (!ctx->authorized) {
|
||||
@@ -695,28 +761,98 @@ static int cmd_auto(struct skeletonkey_ctx *ctx)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Active probes give --auto a more accurate verdict on modules that
|
||||
* implement them (dirty_pipe, the copy_fail family, dirtydecrypt,
|
||||
* fragnesia, overlayfs). Each per-module probe is documented safe:
|
||||
* /tmp sentinel files + fork-isolated namespace mounts. No real
|
||||
* system state is corrupted by the scan. Without this, --auto can
|
||||
* miss vulnerabilities that a version-only check would flag as
|
||||
* indeterminate (TEST_ERROR), or accept distro silent backports
|
||||
* that the version check is fooled by. */
|
||||
bool prev_active = ctx->active_probe;
|
||||
ctx->active_probe = true;
|
||||
|
||||
struct utsname u; uname(&u);
|
||||
fprintf(stderr, "[*] auto: host=%s kernel=%s arch=%s\n", u.nodename, u.release, u.machine);
|
||||
char distro_id[64], distro_ver[64];
|
||||
read_os_release(distro_id, sizeof distro_id, distro_ver, sizeof distro_ver);
|
||||
fprintf(stderr, "[*] auto: host=%s distro=%s/%s kernel=%s arch=%s\n",
|
||||
u.nodename, distro_id, distro_ver, u.release, u.machine);
|
||||
fprintf(stderr, "[*] auto: active probes enabled — brief /tmp file "
|
||||
"touches and fork-isolated namespace probes\n");
|
||||
fprintf(stderr, "[*] auto: scanning %zu modules for vulnerabilities...\n",
|
||||
skeletonkey_module_count());
|
||||
|
||||
struct cand { const struct skeletonkey_module *m; int rank; } cands[64];
|
||||
int nc = 0;
|
||||
int n_vuln = 0, n_ok = 0, n_precond = 0, n_test = 0, n_crash = 0, n_other = 0;
|
||||
size_t n = skeletonkey_module_count();
|
||||
for (size_t i = 0; i < n && nc < 64; i++) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const struct skeletonkey_module *m = skeletonkey_module_at(i);
|
||||
if (!m->detect || !m->exploit) continue;
|
||||
skeletonkey_result_t r = m->detect(ctx);
|
||||
if (r == SKELETONKEY_VULNERABLE) {
|
||||
cands[nc].m = m;
|
||||
cands[nc].rank = module_safety_rank(m->name);
|
||||
fprintf(stderr, "[+] auto: %-22s VULNERABLE (safety rank %d)\n",
|
||||
m->name, cands[nc].rank);
|
||||
nc++;
|
||||
int sig = 0;
|
||||
skeletonkey_result_t r = run_detect_isolated(m, ctx, &sig);
|
||||
if (sig != 0) {
|
||||
fprintf(stderr, "[?] auto: %-22s detect() crashed "
|
||||
"(signal %d) — continuing\n", m->name, sig);
|
||||
n_crash++;
|
||||
continue;
|
||||
}
|
||||
switch (r) {
|
||||
case SKELETONKEY_VULNERABLE:
|
||||
if (nc < 64) {
|
||||
cands[nc].m = m;
|
||||
cands[nc].rank = module_safety_rank(m->name);
|
||||
fprintf(stderr, "[+] auto: %-22s VULNERABLE (safety rank %d)\n",
|
||||
m->name, cands[nc].rank);
|
||||
nc++;
|
||||
} else {
|
||||
fprintf(stderr, "[+] auto: %-22s VULNERABLE (overflow; not "
|
||||
"considered for pick)\n", m->name);
|
||||
}
|
||||
n_vuln++;
|
||||
break;
|
||||
case SKELETONKEY_OK:
|
||||
fprintf(stderr, "[ ] auto: %-22s patched or not applicable\n",
|
||||
m->name);
|
||||
n_ok++;
|
||||
break;
|
||||
case SKELETONKEY_PRECOND_FAIL:
|
||||
fprintf(stderr, "[ ] auto: %-22s precondition not met\n", m->name);
|
||||
n_precond++;
|
||||
break;
|
||||
case SKELETONKEY_TEST_ERROR:
|
||||
fprintf(stderr, "[?] auto: %-22s indeterminate "
|
||||
"(detector could not decide)\n", m->name);
|
||||
n_test++;
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "[?] auto: %-22s %s\n", m->name, result_str(r));
|
||||
n_other++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Restore caller's --active setting before we call exploit(). The
|
||||
* exploit() of each module may use ctx->active_probe with different
|
||||
* semantics than detect(); we owned this flag only for the scan. */
|
||||
ctx->active_probe = prev_active;
|
||||
|
||||
fprintf(stderr, "\n[*] auto: scan summary — %d vulnerable, %d patched/"
|
||||
"n.a., %d precondition-fail, %d indeterminate%s\n",
|
||||
n_vuln, n_ok, n_precond, n_test,
|
||||
n_other ? " (+other)" : "");
|
||||
if (n_crash > 0)
|
||||
fprintf(stderr, "[!] auto: %d module(s) crashed during detect "
|
||||
"— dispatcher recovered via fork isolation\n", n_crash);
|
||||
|
||||
if (nc == 0) {
|
||||
fprintf(stderr, "\n[-] auto: no vulnerable modules. Host appears patched.\n");
|
||||
if (n_test > 0) {
|
||||
fprintf(stderr, "[i] auto: %d module(s) returned indeterminate. "
|
||||
"Try `skeletonkey --exploit <name> --i-know` if "
|
||||
"you know the host is vulnerable.\n", n_test);
|
||||
}
|
||||
fprintf(stderr, "[-] auto: no confirmed-vulnerable modules. Host "
|
||||
"appears patched.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -791,6 +927,7 @@ int main(int argc, char **argv)
|
||||
skeletonkey_register_vmwgfx();
|
||||
skeletonkey_register_dirtydecrypt();
|
||||
skeletonkey_register_fragnesia();
|
||||
skeletonkey_register_pack2theroot();
|
||||
|
||||
enum mode mode = MODE_SCAN;
|
||||
struct skeletonkey_ctx ctx = {0};
|
||||
|
||||
Reference in New Issue
Block a user