(nur auf Englisch verfügbar)
Blogbeitrag 29. September 2021 von Jim Aspers, Sicherheitsspezialist bei Bureau Veritas Cybersecurity
de
de
(nur auf Englisch verfügbar)
Blogbeitrag 29. September 2021 von Jim Aspers, Sicherheitsspezialist bei Bureau Veritas Cybersecurity
Wir haben zwar gezeigt, dass es möglich ist, beliebige iOS-Anwendungen auszuführen, aber wir haben dazu eine legitime Anwendung verwendet und deren Inhalt durch die Anwendung ersetzt, die wir auszuführen versuchten; der Startvorgang wurde nicht in der Tiefe verstanden. Um die volle Kontrolle über die gestartete Anwendung und die Umgebung, in der sie eingesetzt wird, zu erlangen, war ein solideres Verständnis des Startvorgangs erforderlich.
Außerdem stellte sich heraus, dass viele Anwendungen mit integrierter Jailbreak-Erkennung unsere Macs als jailbroken markierten, was eine manuelle Umgehung erforderte. Wir suchten nach einer soliden Möglichkeit, dies zu umgehen, ohne die Zielanwendung zu patchen oder zu instrumentieren.
Es ist wünschenswert, dass Sie so viel Kontrolle wie möglich über die zu testende Anwendung haben. Konkret bedeutet dies (aber nicht nur):
Das Starten von nicht-grafischen iOS-Binärdateien unter macOS lässt sich recht trivial bewerkstelligen, indem Sie den privaten Syscall posix_spawnattr_set_platform_np() verwenden, um das Prozessplattform-Attribut vor dem Spawning auf '2'(PLATFORM_IOS, siehe <mach-o/loader.h in den XNU-Quellen ) zu setzen (wie von Samuel Groß in seinem Beitrag beschrieben). Das Spawning einer grafischen iOS-Anwendung ist jedoch eine andere Art von Spiel. Wir haben teilweise nachgeforscht, wie macOS den Start von grafischen Anwendungen im Allgemeinen handhabt, und dieses Wissen auf den iOS-spezifischen Fall angewendet.
Es stellte sich heraus, dass mehrere Betriebssystemkomponenten am Start einer iOS-Anwendung beteiligt sind, unter anderem:
Viele dieser Komponenten sind mit Sandboxing und anderen sicherheitsrelevanten Aufgaben befasst. Da wir es vorziehen würden, eine Anwendung unter Umgehung dieser sicherheitsrelevanten Komponenten direkt zu starten, haben wir den Start einer einfachen Systemanwendung (Calculator.app) analysiert und den beobachteten Ablauf verglichen. Dieses Mal zeigte sich, dass nur runningboardd aktiv beteiligt war. Damit verlagerte sich der Fokus auf runningboardd.
Zu diesem Zeitpunkt hatte sich der Verdacht erhärtet, dass wir auf irgendeine Weise mit runningboardd kommunizieren mussten, um Anwendungen direkt zu starten. Abgesehen von einer begrenzten Anzahl von Kernkomponenten behält Apple den Quellcode und die private API-Dokumentation für sich, so dass es nicht möglich ist, einfach in den Quellcode von runningboardd zu schauen oder die Schnittstellen nachzulesen, die es offenlegt. Auch ein vollständiges Reverse Engineering des dahinter liegenden Codes (RunningBoard.framework, RunningBoardServices.framework) ist keine realistische Aufgabe.
Der einfachste Weg, um zu lernen, wie man eine App selbst startet, ist, ein wenig zu schummeln und Apples eigene Programme auszuspionieren. Es gibt (mindestens) zwei gängige Möglichkeiten, wie ein macOS-Benutzer eine iOS-Anwendung in einem normalen Nutzungsszenario starten kann: mit Finder.app durch Doppelklick auf das Wrapper-Bundle der App (siehe früheren Beitrag zu diesem Thema) oder mit dem Befehl 'open' mit dem Wrapper-Bundle als Argument. Nach unserer ersten Analyse vermuten wir, dass beide zu einem bestimmten Zeitpunkt eine XPC-Nachricht an runningboardd senden und es auffordern, die Zielanwendung zu starten. Obwohl 'open' die naheliegendste Wahl für das Reverse Engineering zu sein scheint, da es nur eine begrenzte Größe hat und daher die Ergebnisse nur wenig stören, ist das Gute an Finder.app, dass es ein ständig laufender Prozess ist. Das macht ihn zu einem einfachen Ziel für Tracing-Tools wie xpcspy.
Wenn wir uns die Ausgabe ansehen, finden wir die erwarteten XPC-Aufrufe:
Der vollständige Inhalt ist zu umfangreich, um ihn hier anzuzeigen. Was wir damit sagen wollen, ist, dass dieser Aufruf Informationen zu enthalten scheint, die für das Spawnen der Zielanwendung auf Betriebssystemebene verwendet werden (Ziel-Bundle-Kennung, Umgebungsvariablen, LaunchServices-Attribute wie Spawn-Flags und Ausführungsoptionen, ...). Eine weitere wichtige Information aus dieser Ausgabe ist der Selektor, der an die Gegenseite gesendet wurde: executeLaunchRequest:identifier:error:. In Objective-C unter macOS/iOS kann ein "Selektor", der an ein Objekt gesendet wird, als eine Klasse oder Instanzmethode interpretiert werden, die ausgeführt wird. Da wir wissen, dass der Selektor an runningboardd gesendet wird und wir den Schlüssel"rbs_selector" sehen, ist unsere erste Vermutung, dass der API-Aufruf, aus dem diese XPC-Nachricht resultiert, von RunningBoardServices bereitgestellt wird.
Wir verwenden frida-trace, während wir Calculator.app erneut vom Finder aus starten, um herauszufinden, zu welcher Klasse dieser Selektor genau gehört:
Der genaue Selektor, den wir erwartet hatten, wurde nicht gefunden, aber wir haben etwas gefunden, das ähnlich aussieht. Das am interessantesten klingende Argument des Selektors, ein "LaunchRequest"-Objekt, ist auch hier vorhanden. Eine weitere Untersuchung dieses Objekts ergab, dass es sich um ein RBSLaunchRequest-Objekt handelt, das aus einem RBSLaunchContext-Objekt konstruiert wurde, das wiederum den in der xpcspy-Ausgabe gezeigten Wörterbuchinhalt enthält. Diese und weitere, ähnliche Nachforschungen zeigten uns, dass wir das RBSLaunchRequest-Objekt und verwandte Objekte zur Erreichung unserer Ziele nutzen sollten.
Nach einigem Herumstöbern in verschiedenen verwandten Methodenimplementierungen, die von RunningBoardServices.framework zur Verfügung gestellt werden, und einigen Versuchen fanden wir heraus, dass wir eine grafische macOS-Anwendung von unserem eigenen Code aus starten können, indem wir nur ein paar undokumentierte APIs aufrufen. Wie wir aus Experimenten mit einfachen iOS-Binärdateien unter macOS wussten, liegt der Schlüssel zum Starten einer ausführbaren iOS-Datei anstelle einer macOS-Datei in einem Parameter für die Plattformspezifikation. Bei der Suche nach Symbolen, die die Zeichenfolge "platform" enthalten, stellten wir fest, dass ein RBSProcessIdentity-Objekt mit einem Plattform-Argument initialisiert werden kann. Die Nachverfolgung des jeweiligen Aufrufs zeigte, dass dieser tatsächlich getroffen wurde und dass der Wert für dieses Argument beim Start von Calculator.app 1 ("PLATFORM_MACOS") war:
Dies erwies sich als der Schlüssel zum Starten von grafischen iOS-Anwendungen aus unserem eigenen Code. Wenn wir bei der Erstellung des RBSProcessIdentity-Objekts PLATFORM_IOS anstelle von PLATFORM_MACOS mit diesem Selektor angeben und den Start eines iOS-Anwendungsbündels anfordern (nicht nur einen Wrapper, sondern ein tatsächliches iOS-Anwendungsbündel), startet macOS die Anwendung problemlos. Da keine weitere Prüfung stattfindet, wenn wir die Anwendung auf diese Weise starten, können wir das gesamte Bündel einfach ad hoc signieren und die Signatur wird akzeptiert. Dies ist ideal, wenn Sie mit Patches für die Zielanwendung experimentieren.
Jetzt, da wir eine iOS-Anwendung mit den Objekten RBSProcessIdentity und RBSLaunchContext vollständig unter unserer Kontrolle starten können, können wir uns an die Arbeit machen. Zu diesem Zeitpunkt suchen wir nach Lösungen für mindestens die folgenden Punkte:
Wenn wir die Widerstandsfähigkeit von Anwendungen gegen Cracking oder Laufzeitmodifikationen im Allgemeinen bewerten, stoßen wir manchmal auf Anwendungen, die in einer frühen Phase des Anwendungsstarts Anti-Debugging-Maßnahmen implementieren. Dies kann bereits bei den Initialisierungsroutinen der Bibliothek der Fall sein, die nur schwer zu umgehen sind, wenn eine Verschleierung verwendet wird oder es einfach Hunderte von Initialisierungsroutinen zu durchforsten gibt. In solchen Fällen ist es wichtig, einen Debugger in einem sehr frühen Stadium anzuschließen, noch bevor der Anti-Debugging-Schutzmechanismus aktiviert wird. Dies nennen wir frühe Instrumentierung.
Wenn wir keine andere Wahl hätten, würde ein einfaches while() , das darauf wartet, dass ein Prozess gestartet wird, und dann sofort einen Debugger anhängt, wahrscheinlich ausreichen. Da wir nun jedoch die volle Kontrolle über den gespawnten Prozess haben, können wir den Kernel anweisen, den Kindprozess direkt nach dem Spawn in den Suspended Mode zu versetzen (d.h. er stoppt direkt bei _dyld_start, dem Einstiegspunkt einer beliebigen *OS-Ausführung):
Sobald wir mit der Anwendung fertig sind (z.B. Haltepunkte setzen, Anweisungen überspringen, Speicher patchen), können wir ihre Ausführung fortsetzen, indem wir ein SIGCONT senden.
Ein sehr unangenehmer Nebeneffekt des Starts einer Anwendung im angehaltenen Modus ist, dass der Puppet Master von macOS in Form von runningboardd einen 'Heartbeat' vom gespawnten Prozess erwartet. Erhält er nicht rechtzeitig ein solches Lebenszeichen, bleibt die Anwendung in einem unbrauchbaren Zustand und kann nicht mehr verwendet werden (ein kill -9 ist dann erforderlich). Dies geschieht etwa 30 Sekunden, nachdem die Anwendung im angehaltenen Modus gestartet wurde und die Ausführung nicht fortgesetzt wird, mit der folgenden Fehlermeldung:
Es wurde noch keine Lösung für dieses Problem gefunden. Die Verwendung von lldb per Skript sorgt dafür, dass 30 Sekunden ausreichen, um unsere Magie im Allgemeinen auszuführen, aber eine richtige Lösung wäre willkommen.
Die zweite Anforderung war eine bequeme Methode zum Ändern des Verhaltens der Anwendung. Wenn es darum geht, den Ausführungsablauf von Anwendungen zu ändern, gibt es im Allgemeinen drei Möglichkeiten:
Da wir zum Patchen von Maschinencode auf der Festplatte oder im Speicher Assemblercode schreiben müssen, sind dies nicht die bequemsten und flexibelsten Möglichkeiten. Ein Tool wie Frida ist großartig und hat eine große Anzahl von Anwendungsfällen. Für die Anwendung dauerhafter, potenziell komplexer Änderungen ist jedoch eine weniger umständliche Methode wie dyld interposing (für C-Funktionsaufrufe) oder swizzling (für Obj-C-Klassen) vorzuziehen. Mit der vorliegenden Lösung können wir bereits sehr gut swizzeln, indem wir unsere eigene Dylib schreiben und sie mit einer DYLD_INSERT_LIBRARIES-Umgebungsvariablen injizieren. In Bezug auf C-Funktionsaufrufe, wie von Samuel Groß in seinem Artikel (auf den in diesem Beitrag bereits verwiesen wurde) dargelegt, erlaubt macOS keine Zwischenschaltung für iOS-Programme. Da wir die Anwendung jetzt jedoch in einem angehaltenen Zustand starten können, können wir Groß' Code-Injektionsmethode anwenden, um dyld im Speicher zu patchen und so diese Einschränkung zu umgehen. Dies ist auch in unserem Launcher implementiert.
Eine triviale praktische Anwendung dieser Funktion ist die Behebung der Inkompatibilität einer Anwendung mit der iOS-Hardwareplattform, die von macOS emuliert wird (iPad Pro3rd gen mit iOS 14.7 zum Zeitpunkt des Schreibens). Aus diesem Grund konnte die Anwendung unseres Kunden nicht unter macOS ausgeführt werden; wir erhielten die Fehlermeldung, dass unser 'iPad' zu neu sei, um die Anwendung auszuführen. Mit ein paar Zeilen Code wurde jedoch eine dynamische Bibliothek kompiliert, um ein älteres iPad-Modell zu imitieren, das von der Anwendung unterstützt wurde (iPad7). Daraufhin konnten wir die Anwendung ordnungsgemäß ausführen und bewerten.
Beim Testen einer Xamarin-basierten Anwendung, die Cryoprison zur Jailbreak-Erkennung implementierte, wurde festgestellt, dass iOS-Anwendungen, die auf diese Weise gestartet wurden, auf Dateien wie /bin/bash zugreifen konnten. Da solche Dateien auf gesperrten iOS-Installationen nicht vorhanden sein sollten oder zumindest nicht von der Sandbox der Anwendung aus zugänglich sein sollten, überprüfen gängige Jailbreak-Erkennungsimplementierungen den Zugriff auf solche Systemdateien, um festzustellen, ob sie in einer jailbroken oder einer nicht-jailbroken Umgebung ausgeführt werden. Die Tatsache, dass die getestete Xamarin-App in diesem Fall eine jailbroken Plattform erkannte, führte zu dem Verdacht, dass das von macOS auf iOS-Anwendungen angewendete Sandbox-Profil nicht ganz mit der nativen iOS-Sandbox übereinstimmt.
Um die Mechanismen der macOS-Sandbox zu verstehen, haben wir das sandbox-exec-Programm teilweise zurückentwickelt. Mit einem Textabschnitt von etwa 1kB Größe schien diese Binärdatei der perfekte Kandidat für den Beginn des Reverse Engineering-Prozesses zu sein:
Der Programmablauf bestand im Wesentlichen aus zwei Schritten:
Da execve() ein Binärabbild im Prozess des Aufrufers ausführt, wird das im ersten Schritt angewendete Sandbox-Profil auf die ausführbare Zieldatei angewendet. Auf diese Weise muss die ausführbare Zieldatei nicht sandbox-fähig sein.
Eine weitere Analyse ergab, dass zwei undokumentierte API-Aufrufe ausreichen, um ein Sandbox-Profil vorzubereiten und anzuwenden:
Mit der zuvor beschriebenen Methode der Bibliotheksinjektion (unter Verwendung von DYLD_INSERT_LIBRARIES), um eine frühe Codeausführung im Prozesskontext der gestarteten iOS-Anwendung zu erreichen, haben wir diesen Ablauf imitiert. Der Aufruf von sandbox_apply() schlug jedoch fehl, da zu diesem Zeitpunkt bereits ein Sandbox-Profil für den Prozess vorhanden war. Jede Möglichkeit, eine Sandbox-Richtlinie aus dem Prozess selbst heraus zu ändern, nachdem sie aktiviert wurde, könnte zu Sicherheitslücken führen, so dass es sinnvoll ist, dies nicht zuzulassen.
Wir suchten daher nach einer Methode, um unsere Zielanwendung ohne Sandbox zu starten, damit wir anschließend unsere eigene benutzerdefinierte Sandbox anwenden können. Die Analyse des Startvorgangs der Anwendung hatte uns bereits gezeigt, dass die macOS-Komponente secinitd an der Anwendung von Sandbox-Profilen beteiligt ist. Die Analyse der zugrundeliegenden Bibliothek libsystem_secinit.dylib ergab, dass es tatsächlich eine Möglichkeit gibt, auch iOS-Anwendungen zu starten, ohne dass automatisch eine Sandbox angewendet wird: die Berechtigung"com.apple.private.security.no-sandbox". Nachdem wir diese Berechtigung zu den Berechtigungen unserer Zielanwendung hinzugefügt haben (die wir natürlich auch vollständig kontrollieren!), können wir nun unser eigenes benutzerdefiniertes Sandbox-Profil anwenden, indem wir eine Bibliothek mit dem folgenden einfachen Code einfügen:
Durch die Festlegung eines sorgfältig ausgearbeiteten Sandbox-Profils für unsere Zielanwendungen sollten wir in der Lage sein, alle gängigen Jailbreak-Erkennungsmechanismen standardmäßig vollständig zu umgehen, ohne auf herkömmliche Jailbreak-Erkennungsumgehungen zurückgreifen zu müssen. Die Wirksamkeit der Jailbreak-Erkennung kann immer noch bewertet werden, indem einfach die Sandbox-Einschränkungen entfernt und das Verhalten der Anwendung untersucht wird. Dadurch wird die Bewertung von Anwendungen effizienter.
In der Praxis führte die Anwendung dieses Ansatzes mit einem Sandbox-Profil, das den Lese- und Schreibzugriff auf jeden Ort außerhalb des Containers der Anwendung verbietet, dazu, dass eine Testanwendung keine Jailbreak-Flags mehr auslöste:
In diesem Beitrag haben wir besprochen, wie Sie grafische iOS-Anwendungen unter macOS mit voller Kontrolle ausführen können. Es wurde erörtert, wie dies für das Anwendungs-Pentesting und die Sicherheitsforschung genutzt werden kann. Es gibt noch Verbesserungen, aber bisher scheint es, dass die vorliegende Lösung für Forschung und Sicherheitstests perfekt geeignet ist. Wir werden in naher Zukunft so viele Anwendungen wie möglich testen, um eine Aussage über die Anwendbarkeit dieses Ansatzes für die alltäglichen Anwendungstests machen zu können.
Eine WIP-Implementierung des besprochenen Launcher-Tools finden Sie unter srepsa/launchr (github.com).