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 }