⬢ DragonFlyBSD Kernel Audit
DF-3744 / harness_lvds.c
← back to finding ↓ download raw
/*
 * DF-3744 harness: unbounded LVDS mode-patch record walk + fake-EDID copy past the
 * VBIOS image buffer
 *
 * Replica of radeon_atombios_get_lvds_info(), sys/dev/drm/radeon/radeon_atombios.c:1691-1752:
 *   record = bios + usModePatchTableOffset   (BIOS u16, never validated, :1697-1705)
 *   while (*record != ATOM_RECORD_END_TYPE) {          // :1706  no end-of-buffer check
 *       case LCD_FAKE_EDID_PATCH_RECORD_TYPE:          // :1717
 *           if (ucFakeEDIDLength) {
 *               edid_size = max(128, ucFakeEDIDLength);
 *               edid = kmalloc(edid_size);
 *               memcpy(edid, &record->ucFakeEDIDString[0], ucFakeEDIDLength);  // :1725
 *               if (drm_edid_is_valid(edid)) attach as bios_hardcoded_edid
 *           }
 *           record += ucFakeEDIDLength ? ucFakeEDIDLength + 2 : sizeof(...);
 *       ...
 *   }
 *
 * The walk has no bound against the BIOS image length: a table whose last record is a
 * fake-EDID record of length L placed within L-1 bytes of the image end makes the kernel
 * (a) memcpy up to 255 bytes starting past the heap image end, and (b) continue walking
 * past the image end.  On pass of drm_edid_is_valid() (1/256 checksum odds on uncontrolled
 * bytes, attacker-controlled while in-image), the copied bytes become the connector EDID
 * blob, readable from userspace.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32;

#define ATOM_RECORD_END_TYPE            0
#define LCD_MODE_PATCH_RECORD_MODE_TYPE 0x02
#define LCD_RTS_RECORD_TYPE             0x03
#define LCD_CAP_RECORD_TYPE             0x04
#define LCD_FAKE_EDID_PATCH_RECORD_TYPE 0x05
#define LCD_PANEL_RESOLUTION_RECORD_TYPE 0x06

struct bios_image {			/* model of rdev->bios kmalloc(bytes_read) */
	u8 data[8192];
	u8 guard[512];			/* models adjacent heap after the image */
};

static unsigned long oob_reads, oob_copies;
static u8 read_b(const struct bios_image *b, size_t off)	/* bounds-checking CU8() */
{
	size_t total = sizeof(b->data) + sizeof(b->guard);
	if (off >= sizeof(b->data)) oob_reads++;		/* past image, still in model */
	if (off >= total) { oob_reads++; return 0xAA; }		/* far OOB */
	return off < sizeof(b->data) ? b->data[off] : b->guard[off - sizeof(b->data)];
}

int main(void)
{
	static struct bios_image img;
	memset(&img, 0x5C, sizeof(img));			/* adjacent-heap model bytes */

	/* place a fake-EDID record at the very end of the image:
	 * [type=0x05][len=0xC8][200 bytes of "EDID" starting past image end] */
	size_t image_len = sizeof(img.data);
	size_t rec_off = image_len - 2;
	img.data[rec_off]     = LCD_FAKE_EDID_PATCH_RECORD_TYPE;
	img.data[rec_off + 1] = 200;				/* ucFakeEDIDLength */

	u16 usModePatchTableOffset = (u16)rec_off;		/* BIOS u16, unvalidated */
	int oob_copy_bytes = 0;

	/* === verbatim walk, radeon_atombios.c:1706-1752 (bounded only in harness) === */
	size_t roff = usModePatchTableOffset;
	unsigned iter = 0;
	while (read_b(&img, roff) != ATOM_RECORD_END_TYPE) {
		u8 type = read_b(&img, roff);
		if (type == LCD_FAKE_EDID_PATCH_RECORD_TYPE) {
			u8 len = read_b(&img, roff + 1);
			if (len) {
				/* kmalloc(max(128,len)); memcpy from record+2 for len bytes */
				u8 *edid = malloc(len > 128 ? len : 128);
				for (int i = 0; i < len; i++) {
					size_t src = roff + 2 + (size_t)i;
					if (src >= image_len) oob_copy_bytes++;  /* bytes past image */
					edid[i] = read_b(&img, src);
				}
				printf("fake-EDID: len=%u ; %d of %u bytes read from BEYOND the "
				       "VBIOS image end (heap OOB read -> kmalloc'd EDID)\n",
				       len, oob_copy_bytes, len);
				free(edid);
			}
			roff += len ? (size_t)len + 2 : 4;
		} else if (type == LCD_MODE_PATCH_RECORD_MODE_TYPE) {
			roff += 6;
		} else if (type == LCD_RTS_RECORD_TYPE) {
			roff += 3;
		} else if (type == LCD_CAP_RECORD_TYPE) {
			roff += 5;
		} else if (type == LCD_PANEL_RESOLUTION_RECORD_TYPE) {
			roff += 4;
		} else {
			printf("bad record %u at +%zu (walk already %zu bytes past image end)\n",
			       type, roff, roff >= image_len ? roff - image_len + 1 : 0);
			break;					/* kernel: DRM_ERROR + break */
		}
		if (++iter > 64) {
			printf("walk still running after %u records, now %zu bytes past image end\n",
			       iter, roff - image_len);
			break;
		}
	}
	printf("total OOB byte reads in model: %lu\n", oob_reads);
	printf("kernel: bytes past rdev->bios kmalloc'd image copied into bios_hardcoded_edid: %d\n",
	       oob_copy_bytes);

	if (oob_copy_bytes == 0) { printf("FAIL\n"); return 1; }
	printf("REPRODUCED (harness): unbounded record walk reads/copies past VBIOS buffer\n");
	return 0;
}