Voice Control is a powerful feature introduced by Apple in iOS 13 and macOS Catalina. It acts as a substitute for all the touch gestures on the screen, letting you interact with the device using your voice to tap, swipe, type, and more.
com.apple.SpeechRecognitionCore.speechrecognitiond is a system XPCService process that handles voice control.
During an investigation of ZecOps Mobile XDR / Mobile DFIR, we discovered a series of crashes that appears intriguing:
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Subtype: EXC_ARM_DA_ALIGN at 0x0074616f6c460003
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x2000002400000000 -> 0x0000002400000000
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x00000000100c02d8
Mobile Device Investigation Analysis
Not all crashes are the same, but they all have a similar pattern. All crashes occur after some libdispatch.dylib calls.
With this clue, we went on and investigated the cause of this crash.
We’re going to explain two of the most typical cases. Both have occurred when the user is toggling the voice control switch at different timings.
We will attach a POC that demonstrates an unexpected multithread issue, proving that even when the developer uses an optimized thread management library such as Grand Central Dispatch (GCD) dispatch queues, which are already considered safe from multi-threading perspective. The chance of race condition still exists, capable of causing memory corruption and leading to code execution.
Following shows two of the most typical cases:
Race Condition Case 1: When the user turns VoiceControl off
The dispatch queue “RDAudioBufferQueue” is created when the device is actively processing audio data. An AVSoundInput class instance has been passed to this thread to provide the input data through context data. Due to insufficient consideration of thread safety, when the user decides to turn off the voice control feature, the context data may get released early in another queue “RDMainQueue“, which leads to a Use-After-Free (UAF).
There is actually a function dealing with the audio format conversion that is executed between _dispatch_call_block_and_release and <PC Corruption>. It didn’t show in the backtrace because it used the “br” instruction that doesn’t save the return address in the stack.
Below is the pseudo-code of the function:
(1) Inside _addRecordedSpeechSampleData:length: method, It’s trying to invoke a function pointer stored in *( *(context_data + 48) + 16), normally it will execute EARCSpeechRecognitionAudioBufferAddAudioSamples. However, if the user decides to turn off voice control, the context_data will be released in another thread, as shown below:
The problem is the lack of a lock to ensure that RDAudioBufferQueue will exit before context_data is released. The heap memory in *(context_data + 48) could be released early and reoccupied by other data, which led to Program Counter (PC) corruption.
Race Condition Case 2: When the user turns VoiceControl on
RDMainQueue may randomly occur memory corruption on different objects. The above example is that the XPC connection object was released before use, and Use-After-Free causes the thread to crash.
RDMainQueue is used as a multi-purpose general queue. Various callbacks throw tasks into this queue, including accepting and handling xpc requests, reporting audio data feedback and taking action, nested calling was frequently involved.. All these tasks are submitted to RDMainQueue through dispatch_async, and they seem to be correct in the order of calling.
How does a Use-After-Free happen if the use and release of XPC connection objects are all assigned to be processed in the same queue, in the correct order ?
The answer is that dispatch_async does not guarantee the blocks to be executed in the same order it gets called!
The following POC demonstrates the potential threat of using dispatch_async even on a same queue:
dispatch_sync is safer as if you replace all dispatch_async to dispatch_sync, the above code will run flawlessly.
dispatch_async brings the convenience of supporting nested calls. However, for the sake of thread safety, developers better to implement additional checks to ensure that the blocks are executed in the required order.
When the user frequently turns on and off the voice control switch, the busy operation of adding queues will mess up the order, which may still use an object after it’s released.
Triggering of the Crash
You can trigger this Use-After-Free while on the lock screen, following these steps:
1. Press and hold the side button to activate Siri.
2. Say “Turn Off Voice Control”, a window of voice control switch should appear.
3. Switch on/off Voice Control repeatedly. There is some trick to trigger the crash more reliably.
Instead of using Siri, you can also just go to Settings -> Accessibility -> Voice Control
After turning on Voice Control, an icon will appear on the left upper corner, and it first appears as grey, then it will turn blue. The trick to find the best timing is to turn off the voice control right before the icon turns to blue.