DragonFlyBSD Kernel Audit
DF-0878 / harness.c
← back to finding ↓ download raw
/*
 * DF-0878 — Deterministic harness transcribing cd9660_rrip_loop() +
 * cd9660_rrip_altname() (NM handler) verbatim, with a poisoned allocator.
 *
 * Proves the missing upper-bound check lets the NM handler bcopy()
 * (h.length - 5) bytes starting at p+5, reading FAR past the SUSP entry's
 * declared extent (past `pend`, i.e. past the directory record) into
 * adjacent heap.  The OOB region is pre-filled with a recognizable
 * sentinel so the leak extent is measured exactly.
 *
 * This mirrors:
 *   sys/vfs/isofs/cd9660/cd9660_rrip.c:503-539  (cd9660_rrip_loop inner loop)
 *   sys/vfs/isofs/cd9660/cd9660_rrip.c:225-283  (cd9660_rrip_altname, cflag=0)
 *   sys/vfs/isofs/cd9660/cd9660_rrip.h:41-45    (ISO_SUSP_HEADER)
 *   sys/vfs/isofs/cd9660/cd9660_rrip.h:81-84    (ISO_RRIP_ALTNAME)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

/* ---- transcribed kernel types (exact layout) ---- */
typedef struct {
	char    type[2];
	uint8_t length;     /* 7.11 */
	uint8_t version;
} ISO_SUSP_HEADER;     /* 4 bytes */

typedef struct {
	ISO_SUSP_HEADER h;
	char flags;         /* offset 4 */
} ISO_RRIP_ALTNAME;    /* 5 byte header; name follows at offset 5 */

/* 7.11 decode: a single unsigned byte */
static unsigned i711(const uint8_t *p) { return p[0]; }

/*
 * Faithful transcription of cd9660_rrip_altname() cflag==0 path
 * (cd9660_rrip.c:256-277).  We only exercise the cflag=0 branch because
 * that is the one that uses h.length to size the bcopy.
 *
 * Returns number of bytes copied into outbuf (== wlen), and sets *oob_flag
 * if the source range [inbuf, inbuf+wlen) extends past `pend`.
 */
static int rrip_altname_nm(ISO_SUSP_HEADER *p, char *pend,
                           char *outbuf, int maxlen,
                           int *leaked_oob_bytes)
{
	char *inbuf;
	int wlen;

	char flags = ((char *)p)[4];   /* ISO_RRIP_ALTNAME.flags at offset 4 */
	if (flags != 0)            /* only cflag==0 path modeled */
		return 0;
	wlen = i711((uint8_t*)&p->length) - 5;          /* :258 */
	inbuf = (char *)p + 5;                           /* :259 */

	if (wlen > maxlen)
		wlen = maxlen;                /* mimic the ana->maxlen clamp :268 */

	/* how far past pend does the source read go? */
	const char *src_end = inbuf + wlen;
	*leaked_oob_bytes = (src_end > pend) ? (int)(src_end - pend) : 0;

	/* the actual bcopy from :276 — copies wlen bytes from inbuf */
	memcpy(outbuf, inbuf, wlen);
	return wlen;
}

/*
 * Faithful transcription of the cd9660_rrip_loop inner while-loop
 * (cd9660_rrip.c:509-538).  We model a single NM entry and show the
 * dispatch happens WITHOUT any `phead + h.length <= pend` check.
 *
 * Returns total leaked OOB bytes.
 */
static int rrip_loop_transcribe(char *su_start, char *pend,
                                char *outbuf, int maxlen)
{
	ISO_SUSP_HEADER *phead = (ISO_SUSP_HEADER *)su_start;
	int leaked_total = 0;

	/* :509  while (pend >= phead + 1)  -- only checks the 4B header fits */
	while (pend >= (char *)phead + 1) {
		if (phead->version == 1) {                 /* :510 */
			/* :511-518  match 'NM' and dispatch IMMEDIATELY */
			if (phead->type[0] == 'N' && phead->type[1] == 'M') {
				int leaked = 0;
				int n = rrip_altname_nm(phead, pend,
				                        outbuf, maxlen, &leaked);
				printf("[loop] dispatched NM handler: copied %d "
				       "bytes; %d bytes read PAST pend (OOB)\n",
				       n, leaked);
				leaked_total += leaked;
			}
		}
		/* :530 plausibility check -- LOWER bound only, no upper bound */
		if (i711((uint8_t*)&phead->length) < sizeof(*phead))
			break;
		/* :537 advance by h.length */
		phead = (ISO_SUSP_HEADER *)((char *)phead +
		                            i711((uint8_t*)&phead->length));
		if ((char *)phead >= pend)   /* end of modeled single-entry case */
			break;
	}
	return leaked_total;
}

#define SENTINEL 0x5a   /* recognizable poison byte in the OOB region */

int main(void)
{
	/*
	 * Simulate a small directory record tail near a block boundary.
	 * The "valid" record region is `RECORD_LEN` bytes; everything after
	 * is adjacent kernel heap (poisoned).  We place an NM entry whose
	 * declared length grossly exceeds the available System Use space.
	 */
	const int RECORD_LEN   = 16;   /* tiny record: 5B NM header + 3B name + 8B slack */
	const int HEAP_TAIL    = 512;  /* simulated adjacent heap to observe leak into */
	const int CLAIMED_LEN  = 255;  /* h.length the attacker forges */

	char *region = calloc(1, RECORD_LEN + HEAP_TAIL);
	if (!region) { perror("calloc"); return 1; }

	/* poison the adjacent-heap region with the sentinel */
	memset(region + RECORD_LEN, SENTINEL, HEAP_TAIL);

	/* build the directory record: a single NM entry at offset 0 */
	ISO_SUSP_HEADER *nm = (ISO_SUSP_HEADER *)region;
	nm->type[0]  = 'N';
	nm->type[1]  = 'M';
	nm->length   = CLAIMED_LEN;     /* :515 dispatch trusts this */
	nm->version  = 1;
	region[4]    = 0;               /* flags = 0 (component name) :236 */
	/* put 3 legitimate name bytes in the valid region */
	region[5] = 'f'; region[6] = 'o'; region[7] = 'o';

	/* `pend` = end of the directory record (the kernel's bound) */
	char *pend = region + RECORD_LEN;

	char outbuf[300];
	memset(outbuf, 0xee, sizeof(outbuf));
	int maxlen = 255;   /* ana->maxlen */

	printf("=== DF-0878 harness: cd9660_rrip_loop + NM handler OOB read ===\n");
	printf("record region:   [%p, %p)  (%d bytes valid)\n",
	       region, pend, RECORD_LEN);
	printf("NM h.length:     %d  (claimed)\n", CLAIMED_LEN);
	printf("NM handler wlen: %d  (h.length - 5)\n", CLAIMED_LEN - 5);
	printf("valid name bytes in record: 3 ('foo')\n");
	printf("adjacent-heap poison byte: 0x%02x\n", SENTINEL);
	printf("--\n");

	int leaked = rrip_loop_transcribe(region, pend, outbuf, maxlen);

	printf("--\n");
	printf("OUTPUT (filename) bytes copied into outbuf:\n  ");
	/* show first 40 bytes as hex + ascii */
	for (int i = 0; i < 40 && i < (CLAIMED_LEN-5); i++) {
		printf("%02x", (unsigned char)outbuf[i]);
		if (i == 39) printf("...");
	}
	printf("\n  ascii: ");
	for (int i = 0; i < 40 && i < (CLAIMED_LEN-5); i++) {
		unsigned char c = outbuf[i];
		putchar((c >= 32 && c < 127) ? c : '.');
		if (i == 39) printf("...");
	}
	printf("\n");
	printf("--\n");
	int poison_count = 0;
	for (int i = 0; i < (CLAIMED_LEN-5); i++)
		if ((unsigned char)outbuf[i] == SENTINEL) poison_count++;
	printf("RESULT: %d bytes read PAST the directory-record boundary (pend)\n",
	       leaked);
	printf("        %d of those leaked bytes carry the heap sentinel 0x%02x "
	       "(kernel heap info disclosure)\n", poison_count, SENTINEL);
	printf("        Only 3 bytes ('foo') were legitimately inside the record.\n");
	printf("BUG CONFIRMED: cd9660_rrip_loop dispatches the NM handler at\n");
	printf("        cd9660_rrip.c:515 with NO check that phead+h.length<=pend;\n");
	printf("        the plausibility check at :530 is lower-bound only.\n");

	free(region);
	return (leaked > 0) ? 0 : 1;
}