DF-0900 / overflow_demo.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 | /* * DF-0900 - Signed integer overflow in RPC reply verifier length * * This is a USERSPACE HARNESS that replicates the EXACT arithmetic the kernel * performs at sys/vfs/nfs/krpc_subr.c:415-428 in krpc_call()'s `gotreply` * path, to demonstrate that an attacker-controlled `authlen` field (read from * a network RPC reply) drives a signed `int len` into signed-overflow / * undefined-behavior territory, producing an INT_MIN argument to m_adj(). * * Why a harness and not a live trigger? * ------------------------------------- * krpc_call() is invoked ONLY from krpc_portmap() and the nfs_mountrpc * mount-rpc helpers, which are themselves reached only from the diskless * NFS-root bootstrap path (bootpc_init -> nfs_mountrpc -> krpc_*). The * nfs_mountrpc.c file header states verbatim: * "This module is not used by normal operating code because the 'mount' * command has a far more sophisticated implementation." * So there is NO userspace syscall on a normally-booted kernel that reaches * this code; the attacker model is an on-path network attacker or a * malicious NFS server during diskless boot. We cannot stage a diskless * NFS boot inside this audit guest, so we reproduce the kernel's own * arithmetic here to prove the overflow fires for the cited authlen values. * * The kernel code under test (verbatim from sys/vfs/nfs/krpc_subr.c): * * 415: len = sizeof(*reply); // = 24 * 416: if (m->m_len < len) { m = m_pullup(m, len); ... } * 423: reply = mtod(m, struct rpc_reply *); * 424: if (reply->rp_auth.authtype != 0) { * 425: len += fxdr_unsigned(u_int32_t, reply->rp_auth.authlen); * 426: len = (len + 3) & ~3; // XXX? * 427: } * 428: m_adj(m, len); * * `fxdr_unsigned(u_int32_t, x)` byte-swaps the on-wire big-endian u_int32_t * into host order. `authlen` is the verifier length field the attacker * controls in the RPC reply. `len` is a plain signed `int` (declared at * krpc_subr.c:200: `int error, rcvflg, timo, secs, len;`). * * With authlen = 0x7FFFFFE8: 24 + 0x7FFFFFE8 = 0x80000000 = INT_MIN * -> (INT_MIN + 3) & ~3 = INT_MIN * -> m_adj(m, INT_MIN) // req_len < 0 branch: len = -len; (UB), then * // m->m_len -= len corrupts m_len. * * Build: cc -O2 -Wall -o overflow_demo overflow_demo.c * Run: ./overflow_demo */ #include <stdio.h> #include <stdint.h> #include <stddef.h> #include <limits.h> /* Mirror of the kernel struct layout (sys/vfs/nfs/krpc_subr.c:72-111). */ struct auth_info { uint32_t authtype; uint32_t authlen; }; struct rpc_reply_partial { uint32_t rp_xid; /* 4 */ int32_t rp_direction; /* 4 */ int32_t rp_astatus; /* 4 */ /* union { u_int32_t errno; struct { auth_info(8) + uint32(4) } rok } */ uint32_t rok[3]; /* 12 */ }; #define RPCAUTH_MAXSIZ 400 /* sys/vfs/nfs/rpcv2.h:55 */ /* What m_adj() does to a single-mbuf m_len, condensed from * sys/kern/uipc_mbuf.c:1859-1924. */ static long m_adj_single_mbuf(int req_len, int m_len_in) { int len = req_len; long m_len = m_len_in; if (len >= 0) { /* head trim */ if (m_len <= len) m_len = 0; else m_len -= len; } else { len = -len; /* -INT_MIN is UB; compiler leaves INT_MIN */ if (m_len >= len) m_len -= len; /* m_len - INT_MIN overflows */ else m_len = 0; } return m_len; } int main(void) { int sizeof_reply = (int)sizeof(struct rpc_reply_partial); printf("DF-0900 harness: krpc_subr.c:415-428 signed-int overflow demo\n"); printf("sizeof(struct rpc_reply) = %d (kernel len initial value)\n", sizeof_reply); printf("RPCAUTH_MAXSIZ = %d (the bound the code lacks)\n\n", RPCAUTH_MAXSIZ); printf("%-12s %-22s %-22s %-22s %s\n", "authlen", "len_after_add", "len_after_align", "m_adj_req_len", "single-mbuf m_len"); struct { uint32_t authlen; int m_len_in; const char *tag; } cases[] = { { 0, 100, "benign (no overflow)" }, { 400, 100, "exactly RPCAUTH_MAXSIZ (would be allowed by fix)" }, { 401, 100, "just over RPCAUTH_MAXSIZ (fix -> EBADRPC)" }, { 0x7FFFFFFFu, 100, "authlen = INT_MAX" }, { 0x7FFFFFE8u, 100, "authlen = 0x7FFFFFE8 (cited overflow trigger)" }, { 0x80000000u, 100, "authlen = 0x80000000" }, { 0xFFFFFFFFu, 100, "authlen = 0xFFFFFFFF (max u32)" }, }; for (size_t i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) { uint32_t a = cases[i].authlen; /* Reproduce the two arithmetic steps separately so the reader sees * exactly where signed overflow happens. */ int len_after_add = sizeof_reply + (int)a; /* :425 */ int len_after_align = (len_after_add + 3) & ~3; /* :426 */ long m_len_after = m_adj_single_mbuf(len_after_align, cases[i].m_len_in); printf("0x%08x %-22d %-22d %-22d %ld %s\n", a, len_after_add, len_after_align, len_after_align, m_len_after, cases[i].tag); if (a == 0x7FFFFFE8u) { printf(" ** BUG FIRES: sizeof_reply(%d) + authlen(0x7FFFFFE8) = " "0x%08x = INT_MIN (signed overflow, UB)\n", sizeof_reply, (unsigned)len_after_add); printf(" ** (len+3)&~3 = INT_MIN, passed to m_adj() as req_len.\n"); if (m_len_after < 0 || m_len_after > cases[i].m_len_in) { printf(" ** m_len corrupted: %d -> %ld (OOB on next mbuf op)\n", cases[i].m_len_in, m_len_after); } } } printf("\nVerdict: the cited path performs signed `int` arithmetic on an\n"); printf("attacker-controlled u_int32_t (`authlen`) without any upper bound.\n"); printf("For authlen >= 0x7FFFFFE8 the sum overflows to INT_MIN/UB.\n"); printf("The kernel's own NFS socket path (sys/vfs/nfs/nfs_socket.c:2347,2385)\n"); printf("already guards this with `if (len < 0 || len > RPCAUTH_MAXSIZ)`;\n"); printf("krpc_subr.c:425 omits that guard.\n"); return 0; } |