iOS Wi-Fi Demon: From iOS Format String to Zero-Click RCE
by Theotime Chapier-Maldague
So, what happened with iOS?
You might have seen the recent bug in iOS 14.0 to 14.4, that crashed the Wi-Fi service by naming an access point a specific way. Apple tagged this bug as a Denial of Service on the Wi-Fi service, but the Zecops  Research Team has shown proofs that it could be exploited, causing an RCE, and more precisely a Zero-Click RCE. Although their article explains some of the details of the vulnerability, we wanted to make our own investigations. We have made ourselves a good idea of what the vulnerability was, and we will share it through this article.
Debug environment setup
This article won’t only explain what the vulnerability is, it will also allow you to make your own research. To make your life easier we will show you what is needed to be done to debug the iOS Wi-Fi service. The only requirements are having an iPhone (or maybe simulating one), and a device that runs MacOS (I used a Mac mini).
- Preparing the iPhone
The most important thing here is to flash the iOS version of the iPhone to a vulnerable version. Although the format string bug isn’t fixed in versions going from iOS 14.0 to iOS 14.6, the Zero-Click form only exists in versions 14.0 to 14.4, as explained by the Zecops team. My investigations were carried on iOS v14.0, on an iPhone 7+. Firmware images can be easily found online. After flashing the correct firmware on the iPhone, it must be jailbroken in order to establish an SSH connection between the MacOS device and the iPhone, thus allowing to debug its running processes. The unc0ver  jailbreak was used for my investigations, but any jailbreak should do the trick. After this, use Cydia and install the required SSH packages to get SSH running on the iPhone. The connection can be made over the network, but we connected the iPhone to the Mac mini through USB and used iproxy to forward the required ports. If you’re willing to use this technique, you will need one port for the SSH connection and one port for the remote debugging operation. But again, this is not mandatory, and you can just SSH over the network without a cable.
- Remote debugging session
Now that we had my SSH connection, we had to setup the debug environment. For this we used lldb on the Mac mini, and debugserver on the iPhone, which can be installed with Cydia. Then, we just had to use debugserver, and attach the correct PID (the process name is wifid) to start the listener. Note that if the wifid service stays idle to long because of the debugging process, the iPhone will reboot by itself which sometimes required me to repeat the jailbreaking process. What was left to do, was to execute the gdb-remote command on lldb to start the debugging session.
Remote debugging – debugserver and lldb
Vulnerability & root-cause analysis
- Reversing the binary
Using Ghidra and Zecops’ article we found the format string causing the crash:
On the second _objc_msgSend()call, there is no format made thus causing a format string vulnerability. On a debugging aspect, this is where you want your break point to be placed. This function scans for the existing Wi-Fi networks in the area, and always runs even if the phone is locked. It’s the reason why if the RCE is accomplished, it requires no interaction from the user (zero click). As Zecops explained in their article, this format string is different from the usual ones. Since it uses [NSString stringWithFormat:], proper to Apple, and since Apple has removed the support for“%n”,we can’t use the latter to write into the memory. However, the service is written in Objective-C and we can use “%@” so that the function tries to print an Objective-C object. The Zecops team came up with a great idea on how to possibly control the stack and there for entry to control the code execution flow. Basically,they used a be a con flooding attack  that broadcasts hundreds of access points. Thanks to this, these APs will appear on the iPhone and possibly on the stack. I have reproduced this technique by using airmon-ng to setup the monitor interface, and mdk3 with a list of multiple SSIDs to start the flooding.
- Triggering the crash
Since it’s a format string vulnerability, we just need to create hotspot with an SSID holding a value of something like “%x%x%x…%@”. SSID length is limited to 32 bytes, so we have only half of that for our escape characters. With XCode you can analyze the application logs of the iPhone in real time. We filtered the logs with the wifid process and the “crash” string to find the logs that we were interested in, then, we just made an AP with the name stated above.
XCode’s console – wifid crash logs
Here we have our crash, with the error code being “KERN_INVALID_ADDRESS”. In order to understand what happened we just started my debug session. Like mentioned above we used the remote debug session with debugserver running on the iPhone and lldb on the Mac mini.
Setting up an exploitable break point
Since the Kernel ASLR (KASLR) is active, we need an offset that we can then apply to a base address so we can calculate the address of the instruction we want to place our break point at. To get the offset, we simply used Ghidra and went back to the instruction causing the vulnerability. The base address for the __TEXT section in Ghidra is 100000000. We just had to subtract 1000f7fcc from 100000000 (thus obtaining f7fcc).
Ghidra – Getting the vulnerable call’s offset
Now all of what was required to be done to set the break point was to start the debug session, we used the image dump sections wifid command on lldb to get the base address of the __TEXT section.
lldb – Getting the base address of the __TEXT section
Then, we just had to add the offset to this address and set our break point.
lldb – Setting up the breakpoint
To save myself some time, we downloaded the wifid binary directly on the Mac mini so that we can run lldb with it. The reason behind this, is that if we set the break point once, even if the address changes after the process restarts, lldb is going to find itself the new address based on the offset, and we won’t have to repeat the whole process again.
Controlling the execution flow
Now that the break point was set, we just had to trigger the crash to see what was going on. Before that we made the beacon flooding attack to try to spray data on the stack. Zecops have designed a python script made for lldb that automatically checks the stack to find traces of the sprayed SSIDs. However, we didn’t manage to get the script working, but that’s due to my lldb configuration and we couldn’t fix the problem in the limited time we had. We tried to manually scan the stack, but we couldn’t find anything either, no SSID. So instead of looking at the stack we looked in the heap, and this is where we found my Wi-Fi access points. Spraying the stack / heap was supposed to be a way to try to control the code execution flow by controlling registers. It does work but is kind of random due to the other networks around, and due to the random arrangement of the networks in the stack / heap. Refer to Zecops’ article to get more details about the stack spray.
some APs found in the heap
What we had fully control of however, is the x15 register.
lldb – DEADBEEF in x15
On the above screenshot we can easily identify the reason of the crash. Basically, the program tries to access the address that’s 28 bytes further from the address contained in x2, and crashes because this address doesn’t exist.
To control x15, we used the beacon spray and created another hotspot with the following name:
The input placed before the escape characters will be placed into x15 systematically. It is the only input that we managed to fully control. We also found that moving the FEEBDAED string to the right changed the value in x15 to be a part of the existing APs that were broadcasted (not by the spray, we are referring to actual Wi-Fi networks). We are still unsure whether this data is being read from the stack, or if it is read from the heap.
There is a possibility to also control x9 and x10, however we didn’t find a way to achieve it since the spray didn’t seem to affect the stack in this case, and only saw it through Zecops’ screenshots (see below). Controlling both registers seem to give the possibility to then control a part of x2.
Zecops – Register values after spray + crafted SSID
Zecops’ research team has discovered that x9 points to the first member of the Object data structure.
To achieve RCE, one must pass a valid Objective-C object through the spray to control x9. Then, the object will be passed into __objc_msgSend() and arbitrary code execution can be achieved. (Check Zecops’ article to see the scheme of the desired execution flow).
Achieving RCE & discovering additional vulnerabilities (not published yet but triggerable)
For this research, my time was limited. We couldn’t go as far as we would have with more time. A bit more reversing and tests need to be carried, but we are very confident that this vulnerability is exploitable.
However, not through ROP, since we couldn’t find any gadgets suitable for ROP within the wifid binary itself. JOP would be required to exploit this vulnerability if a control of the execution flow is in fact possible, and the JOP payloads should also be part of the AP spray. KASLR is still a problem though, but the one thing we found useful is that when the logs (available on XCode) print the AP names, and when it tries to print the malicious SSID, it replaces the “%x” escape characters with actual addresses, thus causing a memory leak. This happens only when the malicious SSID is part of the spray and is not an individual hotspot. So technically if you need to get an address, you can make multiple SSIDs and spray them to leak memory addresses.
XCode – Memory leak
When we first found the Format String bug in Ghidra, we thought that we had the same function as the one showed on the Zecops article. However, when setting a breakpoint on it, wifid would crash before hitting the break point.
This made me discover that two other functions were vulnerable to the same format string bug. When updating the iPhone back to iOS 14.7 (where the bug was fixed), we noticed that all three functions (the original plus the two others) were patched.
We are not sure why there are three functions doing the AP scan, but Apple has successfully patched all of them as they also were exploitable.
- Bingner, Sam, and Dennis D. Bednarz. “Unc0ver.” Unc0ver.dev, 20 Aug. 2021, unc0ver.dev/. Accessed 6 Sept. 2021.
- Robot, Mr. “Beacon Flooding Attack.” Medium, 13 Dec. 2019, https://medium.com/infosec-adventures/beacon-flooding-attack-a4baadc2242b Accessed 6 Sept. 2021.
- Zecops, Fellow. “Meet WiFiDemon: IOS WiFi RCE 0-Day Vulnerability & a ‘Zero-Click’ Vulnerability That Was Silently Patched.” ZecOps Blog, 17 July 2021, https://blog.zecops.com/research/meet-wifidemon-ios-wifi-rce-0-day-vulnerability-and-a-zero-click-vulnerability-that-was-silently-patched/ Accessed 6 Sept. 2021.
About the Author
Theotime Chapier-Maldague is a student at the University of Lorraine in France. He is a young Pentest and pwn enthusiast. During his internship at POST Luxemburg he worked with a team of offensive security experts, and was given the keys that helped him learn the fundamentals of exploit development. With a constantly growing passion he looks forward to pursue a career in offensive security at the end of his studies.
The article originally published at: https://ictexpertsluxembourg.lu/technical-corner/ios-wi-fi-demon-from-ios-format-string-to-zero-click-rce/