etc.git


etc

~ scripts and random things
git clone https://github.com/djnnvx/etc.git
log | files | refs | archive

fuzz_stub_client.c (16159B)

      1 /*
      2  * fuzz_stub_client.c — Malicious USB-IP client targeting usbipd stub_rx.c.
      3  *
      4  * WHAT THIS TESTS
      5  * ────────────────
      6  * The stub side (usbipd running in the QEMU VM) receives CMD_SUBMIT packets
      7  * from a USB-IP client and calls stub_recv_cmd_submit().  That function has:
      8  *
      9  *   1. No upper bound on transfer_buffer_length before sgl_alloc() —
     10  *      a huge value maps to a giant scatter-gather list → kernel OOM / KASAN.
     11  *
     12  *   2. Integer overflow in number_of_packets for ISO endpoints:
     13  *        kmalloc(number_of_packets * sizeof(struct usbip_iso_packet_descriptor))
     14  *      Same class as the vhci-hcd recv_iso bug but on the stub side.
     15  *
     16  * ARCHITECTURE
     17  * ─────────────
     18  *   VM   : usbipd running, a real USB device (or dummy_hcd) bound and exported
     19  *   Host : fuzz_stub_client <testcase> <vm-ip> <port>  (AFL++ target)
     20  *
     21  *   1. Client TCP-connects to usbipd on <vm-ip>:<port> (default 3240).
     22  *   2. Sends OP_REQ_IMPORT to claim the exported device (busid "1-1").
     23  *   3. Reads OP_REP_IMPORT (import accepted → now in tunnel mode).
     24  *   4. Sends one or more CMD_SUBMIT packets whose fields are taken from the
     25  *      AFL++ test-case bytes.  The 48-byte CMD_SUBMIT header union is the
     26  *      primary mutation target.
     27  *   5. Optionally reads and discards RET_SUBMIT from the stub.
     28  *   6. Exits; AFL++ records a crash if the VM panics and usbipd drops the
     29  *      connection (ECONNRESET → send_all returns -1 → main returns 1).
     30  *
     31  * AFL++ INVOCATION
     32  * ─────────────────
     33  *   afl-fuzz -S stub -i corpus/stub -o output -t 5000 \
     34  *     -- ./fuzz_stub_client @@ <vm-ip> 3240
     35  *
     36  * COMPILE
     37  * ────────
     38  *   afl-clang-fast -O2 -o fuzz_stub_client fuzz_stub_client.c
     39  */
     40 
     41 #include <arpa/inet.h>
     42 #include <errno.h>
     43 #include <fcntl.h>
     44 #include <netinet/in.h>
     45 #include <netinet/tcp.h>
     46 #include <signal.h>
     47 #include <stdint.h>
     48 #include <stdio.h>
     49 #include <stdlib.h>
     50 #include <string.h>
     51 #include <sys/socket.h>
     52 #include <sys/stat.h>
     53 #include <sys/time.h>
     54 #include <unistd.h>
     55 
     56 /* ── USB/IP protocol constants ─────────────────────────────────────────────── */
     57 #define USBIP_VERSION       0x0111
     58 
     59 #define OP_REQ_IMPORT       0x8003
     60 #define OP_REP_IMPORT       0x0003
     61 #define OP_REQ_DEVLIST      0x8005
     62 #define OP_REP_DEVLIST      0x0005
     63 
     64 #define USBIP_CMD_SUBMIT    0x00000001
     65 #define USBIP_CMD_UNLINK    0x00000002
     66 #define USBIP_RET_SUBMIT    0x00000003
     67 
     68 #define SYSFS_BUS_ID_SIZE   32
     69 #define SYSFS_PATH_MAX      256
     70 
     71 /* Maximum out-payload we'll actually send after a CMD_SUBMIT header */
     72 #define MAX_OUT_PAYLOAD     (1 << 16)   /* 64 KB */
     73 
     74 /* ── wire-format structures ─────────────────────────────────────────────────── */
     75 
     76 struct op_common {
     77     uint16_t version;
     78     uint16_t code;
     79     uint32_t status;
     80 } __attribute__((packed));
     81 
     82 struct op_req_import {
     83     struct op_common hdr;
     84     char             busid[SYSFS_BUS_ID_SIZE];
     85 } __attribute__((packed));
     86 
     87 struct op_devlist_reply {
     88     uint32_t ndev;
     89 } __attribute__((packed));
     90 
     91 struct usbip_usb_device {
     92     char     path[SYSFS_PATH_MAX];
     93     char     busid[SYSFS_BUS_ID_SIZE];
     94     uint32_t busnum;
     95     uint32_t devnum;
     96     uint32_t speed;
     97     uint16_t idVendor;
     98     uint16_t idProduct;
     99     uint16_t bcdDevice;
    100     uint8_t  bDeviceClass;
    101     uint8_t  bDeviceSubClass;
    102     uint8_t  bDeviceProtocol;
    103     uint8_t  bConfigurationValue;
    104     uint8_t  bNumConfigurations;
    105     uint8_t  bNumInterfaces;
    106 } __attribute__((packed));
    107 
    108 struct usbip_usb_interface {
    109     uint8_t bInterfaceClass;
    110     uint8_t bInterfaceSubClass;
    111     uint8_t bInterfaceProtocol;
    112     uint8_t padding;
    113 } __attribute__((packed));
    114 
    115 struct op_rep_import {
    116     struct op_common        hdr;
    117     struct usbip_usb_device dev;
    118 } __attribute__((packed));
    119 
    120 /* 20-byte base header common to all URB PDUs */
    121 struct usbip_header_basic {
    122     uint32_t command;
    123     uint32_t seqnum;
    124     uint32_t devid;
    125     uint32_t direction;
    126     uint32_t ep;
    127 } __attribute__((packed));
    128 
    129 /* CMD_SUBMIT union fields (28 bytes) */
    130 struct usbip_header_cmd_submit {
    131     uint32_t transfer_flags;
    132     int32_t  transfer_buffer_length;
    133     int32_t  start_frame;
    134     int32_t  number_of_packets;
    135     int32_t  interval;
    136     uint8_t  setup[8];
    137 } __attribute__((packed));
    138 
    139 /* Full URB PDU header (48 bytes) */
    140 struct usbip_header {
    141     struct usbip_header_basic base;
    142     union {
    143         struct usbip_header_cmd_submit  cmd_submit;
    144         uint8_t                         raw[28];
    145     } u;
    146 } __attribute__((packed));
    147 
    148 /* ── helpers ────────────────────────────────────────────────────────────────── */
    149 
    150 static int recv_all(int fd, void *buf, size_t len)
    151 {
    152     size_t done = 0;
    153     while (done < len) {
    154         ssize_t r = recv(fd, (char *)buf + done, len - done, 0);
    155         if (r <= 0)
    156             return -1;
    157         done += (size_t)r;
    158     }
    159     return 0;
    160 }
    161 
    162 static int send_all(int fd, const void *buf, size_t len)
    163 {
    164     size_t done = 0;
    165     while (done < len) {
    166         ssize_t r = send(fd, (const char *)buf + done, len - done, MSG_NOSIGNAL);
    167         if (r <= 0)
    168             return -1;
    169         done += (size_t)r;
    170     }
    171     return 0;
    172 }
    173 
    174 /* ── import handshake ───────────────────────────────────────────────────────── */
    175 
    176 static int do_import(int sock, const char *busid)
    177 {
    178     struct op_req_import req;
    179     memset(&req, 0, sizeof(req));
    180     req.hdr.version = htons(USBIP_VERSION);
    181     req.hdr.code    = htons(OP_REQ_IMPORT);
    182     req.hdr.status  = htonl(0);
    183     strncpy(req.busid, busid, SYSFS_BUS_ID_SIZE - 1);
    184 
    185     if (send_all(sock, &req, sizeof(req)) < 0)
    186         return -1;
    187 
    188     /* Read the server's reply */
    189     struct op_rep_import rep;
    190     memset(&rep, 0, sizeof(rep));
    191     if (recv_all(sock, &rep, sizeof(rep)) < 0)
    192         return -1;
    193 
    194     uint16_t code   = ntohs(rep.hdr.code);
    195     uint32_t status = ntohl(rep.hdr.status);
    196     if (code != OP_REP_IMPORT || status != 0)
    197         return -1;
    198 
    199     return 0;
    200 }
    201 
    202 /* Query OP_REQ_DEVLIST and return first busid found. */
    203 static int discover_busid(int sock, char out_busid[SYSFS_BUS_ID_SIZE])
    204 {
    205     struct op_common req;
    206     memset(&req, 0, sizeof(req));
    207     req.version = htons(USBIP_VERSION);
    208     req.code    = htons(OP_REQ_DEVLIST);
    209     req.status  = htonl(0);
    210     if (send_all(sock, &req, sizeof(req)) < 0)
    211         return -1;
    212 
    213     struct op_common rep;
    214     memset(&rep, 0, sizeof(rep));
    215     if (recv_all(sock, &rep, sizeof(rep)) < 0)
    216         return -1;
    217     if (ntohs(rep.code) != OP_REP_DEVLIST || ntohl(rep.status) != 0)
    218         return -1;
    219 
    220     struct op_devlist_reply dr;
    221     memset(&dr, 0, sizeof(dr));
    222     if (recv_all(sock, &dr, sizeof(dr)) < 0)
    223         return -1;
    224 
    225     uint32_t ndev = ntohl(dr.ndev);
    226     if (ndev == 0)
    227         return -1;
    228     if (ndev > 1024)
    229         ndev = 1024;
    230 
    231     out_busid[0] = '\0';
    232 
    233     for (uint32_t i = 0; i < ndev; i++) {
    234         struct usbip_usb_device dev;
    235         memset(&dev, 0, sizeof(dev));
    236         if (recv_all(sock, &dev, sizeof(dev)) < 0)
    237             return -1;
    238 
    239         if (out_busid[0] == '\0') {
    240             memcpy(out_busid, dev.busid, SYSFS_BUS_ID_SIZE);
    241             out_busid[SYSFS_BUS_ID_SIZE - 1] = '\0';
    242         }
    243 
    244         uint8_t nif = dev.bNumInterfaces;
    245         if (nif > 64)
    246             nif = 64;
    247         for (uint8_t j = 0; j < nif; j++) {
    248             struct usbip_usb_interface inf;
    249             if (recv_all(sock, &inf, sizeof(inf)) < 0)
    250                 return -1;
    251         }
    252     }
    253 
    254     return out_busid[0] == '\0' ? -1 : 0;
    255 }
    256 
    257 /* ── fuzzing loop ───────────────────────────────────────────────────────────── *
    258  *
    259  * Interprets the fuzz input as a stream of CMD_SUBMIT packets:
    260  *   [48-byte header] [transfer_buffer_length bytes OUT payload]
    261  *   [48-byte header] ...
    262  *
    263  * The 48-byte header is taken verbatim from fuzz bytes (with command and seqnum
    264  * fixed to valid values so usbipd doesn't drop the packet before parsing the
    265  * fields we care about: transfer_buffer_length, number_of_packets).
    266  *
    267  * transfer_buffer_length is clamped for the actual payload we send, but the
    268  * stub receives the UNCLAMPED value from the header, which is what triggers the
    269  * sgl_alloc() bug in stub_recv_cmd_submit().
    270  */
    271 
    272 static int fuzz_loop(int sock, const uint8_t *fuzz, size_t fuzz_len)
    273 {
    274     size_t   fuzz_off       = 0;
    275     uint32_t seqnum         = 1;
    276     uint32_t last_submit_seq = 0;   /* last CMD_SUBMIT seqnum, for CMD_UNLINK */
    277 
    278     /* OUT payload — static buffer of poison bytes */
    279     static uint8_t out_payload[MAX_OUT_PAYLOAD];
    280     memset(out_payload, 0xaa, sizeof(out_payload));
    281 
    282     while (fuzz_off + sizeof(struct usbip_header) <= fuzz_len) {
    283         struct usbip_header pdu;
    284         memset(&pdu, 0, sizeof(pdu));
    285 
    286         /* Overlay fuzz bytes onto the full 48-byte PDU header */
    287         memcpy(&pdu, fuzz + fuzz_off, sizeof(pdu));
    288         fuzz_off += sizeof(pdu);
    289 
    290         /*
    291          * Bit 0 of the fuzz command field selects the PDU type:
    292          *   0 → CMD_SUBMIT  (exercises sgl_alloc + ISO kmalloc overflow)
    293          *   1 → CMD_UNLINK  (exercises seqnum linked-list lookup + potential UAF)
    294          * CMD_UNLINK requires at least one prior CMD_SUBMIT for a valid seqnum
    295          * to reference; fall back to CMD_SUBMIT on the very first packet.
    296          */
    297         uint32_t fuzz_cmd = ntohl(pdu.base.command);
    298         if ((fuzz_cmd & 1) && last_submit_seq > 0) {
    299             /* ── CMD_UNLINK path ─────────────────────────────────────────── */
    300             pdu.base.command = htonl(USBIP_CMD_UNLINK);
    301             pdu.base.seqnum  = htonl(seqnum++);
    302             pdu.base.devid   = htonl(0x00010001);
    303             /* unlink_seqnum sits at the start of the 28-byte union (BE u32) */
    304             memset(pdu.u.raw, 0, sizeof(pdu.u.raw));
    305             *(uint32_t *)pdu.u.raw = htonl(last_submit_seq);
    306             if (send_all(sock, &pdu, sizeof(pdu)) < 0)
    307                 return 1;
    308             struct usbip_header ret_ul;
    309             recv(sock, &ret_ul, sizeof(ret_ul), MSG_DONTWAIT);
    310         } else {
    311             /* ── CMD_SUBMIT path ─────────────────────────────────────────── */
    312             pdu.base.command = htonl(USBIP_CMD_SUBMIT);
    313             pdu.base.seqnum  = htonl(seqnum);
    314             last_submit_seq  = seqnum++;
    315             /* devid is ignored by the stub once in tunnel mode */
    316             pdu.base.devid   = htonl(0x00010001);
    317 
    318             /* Send the CMD_SUBMIT header */
    319             if (send_all(sock, &pdu, sizeof(pdu)) < 0)
    320                 return 1;   /* ECONNRESET = stub panicked → AFL++ crash */
    321 
    322             /*
    323              * For OUT transfers (direction == 0) the stub expects
    324              * transfer_buffer_length bytes of payload after the header.
    325              * Send at most MAX_OUT_PAYLOAD bytes; the stub sees the full
    326              * (unclamped) length field in the header regardless.
    327              */
    328             uint32_t dir = ntohl(pdu.base.direction);
    329             if (dir == 0 /* USBIP_DIR_OUT */) {
    330                 int32_t tbl      = ntohl(pdu.u.cmd_submit.transfer_buffer_length);
    331                 int32_t send_len = tbl;
    332                 if (send_len < 0)               send_len = 0;
    333                 if (send_len > MAX_OUT_PAYLOAD)  send_len = MAX_OUT_PAYLOAD;
    334                 if (send_len > 0) {
    335                     if (send_all(sock, out_payload, (size_t)send_len) < 0)
    336                         return 1;
    337                 }
    338             }
    339 
    340             /*
    341              * Optionally drain a RET_SUBMIT from the stub (non-blocking).
    342              * We don't need to read it for the attack — the stub will crash
    343              * before replying if we've triggered the bug.  We just discard
    344              * whatever comes back to keep the pipe clear.
    345              */
    346             {
    347                 struct usbip_header ret;
    348                 recv(sock, &ret, sizeof(ret), MSG_DONTWAIT);
    349             }
    350         }
    351     }
    352 
    353     return 0;
    354 }
    355 
    356 /* ── entry point ─────────────────────────────────────────────────────────────── */
    357 
    358 static uint8_t fuzz_buf[1 << 16];
    359 
    360 int main(int argc, char *argv[])
    361 {
    362     if (argc < 4) {
    363         fprintf(stderr,
    364                 "usage: %s <testcase> <vm-ip> <port>\n"
    365                 "  testcase — AFL++ @@ input file (stream of 48-byte CMD_SUBMIT headers)\n"
    366                 "  vm-ip    — IP of QEMU VM running usbipd (e.g. 127.0.0.1)\n"
    367                 "  port     — usbipd port (default 3240)\n",
    368                 argv[0]);
    369         return 1;
    370     }
    371 
    372     const char *testcase = argv[1];
    373     const char *vmip     = argv[2];
    374     int         port     = atoi(argv[3]);
    375 
    376     int fd = open(testcase, O_RDONLY);
    377     if (fd < 0) { perror("open"); return 1; }
    378     ssize_t fuzz_len = read(fd, fuzz_buf, sizeof(fuzz_buf));
    379     close(fd);
    380     if (fuzz_len <= 0)
    381         return 0;
    382 
    383     signal(SIGPIPE, SIG_IGN);
    384 
    385     int sock = socket(AF_INET, SOCK_STREAM, 0);
    386     if (sock < 0) { perror("socket"); return 1; }
    387 
    388     /* Short connect timeout — VM might not be ready */
    389     struct timeval tv = { .tv_sec = 3, .tv_usec = 0 };
    390     setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    391     setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
    392 
    393     int one = 1;
    394     setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
    395 
    396     struct sockaddr_in sa;
    397     memset(&sa, 0, sizeof(sa));
    398     sa.sin_family = AF_INET;
    399     sa.sin_port   = htons((uint16_t)port);
    400     if (inet_pton(AF_INET, vmip, &sa.sin_addr) != 1) {
    401         fprintf(stderr, "bad address: %s\n", vmip);
    402         close(sock);
    403         return 1;
    404     }
    405 
    406     if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
    407         /* VM not reachable — not a crash */
    408         close(sock);
    409         return 0;
    410     }
    411 
    412     /* Widen I/O timeout for the fuzzing phase */
    413     struct timeval iotv = { .tv_sec = 0, .tv_usec = 500000 };
    414     setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &iotv, sizeof(iotv));
    415     setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &iotv, sizeof(iotv));
    416 
    417     char busid[SYSFS_BUS_ID_SIZE];
    418     memset(busid, 0, sizeof(busid));
    419 
    420     /*
    421      * Discover exported busid first. If this fails, fall back to 1-1.
    422      * Reconnect before IMPORT/fuzz so the tunnel starts from a clean socket.
    423      */
    424     int have_busid = (discover_busid(sock, busid) == 0);
    425     close(sock);
    426 
    427     sock = socket(AF_INET, SOCK_STREAM, 0);
    428     if (sock < 0) { perror("socket"); return 1; }
    429     setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &iotv, sizeof(iotv));
    430     setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &iotv, sizeof(iotv));
    431     setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
    432     if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
    433         close(sock);
    434         return 0;
    435     }
    436 
    437     const char *target_busid = have_busid ? busid : "1-1";
    438     if (do_import(sock, target_busid) < 0) {
    439         /* Import failed — try fallback busid before giving up. */
    440         if (strcmp(target_busid, "1-1") != 0 && do_import(sock, "1-1") == 0) {
    441             /* fallback succeeded */
    442         } else {
    443             close(sock);
    444             return 0;
    445         }
    446     }
    447 
    448     if (have_busid) {
    449         fprintf(stderr, "[stub] using discovered busid: %s\n", target_busid);
    450     } else {
    451         fprintf(stderr, "[stub] devlist discovery failed, fallback busid: 1-1\n");
    452     }
    453 
    454     {
    455         int rc = fuzz_loop(sock, fuzz_buf, (size_t)fuzz_len);
    456         close(sock);
    457         return rc;  /* 1 = ECONNRESET = kernel panic → AFL++ crash */
    458     }
    459 }