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.
In fileviewerd
It will register a service with com.xia0o0o0o.fileviewerd and look up service with com.xia0o0o0o.securityd.
We can see the event source is basically
and the event handler is wrapped in a dispatch_async block.
fileviewerd can register a callback port
and when calling read_file() or move_file() it will first verify the user of the request and the actual file owner by communicating with securityd.
The move_file() function will also update the permission of the file.
The result will be send back to the callback port. securityd is a simple daemon that verifies the user of the request and the actual file owner.
Vulnerability
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.
Then we can try to decrease the reference count of the port by
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.
And now we can keep registering the callback port and wait for the message from fileviewerd.
Then we can move the flag into our sandbox and change its permission
And that’s it! We can now read the flag in our own sandbox.