Introduction
plutoo and I found this bug back in september of last year. Effectively this will grant ROP in an IOS usermode process which may then be further used to target the kernel. The vulnerability itself is a TOCTTOU race condition.
The Bug
Initially the ioctlv handling of the IOS kernel contained a major design flaw, namely that the buffer address verification of the vectors happens in-place provided that we supply more than 8 vectors. This enables the PPC side to change the buffer address after its verification. Due to the nature of the bug exploitation requires the number of vectors passed in to be relatively large.
int _ioctlv(int fd, int cmd, u32 num_in, u32 num_io, ioctlv_vec *vec, ...) | |
{ | |
/* ... */ | |
dev_ctxt *dev = get_dev(...); | |
u32 num_total = num_int + num_out; | |
/* ... */ | |
/* Copy in vectors if num_total <= 8 else use external vectors. */ | |
/* Check vector buffer addrs. */ | |
/* ... */ | |
} |
The Fix
The bug was fixed with version 5.2.0 by adding a new field to the device context that limits the number of vectors, which is set to 8 by default and may be changed using syscall 0x2E if required.
int _ioctlv(int fd, int cmd, u32 num_in, u32 num_io, ioctlv_vec *vec, ...) | |
{ | |
/* ... */ | |
dev_ctxt *dev = get_dev(...); | |
u32 num_total = num_int + num_out; | |
/* ... */ | |
if(num_total <= dev->max_vecs) | |
{ | |
/* Copy in vectors if num_total <= 8 else use external vectors. */ | |
/* Check vector buffer addrs. */ | |
} | |
else | |
res = -11; | |
/* ... */ | |
} | |
int _dev_register(char *path, int mqid, int pid) | |
{ | |
/* ... */ | |
dev->max_vecs = 8; | |
/* ... */ | |
} | |
int syscall_2E_set_ioctlv_max_vecs(char *name, u16 num) | |
{ | |
/* ... */ | |
dev->max_vecs = num; | |
/* ... */ | |
} |
Exploitation
The goal was to gain ROP under an IOS usermode process. For this we had to look for a device that did not check the number of vectors itself. It turns out that “/dev/im” provides us with some very handy ioctlv handlers, namely:
static int dev_im_mq; | |
static int hb_param_idx; | |
static int hb_param_type; | |
int SetDeviceState(fd_ctxt *fd, int *buf) | |
{ | |
/* ... */ | |
switch(buf[0]) | |
{ | |
/* ... */ | |
case 3: | |
hb_param_idx = buf[2]; | |
hb_param_type = buf[1]; | |
/* ... */ | |
} | |
/* ... */ | |
} | |
int GetHomeButtonParams(int *buf) | |
{ | |
buf[0] = hb_param_type; | |
buf[1] = hb_param_idx; | |
return 0; | |
} | |
int dev_im_handler(...) | |
{ | |
while(!recv_msg(dev_im_mq, &req, 0)) | |
{ | |
switch(req->cmd) | |
{ | |
/* ... */ | |
case 4: | |
/* ... */ | |
SetDeviceState(fd_ctxt, (int *)req->vecs[0].phys); | |
/* ... */ | |
case 7: | |
/* ... */ | |
GetHomeButtonParams((int *)req->vecs[0].phys); | |
/* ... */ | |
/* ... */ | |
} | |
/* ... */ | |
} | |
/* ... */ | |
} |
Thus this allows us to write 8 bytes worth of data to an address we eventually control. With this arbitrary write we can now carefully setup a ROP stack inside the AUXIL process, overwrite the return address of one of the devices’ handler threads and get the handler thread to return by overwriting the corresponding message queue handle.
Note that this is by no means the only way to exploit this flaw – interested readers are encouraged to let us know about any alternative strategies they might come up with.