โฌข DragonFlyBSD Kernel Audit
DF-0617 / uaf_ng_ether.c
โ† back to finding โ†“ download raw
  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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
/*
 * DF-0617 โ€” Code-level harness proving the Use-After-Free in
 * ng_ether_rcv_upper() (sys/netgraph7/ether/ng_ether.c:640-666).
 *
 * The vulnerability: bridge_input_p() is called as a statement-expression
 * (return value discarded) at ng_ether.c:658. bridge_input() returns NULL
 * when it consumes/frees the mbuf (IFF_MONITOR, packet-for-bridge-MAC, BPDU,
 * bridge_forward, ether_reinput_oncpu, sender's-own-MAC โ€” see
 * sys/net/bridge/if_bridge.c:2646-2662, 2804-2805, 2936-2960, 2973-2989).
 *
 * Because the return value is discarded, the local variable `m` is never
 * updated and the subsequent `if (m == NULL) return (0);` at line 659 is
 * dead code. Execution falls through to ether_demux_oncpu(ifp, m) at line
 * 664, which dereferences the freed mbuf:
 *   M_ASSERTPKTHDR(m)              โ€” reads m->m_flags          (if_ethersubr.c:992)
 *   KASSERT(m->m_len >= ...)       โ€” reads m->m_len            (if_ethersubr.c:993)
 *   eh = mtod(m, ...)              โ€” dereferences m->m_data    (if_ethersubr.c:996)
 *
 * The canonical correct pattern at sys/net/if_ethersubr.c:1252 is:
 *   m = bridge_input_p(ifp, m);
 *   if (m == NULL) return;
 *
 * This harness replicates the EXACT control flow of ng_ether_rcv_upper()
 * with a poisoned-freed-memory allocator: when bridge_input_p() frees the
 * mbuf, its backing memory is overwritten with 0xDE bytes. The UAF is then
 * observable: ether_demux_oncpu() reads the poison pattern from the freed
 * mbuf. Two modes are provided:
 *
 *   --buggy : replicates the current kernel code (return value discarded)
 *   --fixed : replicates the one-line fix (m = bridge_input_p(ifp, m))
 *
 * Build:  cc -O2 -o uaf_ng_ether uaf_ng_ether.c
 * Run:    ./uaf_ng_ether --buggy   (shows UAF: freed mbuf accessed)
 *         ./uaf_ng_ether --fixed   (shows fix: early return, no access)
 *         ./uaf_ng_ether           (runs both, summary)
 *
 * NOTE: This is a code-level proof. A live runtime trigger requires the
 * netgraph7 ng_ether + if_bridge topology on a bridged NIC, which causes
 * guest instability on this QEMU/vtnet0 guest (the ng_ether input-orphan
 * hooks interfere with normal bridge traffic processing, hanging the guest
 * before the upper-hook injection path can be exercised). The control flow
 * replicated here is an exact transcription of the kernel source.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

/* ---- Simulated kernel structures (minimal fields for the UAF path) ---- */

struct ether_header {
    uint8_t  ether_dhost[6];
    uint8_t  ether_shost[6];
    uint16_t ether_type;
} __attribute__((packed));

#define ETHER_HDR_LEN   14
#define ETHER_ADDR_LEN  6
#define M_PKTHDR        0x00000002   /* m_flags: packet header present */
#define POISON_BYTE     0xDE

struct mbuf {
    /* The fields accessed by the vulnerable code path, in access order: */
    uint32_t  m_flags;          /* M_ASSERTPKTHDR checks M_PKTHDR        */
    int       m_len;            /* KASSERT(m_len >= ETHER_HDR_LEN)       */
    void     *m_data;           /* mtod() dereferences this              */
    int       m_pkthdr_len;     /* ng_ether_rcv_upper line 646           */
    struct {
        int len;                /* m->m_pkthdr.len                      */
        void *rcvif;            /* m->m_pkthdr.rcvif (line 654)          */
    } m_pkthdr;
    /* padding so the allocation is a realistic size */
    uint8_t   pad[224];
};

struct ifnet {
    int       if_flags;
    void     *if_bridge;        /* non-NULL => bridge member             */
};

/* IFF_MONITOR on the bridge interface */
#define IFF_MONITOR   0x00040000
#define IFF_RUNNING   0x00000040

/* ---- Poisoned allocator: freed memory is overwritten with 0xDE ----
 *
 * Uses a STATIC pool (not malloc/free) so the compiler cannot optimize
 * away the poison write as a "dead store before free" (which -O2 does
 * with real malloc/free due to the UB of accessing freed memory).
 *
 * In the real kernel, m_freem() returns the mbuf to the slab allocator;
 * the backing page may be reused immediately for a different object.
 * Our static pool simulates this: freed mbufs are poisoned with 0xDE
 * and flagged, and the poison persists so the UAF is observable. */

static int g_freed_count = 0;

#define POOL_SIZE 4
static struct mbuf m_pool[POOL_SIZE];
static int m_pool_used[POOL_SIZE];  /* 0=free, 1=allocated */

static struct mbuf *
mock_m_gethdr(int len)
{
    for (int i = 0; i < POOL_SIZE; i++) {
        if (!m_pool_used[i]) {
            struct mbuf *m = &m_pool[i];
            m_pool_used[i] = 1;
            memset(m, 0, sizeof(*m));
            m->m_flags = M_PKTHDR;
            m->m_len = len;
            m->m_pkthdr.len = len;
            m->m_pkthdr.rcvif = NULL;
            /* m_data points to a simulated packet buffer right after the header */
            m->m_data = (void *)((uint8_t *)m + sizeof(*m) - 64);
            /* Write a recognizable Ethernet header into the data area */
            struct ether_header *eh = (struct ether_header *)m->m_data;
            memset(eh->ether_dhost, 0xAA, ETHER_ADDR_LEN);
            memset(eh->ether_shost, 0xBB, ETHER_ADDR_LEN);
            eh->ether_type = 0x0800;
            return m;
        }
    }
    fprintf(stderr, "mbuf pool exhausted\n");
    abort();
    return NULL;
}

static void
mock_m_freem(struct mbuf *m)
{
    /* Simulate m_freem(): poison the mbuf's backing memory with 0xDE.
     * In the real kernel, the slab allocator would record the free and
     * the page could be reclaimed for a different object type. The poison
     * pattern makes the subsequent UAF access observable. */
    if (m) {
        /* volatile sink prevents the compiler from eliding the write */
        volatile unsigned char *p = (volatile unsigned char *)m;
        size_t n = sizeof(*m);
        while (n--) *p++ = POISON_BYTE;
        g_freed_count++;
    }
}

/* ---- Simulated bridge_input() โ€” the IFF_MONITOR NULL-return path ----
 *
 * Transcribed from sys/net/bridge/if_bridge.c:2615-2662:
 *
 *   static struct mbuf *
 *   bridge_input(struct ifnet *ifp, struct mbuf *m)
 *   {
 *       struct bridge_softc *sc = ifp->if_bridge;
 *       ...
 *       if (sc == NULL)
 *           return m;                        // line 2631-2632
 *       ...
 *       if ((bifp->if_flags & IFF_RUNNING) == 0)
 *           goto out;                        // line 2637-2638 -> returns m
 *       ...
 *       if (bifp->if_flags & IFF_MONITOR) { // line 2646
 *           ...
 *           m_freem(m);                      // line 2660
 *           m = NULL;                        // line 2661
 *           goto out;                        // line 2662 -> returns NULL
 *       }
 *       ...
 *   }
 *
 * When IFF_MONITOR is set on the bridge, bridge_input() ALWAYS frees the
 * mbuf and returns NULL, regardless of the packet content.
 */
static struct mbuf *
mock_bridge_input(struct ifnet *ifp, struct mbuf *m)
{
    /* ifp->if_bridge is the bridge softc; non-NULL means we're a member */
    if (ifp->if_bridge == NULL)
        return m;             /* not a bridge member โ€” return untouched */

    /* Simulate bifp (the bridge interface) having IFF_MONITOR set */
    int bifp_flags = IFF_RUNNING | IFF_MONITOR;

    if ((bifp_flags & IFF_RUNNING) == 0)
        return m;             /* bridge not running โ€” return untouched   */

    if (bifp_flags & IFF_MONITOR) {
        /* IFF_MONITOR path (if_bridge.c:2660-2662):
         *   m_freem(m);
         *   m = NULL;
         *   goto out;   // return NULL
         */
        mock_m_freem(m);
        return NULL;          /* mbuf consumed/freed, return NULL        */
    }

    return m;                 /* not consumed โ€” return original mbuf      */
}

/* Function pointer matching the kernel's bridge_input_p */
static struct mbuf *(*bridge_input_p)(struct ifnet *, struct mbuf *) = mock_bridge_input;

/* ---- Simulated ether_demux_oncpu() โ€” the UAF sink ----
 *
 * Transcribed from sys/net/if_ethersubr.c:985-996:
 *
 *   void ether_demux_oncpu(struct ifnet *ifp, struct mbuf *m)
 *   {
 *       M_ASSERTPKTHDR(m);                              // line 992
 *       KASSERT(m->m_len >= ETHER_HDR_LEN, ...);        // line 993
 *       eh = mtod(m, struct ether_header *);            // line 996
 *       ...
 *   }
 */
static int g_uaf_detected = 0;

static void
mock_ether_demux_oncpu(struct ifnet *ifp, struct mbuf *m)
{
    /* M_ASSERTPKTHDR(m) โ€” reads m->m_flags (if_ethersubr.c:992) */
    uint32_t flags_read = m->m_flags;

    /* KASSERT(m->m_len >= ETHER_HDR_LEN) โ€” reads m->m_len (if_ethersubr.c:993) */
    int len_read = m->m_len;

    /* eh = mtod(m, struct ether_header *) โ€” dereferences m->m_data (line 996) */
    struct ether_header *eh = (struct ether_header *)m->m_data;

    /* Detect UAF: if the mbuf was freed and poisoned, ALL fields are 0xDE.
     * m_flags would be 0xDEDEDEDE and m_len would be (int)0xDEDEDEDE. */
    int poisoned = (flags_read == 0xDEDEDEDEU);

    printf("    ether_demux_oncpu: m->m_flags=0x%08x  m->m_len=%d  m->m_data=%p\n",
           flags_read, len_read, m->m_data);

    if (poisoned) {
        printf("    *** UAF DETECTED: mbuf was freed (all fields poisoned to 0x%02x) "
               "but still dereferenced! ***\n", POISON_BYTE);
        g_uaf_detected++;
    } else if (flags_read & M_PKTHDR) {
        printf("    ether_demux_oncpu: valid mbuf (m_flags has M_PKTHDR), "
               "ether_type=0x%04x โ€” normal processing\n",
               eh ? eh->ether_type : 0);
    } else {
        printf("    ether_demux_oncpu: mbuf in unexpected state (m_flags=0x%08x)\n",
               flags_read);
    }

    (void)ifp;
}

/* ---- ng_ether_rcv_upper โ€” EXACT transcription of the kernel code ----
 *
 * sys/netgraph7/ether/ng_ether.c:639-666
 *
 * static int
 * ng_ether_rcv_upper(node_p node, struct mbuf *m)
 * {
 *     const priv_p priv = NG_NODE_PRIVATE(node);
 *     struct ifnet *ifp = priv->ifp;
 *
 *     // Check length and pull off header
 *     if (m->m_pkthdr.len < sizeof(struct ether_header)) {   // line 646
 *         NG_FREE_M(m);
 *         return (EINVAL);
 *     }
 *     ...
 *     m->m_pkthdr.rcvif = ifp;                               // line 654
 *
 *     // Pass the packet to the bridge, it may come back to us
 *     if (ifp->if_bridge) {                                  // line 657
 *         bridge_input_p(ifp, m);          <-- BUG: return value discarded (line 658)
 *         if (m == NULL)                   <-- DEAD CODE     (line 659)
 *             return (0);
 *     }
 *
 *     // Route packet back in
 *     ether_demux_oncpu(ifp, m);           <-- UAF SINK      (line 664)
 *     return (0);
 * }
 */

static int
ng_ether_rcv_upper_BUGGY(struct ifnet *ifp, struct mbuf *m)
{
    /* line 646: length check */
    if (m->m_pkthdr.len < ETHER_HDR_LEN) {
        mock_m_freem(m);
        return -1;  /* EINVAL */
    }
    /* line 650-652: m_pullup (omitted โ€” m_len is already >= ETHER_HDR_LEN) */

    /* line 654: m->m_pkthdr.rcvif = ifp */
    m->m_pkthdr.rcvif = ifp;

    /* line 657-661: THE BUG */
    if (ifp->if_bridge) {
        /* BUGGY: bridge_input_p(ifp, m); โ€” return value DISCARDED */
        bridge_input_p(ifp, m);           /* line 658: m NOT updated */
        if (m == NULL)                    /* line 659: DEAD โ€” m is never NULL */
            return 0;
    }

    /* line 664: UAF SINK โ€” m was freed inside bridge_input_p but the local
     * pointer still points at the freed/poisoned memory */
    printf("  [BUGGY] Falling through to ether_demux_oncpu with freed mbuf %p\n", m);
    mock_ether_demux_oncpu(ifp, m);
    return 0;
}

static int
ng_ether_rcv_upper_FIXED(struct ifnet *ifp, struct mbuf *m)
{
    /* line 646: length check */
    if (m->m_pkthdr.len < ETHER_HDR_LEN) {
        mock_m_freem(m);
        return -1;  /* EINVAL */
    }

    /* line 654: m->m_pkthdr.rcvif = ifp */
    m->m_pkthdr.rcvif = ifp;

    /* line 657-661: THE FIX โ€” capture the return value */
    if (ifp->if_bridge) {
        /* FIXED: m = bridge_input_p(ifp, m); โ€” return value CAPTURED */
        m = bridge_input_p(ifp, m);       /* line 658: m IS updated */
        if (m == NULL)                    /* line 659: NOW FIRES */
            return 0;                     /* early return โ€” no UAF */
    }

    /* line 664: only reached if bridge did NOT consume the mbuf */
    printf("  [FIXED] Falling through to ether_demux_oncpu with valid mbuf %p\n", m);
    mock_ether_demux_oncpu(ifp, m);
    return 0;
}

/* ---- Main: run both modes and report ---- */

int main(int argc, char **argv)
{
    int run_buggy = 1, run_fixed = 1;

    if (argc > 1) {
        if (strcmp(argv[1], "--buggy") == 0) { run_fixed = 0; }
        else if (strcmp(argv[1], "--fixed") == 0) { run_buggy = 0; }
        else {
            fprintf(stderr, "Usage: %s [--buggy|--fixed]\n", argv[0]);
            return 2;
        }
    }

    /* Topology: ifp is a bridge member (if_bridge != NULL), bridge has
     * IFF_MONITOR set โ€” the most common deterministic NULL-return path. */
    struct ifnet ifp_obj;
    memset(&ifp_obj, 0, sizeof(ifp_obj));
    ifp_obj.if_bridge = (void *)0xCAFE0000;  /* non-NULL: we are a bridge member */

    printf("=== DF-0617: ng_ether_rcv_upper bridge_input UAF harness ===\n");
    printf("Topology: ifp->if_bridge=%p (bridge member), bridge IFF_MONITOR set\n",
           ifp_obj.if_bridge);
    printf("bridge_input() IFF_MONITOR path: m_freem(m) + return NULL\n\n");

    int buggy_uaf = 0, fixed_uaf = 0;

    if (run_buggy) {
        printf("--- BUGGY mode (current kernel: ng_ether.c:658 discards return value) ---\n");
        g_freed_count = 0;
        g_uaf_detected = 0;
        struct mbuf *m = mock_m_gethdr(60);
        printf("  Allocated mbuf %p (m_flags=0x%08x, m_len=%d)\n",
               m, m->m_flags, m->m_len);
        ng_ether_rcv_upper_BUGGY(&ifp_obj, m);
        printf("  Result: freed_count=%d, uaf_detected=%d\n\n",
               g_freed_count, g_uaf_detected);
        buggy_uaf = g_uaf_detected;
    }

    if (run_fixed) {
        printf("--- FIXED mode (m = bridge_input_p(ifp, m); โ€” return value captured) ---\n");
        g_freed_count = 0;
        g_uaf_detected = 0;
        struct mbuf *m = mock_m_gethdr(60);
        printf("  Allocated mbuf %p (m_flags=0x%08x, m_len=%d)\n",
               m, m->m_flags, m->m_len);
        ng_ether_rcv_upper_FIXED(&ifp_obj, m);
        printf("  Result: freed_count=%d, uaf_detected=%d\n\n",
               g_freed_count, g_uaf_detected);
        fixed_uaf = g_uaf_detected;
    }

    /* ---- Summary ---- */
    printf("=== SUMMARY ===\n");
    if (run_buggy) {
        printf("BUGGY: %s โ€” %s\n",
               buggy_uaf ? "UAF CONFIRMED" : "no UAF",
               buggy_uaf ? "freed mbuf dereferenced in ether_demux_oncpu"
                         : "(unexpected)");
    }
    if (run_fixed) {
        printf("FIXED: %s โ€” %s\n",
               fixed_uaf ? "UAF STILL PRESENT" : "UAF ELIMINATED",
               fixed_uaf ? "freed mbuf still dereferenced"
                         : "early return before ether_demux_oncpu");
    }
    if (run_buggy && run_fixed) {
        printf("\nVerdict: %s\n",
               (buggy_uaf && !fixed_uaf)
                   ? "REPRODUCED โ€” the one-line fix (m = bridge_input_p(...)) "
                     "eliminates the UAF"
               : (buggy_uaf && fixed_uaf)
                   ? "FIX FAILED โ€” UAF present in both modes"
                   : "UNEXPECTED โ€” investigate");
    }

    return (run_buggy && buggy_uaf) ? 0 : 1;
}