⬢ DragonFlyBSD Kernel Audit
DF-3562 / serpoob.c
← back to finding ↓ download raw
/*
 * serpoob.c - demonstrates the unsigned-wraparound key-length defect in
 * sys/crypto/serpent/serpent.c serpent_set_key() (pass-2 finding DF-3562).
 *
 * Root cause (serpent.c:453): i, lk are u_int32_t.
 *   serpent.c:456  only upper-bound check `if (key_len > 256) return;'
 *   serpent.c:459  lk = (key_len + 31) / 32
 *   serpent.c:461  while (i < lk)  loads ctx->l_key[i] from in_key+4*i
 *   serpent.c:472  i = key_len / 32
 *   serpent.c:474  ctx->l_key[i] = (ctx->l_key[i] & (lk-1)) | lk;
 *
 * For negative key_len the int divisions truncate to negative values which
 * then wrap when stored/compared as u_int32_t:
 *
 * Case A (key_len <= -63): lk = (u32)((key_len+31)/32) is huge (e.g.
 *   key_len=-64 -> 0xFFFFFFFF) so the load loop at :461-464 runs away:
 *   linear OOB write of htole32(in_key + 4*i) into ctx->l_key[i] for
 *   billions of words (~16 GiB), reading the caller's key buffer OOB the
 *   whole way.  Child dies on the first unmapped page (stack guard).
 *
 * Case B (key_len in [-63,-32] with (key_len+31)/32 == 0): the load loop
 *   is skipped, but i = (u32)(key_len/32) = -1/-2 -> single wild access
 *   at ctx->l_key[0xFFFFFFFF] (offset +16 GiB).  Child dies of SIGBUS.
 *
 * Case C (key_len = 129): lk = (129+31)/32 = 5 -> reads 5*4 = 20 bytes
 *   from in_key although the caller's key buffer holds ceil(129/8) = 17
 *   bytes: 3-byte OOB read past the key buffer (guard page -> SIGSEGV).
 *
 * In-tree reachability: NONE today - the only caller chain is
 * cryptoapi.c serpent_cbc_setkey/serpent_xts_setkey which validate
 * 128/192/256 (256/512 halved) before calling.  Defense-in-depth parity
 * with the twofish twins DF-0965/DF-2371.
 *
 * Build: cc -O2 -Wall -I stage -o serpoob serpoob.c stage/crypto/serpent/serpent.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/mman.h>

#include "crypto/serpent/serpent.h"

static void
child_report(int sig, siginfo_t *si, void *uc)
{
	(void)uc;
	fprintf(stderr, "child: died of signal %d si_code=%d si_addr=%p\n",
	    sig, si->si_code, si->si_addr);
	/* re-raise so the parent sees a real kill, not our exit code */
	signal(sig, SIG_DFL);
	raise(sig);
	_exit(128 + sig);
}

static void
arm_handlers(void)
{
	struct sigaction sa;

	memset(&sa, 0, sizeof(sa));
	sa.sa_sigaction = child_report;
	sa.sa_flags = SA_SIGINFO;
	sigaction(SIGBUS, &sa, NULL);
	sigaction(SIGSEGV, &sa, NULL);
}

/* run serpent_set_key(key, key_len) in a forked child; report how it died */
static int
run_case(const uint8_t *key, int key_len, const char *tag)
{
	pid_t p = fork();

	if (p == 0) {
		serpent_ctx ctx;

		arm_handlers();
		memset(&ctx, 0, sizeof(ctx));
		serpent_set_key(&ctx, key, key_len);
		fprintf(stderr, "child(%s): returned normally?!\n", tag);
		_exit(0);
	}
	int st = 0;
	waitpid(p, &st, 0);
	/* the handler reports si_code/si_addr on stderr and re-raises, but
	 * the re-raised signal may still be blocked in the handler, in
	 * which case the child exits 128+sig instead: accept both forms */
	if (WIFSIGNALED(st) ||
	    (WIFEXITED(st) && WEXITSTATUS(st) >= 128 && WEXITSTATUS(st) <= 140)) {
		printf("%-28s child killed (OOB confirmed)\n", tag);
		return (1);
	}
	printf("%-28s child exited %d (no fault)\n", tag,
	    WIFEXITED(st) ? WEXITSTATUS(st) : -1);
	return (0);
}

int
main(void)
{
	uint8_t key[32];
	int evid = 0;

	setvbuf(stdout, NULL, _IONBF, 0);
	memset(key, 0x41, sizeof(key));

	printf("== Case A: key_len=-64 -> u32-wrapped lk=0xFFFFFFFF -> "
	    "runaway l_key[] fill loop ==\n");
	evid += run_case(key, -64, "key_len=-64");

	printf("== Case B: key_len=-32 -> i=(u32)-1 -> access at "
	    "l_key[0xFFFFFFFF] (+16GiB) ==\n");
	evid += run_case(key, -32, "key_len=-32");

	printf("== Case C: key_len=129 -> 3-byte OOB read past key buffer "
	    "(guard page) ==\n");
	{
		size_t pg = (size_t)sysconf(_SC_PAGESIZE);
		uint8_t *base = mmap(NULL, 2 * pg, PROT_READ | PROT_WRITE,
		    MAP_ANON | MAP_PRIVATE, -1, 0);

		if (base == MAP_FAILED) {
			perror("mmap");
			return (2);
		}
		if (mprotect(base + pg, pg, PROT_NONE) != 0) {
			perror("mprotect");
			return (2);
		}
		uint8_t *key17 = base + pg - 17;	/* 17 = ceil(129/8) */
		memset(key17, 0x42, 17);
		/* child inherits the mapping */
		pid_t p = fork();
		if (p == 0) {
			serpent_ctx ctx;

			arm_handlers();
			memset(&ctx, 0, sizeof(ctx));
			serpent_set_key(&ctx, key17, 129);
			fprintf(stderr, "child: returned without fault\n");
			_exit(0);
		}
		int st = 0;
		waitpid(p, &st, 0);
		if (WIFSIGNALED(st) ||
		    (WIFEXITED(st) && WEXITSTATUS(st) >= 128 &&
		     WEXITSTATUS(st) <= 140)) {
			printf("%-28s child SIGSEGV (OOB read confirmed)\n",
			    "key_len=129/guard");
			evid++;
		} else {
			printf("%-28s no fault (status %d)\n", "key_len=129",
			    st);
		}
		munmap(base, 2 * pg);
	}

	printf("== RESULT: %d/3 fault demonstrations ==\n", evid);
	return (evid == 3 ? 0 : 1);
}