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
150 changes: 150 additions & 0 deletions pocs/linux/kernelctf/CVE-2025-21756_cos_2/docs/exploit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Vulnerability
This vulnerability occurs because the `vsock_remove_sock()` function called in the `vsock_connect()` flow calls `vsock_remove_bound()` and decrements the vsk’s reference count even though the vsk is not bound [1].

```c
void vsock_remove_sock(struct vsock_sock *vsk)
{
vsock_remove_bound(vsk); // [1]
vsock_remove_connected(vsk);
}
EXPORT_SYMBOL_GPL(vsock_remove_sock);
```

The procedure to trigger the UAF is as follows. First, create a vsock, then call bind() and use getsockname() to obtain the addr information. `In the vsock_bind()` flow, when binding with `VMADDR_PORT_ANY`, a random `port` value is stored in a static variable and then used, so we need to read that `port` value from the addr information [2].

```c
static int __vsock_bind_connectible(struct vsock_sock *vsk,
struct sockaddr_vm *addr)
{
static u32 port; // [2]
struct sockaddr_vm new_addr;

if (!port)
port = get_random_u32_above(LAST_RESERVED_PORT);

vsock_addr_init(&new_addr, addr->svm_cid, addr->svm_port);

if (addr->svm_port == VMADDR_PORT_ANY) {
bool found = false;
unsigned int i;

for (i = 0; i < MAX_PORT_RETRIES; i++) { // [3]
if (port <= LAST_RESERVED_PORT)
port = LAST_RESERVED_PORT + 1;

new_addr.svm_port = port++;

if (!__vsock_find_bound_socket(&new_addr)) {
found = true;
break;
}
}

if (!found)
return -EADDRNOTAVAIL; // [4]
} else {
```

Next, starting from the retrieved `port` value, create and bind vsocks up to `MAX_PORT_RETRIES`. In `__vsock_bind_connectible()`, it loops `MAX_PORT_RETRIES` times from the given port to search for an available port number [3], and if none is found, it returns `-EADDRNOTAVAIL` [4]. In other words, this `bind()` operation is intended to force `__vsock_bind_connectible()` to return an error on a subsequent call.

Now, when `connect()` is called, `__vsock_bind_connectible()` is invoked in the following flow:

```c
vsock_connect()
=> vsock_auto_bind()
=> __vsock_bind()
=> __vsock_bind_connectible()
```

In the auto-bind flow of `vsock_connect()`, the function `__vsock_bind_connectible()` is called with the argument `VMADDR_PORT_ANY`, causing it to attempt to bind based on the `port` value[2]. However, because in the previous step we already created and bound vsocks starting from that `port` value for `MAX_PORT_RETRIES`, it fails to find an available `port` and returns an error [4].

Next, the `svm_cid` value is changed and `connect()` is attempted again. This triggers the vulnerable `vsock_remove_sock()` function in the flow that releases the existing transport[5].

```c
vsock_connect()
=> vsock_assign_transport()
=> virtio_transport_release()
=> virtio_transport_remove_sock()
=> vsock_remove_sock() // [5]
=> vsock_remove_bound()
=> __vsock_remove_bound()
=> sock_put(&vsk->sk)
```

Because the bind() failed in the previous step [4], the socket should not be removed from the bound table, but without any check it calls `__vsock_remove_bound()`, decrementing the vsk’s refcnt by 1 [6]. As a result, the vsk’s refcnt is now 1.

```c
static void __vsock_remove_bound(struct vsock_sock *vsk)
{
list_del_init(&vsk->bound_table);
sock_put(&vsk->sk); // [6]
}
```

Finally, calling `bind(VMADDR_PORT_ANY)` triggers the `__vsock_bind_connectible()` function. This function calls `__vsock_remove_bound()` at the end, decrementing the vsk’s refcnt by 1. As a result of that function call, the refcnt reaches 0 and the vsk is freed [7].

```c
static int __vsock_bind_connectible(struct vsock_sock *vsk,
struct sockaddr_vm *addr)
{
...

/* Remove connection oriented sockets from the unbound list and add them
* to the hash table for easy lookup by its address. The unbound list
* is simply an extra entry at the end of the hash table, a trick used
* by AF_UNIX.
*/
__vsock_remove_bound(vsk); // [7]
__vsock_insert_bound(vsock_bound_sockets(&vsk->local_addr), vsk);

return 0;
```

Then it calls `__vsock_insert_bound()`, which references the already freed vsk and thus triggers a Use-After-Free.

# COS exploit (`cos-105-17412.495.75`)

This is the COS-only resubmission of exp237 from [PR #205](https://github.com/google/security-research/pull/205). The original archive is preserved in `original.tar.gz`; the buildable exploit has been updated for the exact COS image.

## KASLR and target offsets

The exploit uses [AVX masked-load timing](https://arxiv.org/pdf/2304.07940) in `kaslr.h`. A zero mask suppresses faults while it measures 512 kernel virtual-address slots at 2 MiB spacing. The exact COS image gives a stable five-peak pattern on AMD (relative slots `10, 18, 23, 24, 26`) and a complementary Intel pattern. The implementation scores these target-specific signatures and requires a three-of-five vote across independent profiles. It runs as the unprivileged exploit user, so no separate KASLR leak is requested (`requires_separate_kaslr_leak: false`).

The target configuration sets `CONFIG_PHYSICAL_ALIGN=0x1000000`, so candidate bases are checked on a 16 MiB grid. The AVX signatures were measured on the exact COS image under GitHub-hosted EPYC 7763, EPYC 9V45 and Xeon 8573C runners, and on a local Intel host.

The unmapped AVX reference address is prepared before socket grooming. The timed scan runs after the vulnerable `bind()` and before the front slab is released, so verification kernels can report the UAF before a COS-specific mapping signature is needed. The scan itself performs no heap allocation.

For vulnerability verification, `./exploit --vuln-trigger` follows the same port-exhaustion and transport-reassignment path through the vulnerable `bind()`, then stops before the COS-specific KASLR scan and exploitation stages if the kernel does not report the UAF.

The following offsets were checked against the release's `vmlinux` (`cc53f55433a4e01ea51d28dde1594514252eace2`). They are added to the recovered kernel base, whose unrelocated value is `0xffffffff81000000`:

| Purpose | Offset |
| --- | ---: |
| Stack pivot (`mov rsp, rbp; pop rbp; pop r15; pop r13; pop r12`) | `0x93527` |
| `pop rdi; pop rsi; pop rdx; pop rcx` | `0x2bee9` |
| `_copy_from_user` | `0x7a41f0` |
| `msleep` | `0x170540` |
| `core_pattern` | `0x279eac0` |

## Reclaiming the dangling vsock

The original port-exhaustion and transport-reassignment sequence places a freed `vsock_sock` on the bound-socket list. The timerfd/epoll waiters delay the loopback worker while the surrounding vsock slabs are released. The main thread then waits 25 jiffies and allocates `0x400` 16 KiB `simple_xattr` objects. A second pipe prevents the helper thread from sending its VSOCK request until the xattr spray is complete.

The replacement value repeats a fake vsock every `0x500` bytes, matching the target slab object stride. On this image, `simple_xattr.value` begins 32 bytes after the allocation base. The payload offsets therefore account for that header:

| `vsock_sock` field | Target offset | Offset in xattr value |
| --- | ---: | ---: |
| `sk_write_space` | `728` | `696` |
| `local_addr` | `824` | `792` |

The fake object leaves the socket lock zeroed, sets the reference count to one where the value covers it, and gives the address fields the wildcard values used by the bound-socket lookup. Compile-time assertions check the xattr header and key field offsets.

## ROP and flag readout

At `sk_write_space(sk)`, both `rdi` and `rbp` point to the fake socket. The pivot moves `rsp` to `rbp` and pops four 8-byte words, skipping the 32-byte `simple_xattr` header. The first controlled word then loads the four arguments for `_copy_from_user`: `core_pattern`, the mapped user buffer at `0xa00000`, the command length (including NUL), and an unused fourth argument. The copied command is `|/proc/%P/fd/666 %P`; `msleep` keeps the worker from returning through the reclaimed object.

A watcher notices the changed `core_pattern`, copies its own executable to a memfd at file descriptor 666, and deliberately faults. The watcher exits after launching one crash child, preventing repeated core dumps from keeping the reproduction log active indefinitely. The resulting core-pattern helper runs in the privileged context and reads `/flag`.

## Reproduction

`make exploit` builds the static binary. The submission checker selects only `cos-105-17412.495.75` from this folder. Using the exact COS `bzImage`, kernelCTF's `rootfs_repro_v2.img`, and the current upstream `repro.sh` and `init.sh`, the cleaned exploit obtained the flag in 10 of 10 local KVM runs. GitHub-hosted CI results must be checked separately.
12 changes: 12 additions & 0 deletions pocs/linux/kernelctf/CVE-2025-21756_cos_2/docs/vulnerability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- Requirements:
- Capabilites: None
- Kernel configuration: CONFIG_VSOCKETS, CONFIG_VSOCKETS_LOOPBACK
- User namespaces required: No
- Introduced by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c0cfa2d8a788fcf45df5bf4070ab2474c88d543a
- Fixed by: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fcdd2242c0231032fc84e1404315c245ae56322a
- Affected kernel versions: v5.5-rc1 - v6.13.3
- Affected component: vsock
- Syscall to disable: socket
- URL: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2025-21756
- Cause: Use-After-Free
- Description: A Use-After-Free vulnerability occurs by decrementing the refcnt even when the vsock is not bound.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# define complier type
CC = gcc
# compile options setting
CFLAGS = -O2 -static -w
# library link & option setting
LDFLAGS = -lkeyutils

SUBDIRS = modules

prerequisites:
sudo apt-get install libkeyutils-dev

clean_subdirs:
@for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir clean; \
done

# The upstream workflow renames this target for its debug build.
exploit_debug: CFLAGS += -g

exploit: exploit.c kaslr.h modules/helper.o modules/pipe.o modules/xattr.o modules/msg_msg.o modules/keyring.o
$(CC) $(CFLAGS) $(filter-out %.h,$^) -o $@ $(LIBS) $(INCLUDES) $(LDFLAGS)

all:
$(MAKE) exploit

clean:
$(MAKE) clean_subdirs
rm -f *.o exploit
Binary file not shown.
Loading
Loading