WindowServer: The privilege chameleon on macOS (Part 2)


by Liang Chen (@chenliang0817)

From my last blog post “WindowServer: The privilege chameleon on macOS (Part 1)”, we discussed some basic concepts, the history and architecture of WindowServer, as well as the details of CVE-2016-1804 - A Use-After-Free (Or we can also call it double free) bug with very small time window. Several troubles still exist before we can write the exploit code of this bug, now let’s resolve them one by one.

0x9 Sandbox not defined == Cannot open?

Since the Free and Use primitive reside in a single MIG call, it is not possible to fill in the controlled data in between two frees. Also all CoreGraphics server APIs are running in a single-threaded server loop, we can not use other APIs in CoreGraphics to control the freed memory content. The only possible way is to leverge QuartzCore APIs which run at another thread. QuartzCore is also known as CoreAnimation. Compared with CoreGraphics, QuartzCore framework provides with more complex graphics operation such as animation when multiple layers are involved in the action. Unlike CoreGraphics, QuartzCore service is not explicitly defined in application’s sandbox.

Does it mean we cannot open the port of QuartzCore service? By taking traditional approach, we cannot open as it is blocked by sandbox. But let’s review the last blog post, did we miss some key part?

Yes, remember we have three different types of complex message: OOL descriptor message, port descriptor message, and OOL port descriptor message. Port descriptor is needed in case we can not create a mach port at our process and have to use IPC to make the server process to create the port and send it back to our process. This is quite similiar with DuplicateHandle API on Windows platform. Thus, we assume there exists a server interface in CoreGraphics API that can help create the service port of com.apple.CARenderServer. By auditing the code, we finally find the right one: __XCreateLayerContext:

0xA QuartzCore: the hidden interface, and new territory

Because QuartzCore service is not explicitly defined to allow open in application sandbox. By code auditing we find there is no sandbox consideration in any of its service interface.For example, _XSetMessageFile interface allows sandboxed application to set the log file path and file name. In other words, sandboxed application can create any files under any path within windowserver user’s privilege, although the windowserver privilege is quite limited, it still deviates from the original sandbox’s privilege scope. On iOS the impact is higher because the backboardd process is running under mobile user, which means you can create any file under the path where mobile user can create.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall _XSetMessageFile(__int64 a1, __int64
a2)
{
if ( memchr((const void *)(a1 + 40), 0, v5) ) //a1 + 40
is user controllable, which is the file path
{
LOBYTE(v6) = CASSetMessageFile(*(unsigned int *)(a1 + 12),
(const char *)(a1 + 40)); //will set create thefile whose path and filename can be specified by user
*(_DWORD *)(a2 + 32) = v6;
}
else
{
LABEL_14:
*(_DWORD *)(a2 + 32) = -304;
}
result = *(_QWORD *)NDR_record_ptr;
*(_QWORD *)(a2 + 24) = *(_QWORD *)NDR_record_ptr;
return result;
}

But this bug is not critical in our case. By calling __XCreateLayerContext, at least we found a way now to control another thread and had some hope for exploiting CVE-2016-1804.

0xB Crash on failure?

In race condition case, the key factor of the exploitability is whether failure of racing will result in panic or process crash especially when the racing time window is small. At the first glance, it is normal for process to crash with a double free on same memory block. Considering the following code:

1
2
3
4
5
char * buf = NULL;
buf = malloc(0x60);
memset(buf , 0x41, 0x60);
free(buf);
free(buf);

When running on OS X, it will result in process crash:

checkCFData(878,0x7fff79c57000) malloc: *** error for object 0x7fe9ba40f000: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
[1]    878 abort

Now let’s try CFRelease case:

1
2
3
CFDataRef data = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, buf, 0x60, kCFAllocatorNull);
CFRelease(data);
CFRelease(data); //No crash will happen

Surprisingly there is no crash! That is good news for this bug. It means we can try triggering this bug a lot of times until success. Here is the strategy to fill in controlled data in between two CFRelease:

0xC Race to fill in the controlled data

The next problem is: what API in QuartzCore should be chosen to fill in the freed CFData struct? This is still a challenge because:

  • CFData is 0x30 in size, we need an object whose size is also 0x30 to fill in
  • In that API, not so much noise (a lot of irrelevant 0x30 objects may cause higher rate of failure )
  • Higher rate of being filled in. (Better to be a loop to allocate objects again and again)
  • The first 8 bytes of the 0x30 object can be controlled. (Can confuse the method table of CFData)
    This part is the key in the whole exploitation process and I would like to discuss in detail next week at Black Hat USA.

After finding a good API candidate to fill in, we got stable crash on controlled address:

0xD HeapSpray

Heap spraying is always an interesting problem in 64bit process. On OS X, for small block heap memory allocation, a randomized heap based is involved. Considering the following code:

1
2
buf = malloc(0x60);
printf("addr is %p.\n", buf);

By running the code several times, the results are:

addr is 0x7fd1e8c0f000.
addr is 0x7fb720c0f000.
addr is 0x7f8b2a40f000.

We can see the 5th byte of the address varies between diferent processes, which means you need to spray more than 1TB memory to achieve reliable heap spraying. However for large block (larger than 0x20000) of memory, the randomization are not that good:

1
2
buf = malloc(0x20000);
printf("addr is %p.\n", buf);

The addresses are like this:

addr is 0x10d2ed000.
addr is 0x104ff7000.
addr is 0x10eb68000.

The higher 4 bytes are always 1, and the address allocation is from lower address to higher address. By allocating a lot of 0x20000 blocks we can make sure some fixed addresses filling with our desired data. The next question is: how can we do heap spraying in WindowServer process? There are a lot of interfaces within CoreGraphics, and we need to find those which meet the following criteria:
– Interface accepts OOL message
– Interface will allocate user controllable memory and not free it immediately
We finally pick up interface _XSetConnectionProperty. We can specify different key/value pairs and set it in the connection based dictionary, where the memory will be kept within WindowServer process.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __fastcall CGXSetConnectionProperty(int a1, __int64 a2, __int64 a3)
{
...
v3 = a3;
if ( !a2 )
return;
if ( a1 )
{
v5 = CGXConnectionForConnectionID();
v6 = v5;
if ( !v5 )
return;
v7 = *(_QWORD *)(v5 + 160); //get the connection based dictionary, if not exist, create it.
if ( !v7 ) {
v7 = CFDictionaryCreateMutable(0LL, 0LL,
kCFTypeDictionaryKeyCallBacks_ptr,
kCFTypeDictionaryValueCallBacks_ptr);
*(_QWORD *)(v6 + 160) = v7;
}
if ( v3 )
CFDictionarySetValue(v7, a2, v3);
...
}

0xE ASLR/DEP/Code Execution

ASLR is not a problem in our case as we pwn Safari browser first and the base addresses of most apple framework are the same among different processes. DEP can also be bypassed by ROP. Code execution is not a big deal, can refer to the phrack article.

0xF Hey Chameleon, Now I want you to be ROOT!

As we know, we successfully bypassed Apple sandbox to get current user’s context by exploiting CVE-2014-1314. And by finding the hidden interface, we obtained a new territory and found a bug which allows writing arbitrary files under _windowserver’s context.
How about CVE-2016-1804? As we got arbitrary code execution within WindowServer process, why not try calling setuid(0) and see what happened?
The result is amazing, we successfully get root!!

root  560 0.0 0.7  2614800  33888   ??  SXs   4:30上午   0:00.04 /System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreGraphics.framework/Resources/WindowServer -daemon

Why? We know that WindowServer has session management feature and need to fork new login process under user’s context, so it must have setuid privilege. But how it is implemented?

Actually when WindowServer process is firstly spawned by launchd daemon, it is running as root which inherited its parent process’s uid.

(lldb) chenliangs-Mac:~ chenliang$ sudo lldb
(lldb) process attach --name WindowServer  --waitfor
Process 910 stopped
* thread #1: tid = 0x5cda, 0x00007fff6314d302 dyld`stat64 + 10, stop reason = signal SIGSTOP
    frame #0: 0x00007fff6314d302 dyld`stat64 + 10
dyld`stat64:
->  0x7fff6314d302 <+10>: jae    0x7fff6314d30c            ; <+20>
    0x7fff6314d304 <+12>: mov    rdi, rax
    0x7fff6314d307 <+15>: jmp    0x7fff6314c89c            ; cerror_nocancel
    0x7fff6314d30c <+20>: ret    

Executable module set to "/System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreGraphics.framework/Resources/WindowServer".
Architecture set to: x86_64-apple-macosx.
(lldb) expr -- (int)getuid()
(int) $0 = 0
(lldb) expr -- (int)getgid()
(int) $1 = 0
(lldb) expr -- (int)geteuid()
(int) $2 = 0
(lldb) expr -- (int)getegid()
(int) $3 = 0

Then some code is executed to change WindowServer’s euid to 88 (_windowserver) while its uid is never changed. That will limit the process’s capability if we have logical bug as _windowserver’s permission is quite limited:

(lldb) bt
* thread #1: tid = 0x2ef1, 0x00007fff955c364c libsystem_kernel.dylib`seteuid, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2
  * frame #0: 0x00007fff955c364c libsystem_kernel.dylib`seteuid
    frame #1: 0x00007fff8612db15 CoreGraphics`CGXRestoreCredentials + 192
    frame #2: 0x00007fff8641024d CoreGraphics`CGXRunOneServicesPass + 784
    frame #3: 0x00007fff86314f9d CoreGraphics`post_notification(CGSNotificationType, void*, unsigned long, bool, double, int, unsigned int const*, int) + 325
    frame #4: 0x00007fff861fc4f2 CoreGraphics`CGXDisplaysWillReconfigure + 1230
    frame #5: 0x00007fff861f6110 CoreGraphics`reconfigureDisplays + 2351
    frame #6: 0x00007fff861f36b6 CoreGraphics`setup_and_reconfigure_displays + 314
    frame #7: 0x00007fff86412001 CoreGraphics`CGXServer + 6213
    frame #8: 0x000000010cc24f7e WindowServer`_mh_execute_header + 3966
    frame #9: 0x00007fff91e975ad libdyld.dylib`start + 1
    frame #10: 0x00007fff91e975ad libdyld.dylib`start + 1
(lldb) reg read
General Purpose Registers:
       rax = 0x0000000000000000
       rbx = 0x0000000000000058
       rcx = 0x00007fff955c363e  libsystem_kernel.dylib`setegid + 10
       rdx = 0x0000000000000000
       rdi = 0x0000000000000058 // change euid to _windowserver(88)
       rsi = 0x00007fff52fd2fa0
       rbp = 0x00007fff52fcaf30
       rsp = 0x00007fff52fcaef8
        r8 = 0x0000000000000002
        r9 = 0x0000000000000000
       r10 = 0x00007fff955c3656  libsystem_kernel.dylib`seteuid + 10
       r11 = 0x0000000000000292
       r12 = 0x00007fc8c1412290
       r13 = 0x0000000000000101
       r14 = 0x0000005800000058
       r15 = 0x00007fff52fd2fa0
       rip = 0x00007fff955c364c  libsystem_kernel.dylib`seteuid
    rflags = 0x0000000000000202
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

And that will make ps utility show _windowserver in the output:

_windowserver 910 0.0  1.6  3368352  78004   ??  SXs   7:51上午   0:02.57 /System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreGraphics.framework/Resources/WindowServer -daemon

That is quite confusing. If we use lldb, we can clearly figure out its euid is _windowserver while its uid is still root, and make WindowServer a privilege chameleon.

(lldb) expr -- (int)getuid()
(int) $4 = 0
(lldb) expr -- (int)geteuid()
(int) $5 = 88
(lldb) expr -- (int)getgid()
(int) $6 = 0
(lldb) expr -- (int)getegid()
(int) $7 = 88

So finally we wrap up CVE-2016-1804 with full remote root by chaining with Safari exploit.

Oh, wait… How to race and fill in the controlled data in between two free, is still unknown? No worries, next week at Black Hat you will get all the answer…