DF-3562 / serpoob.c
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 | /* * 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); } |