1 Punkte von GN⁺ 2025-02-19 | 1 Kommentare | Auf WhatsApp teilen
  • Reverse Engineering von iOS-Apps erfordert, laufende Apps beobachten und manipulieren zu können; diese Widget-App nutzt jedoch Debugger-Blockierung, Code-Injection-Blockierung und Jailbreak-Erkennung zusammen
  • Die zentrale Blockierung kann über PT_DENY_ATTACH von ptrace oder einen direkten Systemaufruf mit derselben Wirkung (svc #0x80) implementiert sein, weshalb ein einfacher ptrace-Breakpoint sie nicht erfasst
  • Direkte Systemaufrufe werden umgangen, indem man im Binary nach dem Muster mov w16, #26 sucht, an der svc-Position einen Breakpoint setzt und die Anweisung mit lldb jump überspringt
  • Das Verhalten, das nach Jailbreak-Erkennung das Telefon soft-rebootet bzw. einen Respring auslöst, war mit einer Funktion verbunden, die snapshotViewAfterScreenUpdates: in einer Endlosschleife aufruft; sie wird am Funktionsanfang mit thread return übersprungen
  • Der Crash nach Code-Injection sah weniger nach einer separaten Runtime-Framework-Prüfung aus, sondern eher nach Verlust der App-Group-Berechtigung; durch ein Swizzling von containerURLForSecurityApplicationGroupIdentifier: auf ein temporäres Verzeichnis lässt sich die App auch auf normalen Geräten ausführen

Eine iOS-Widget-App mit mehreren übereinandergelegten Schutzmechanismen

  • Ziel ist eine Widget-App aus dem App Store, die stärkere Abwehrmechanismen enthält als typische Widget-Apps
    • Blockieren des Debugger-Attachments
    • Beenden der App bei Code-Injection
    • Bei Ausführung in einer Jailbreak-Umgebung wird das gesamte Telefon soft-rebootet bzw. ein Respring ausgelöst
  • Dass iOS-Apps Schutzmechanismen wie Jailbreak-Erkennung oder Code-Obfuskation einbauen, ist nicht ungewöhnlich; diese App kombiniert jedoch mehrere Methoden
  • Weitere interessante Verhaltensweisen innerhalb der App bleiben Thema eines späteren Beitrags

Blockieren des Debugger-Attachments mit PT_DENY_ATTACH

  • Auf einem Jailbreak-Gerät kann man sich üblicherweise per ssh verbinden, debugserver starten und die App von lldb auf einem anderen Computer aus debuggen
  • Wenn man debugserver auf dieselbe Weise an diese App anhängt, tritt ein Segmentation fault auf und das Attach schlägt fehl
  • Die Ursache hängt mit der PT_DENY_ATTACH-Anfrage von ptrace zusammen
    • ptrace ist unter iOS eine private API und unter macOS eine öffentliche API
    • PT_DENY_ATTACH setzt ein Flag, das nachfolgend das Tracing durch den Elternprozess verweigert
    • Wird der Prozess bereits getraced, beendet sich die App mit dem Status ENOTSUP
    • Versucht der Elternprozess, einen Prozess mit gesetztem Flag zu tracen, kommt es auf der Elternseite zu einer segmentation violation
  • Eine einfache Implementierung ist per Aufruf von ptrace(PT_DENY_ATTACH, 0, 0, 0) möglich
    • Da es sich unter iOS um eine private API handelt, muss der tatsächliche Aufruf das ptrace-Symbol in libsystem_kernel.dylib per dlopen und dlsym finden
    • Diese Methode lässt sich relativ leicht umgehen, indem man einen Breakpoint auf ptrace setzt und den Aufruf mit thread return überspringt

Warum die einfache Umgehung nicht funktionierte

  • PT_DENY_ATTACH blockiert den Debugger erst nach dem Aufruf; wenn man also vor Ausführung des App-Codes attached, kann man einen Umgehungspunkt setzen
  • Statt debugserver direkt an den Prozess anzuhängen, startet man ihn und wartet dann in lldb mit process attach --name TopWidget --waitfor auf den Start der App, sodass man vor der Ausführung des App-Codes attachen kann
  • In dieser App wurde der Breakpoint b ptrace anfangs jedoch nicht aufgelöst; selbst nachdem er später aufgelöst wurde, wurde er nicht tatsächlich getroffen, bevor die App beendet wurde
  • Der Grund war, dass die App statt eines Aufrufs der Funktion ptrace einen direkten Systemaufruf mit derselben Wirkung verwendete

Die Stelle des direkten Systemaufrufs finden

  • Die Disassembly der Funktion ptrace enthält im Kern den Systemaufruf svc #0x80
    • In x0 steht 31, der Wert von PT_DENY_ATTACH
    • In x1, x2 und x3 stehen die ungenutzten Argumente 0
    • In x16 steht 26, die Systemaufrufnummer von ptrace
  • Die App kann statt eines Aufrufs der Funktion ptrace dieselben Registerwerte per Inline-Assembly setzen und dann direkt svc #0x80 ausführen
    • Diese Methode vermeidet verdächtige private API-Lookups wie dlopen und dlsym
    • Sie ist schwer zu erwischen, wenn man nur einen Breakpoint auf die gemeinsame Funktion ptrace setzt
  • Zur Umgehung muss man das entschlüsselte App-Binary in einem Disassembler öffnen und die Stelle des ptrace-Systemaufrufs finden
  • Gesucht wird nach mov x16, #26 oder nach der 32-Bit-View desselben Registers, mov w16, #26
    • Mit armconverter.com erhält man für mov x16, #26 die Bytes 50 03 80 D2, die für eine Binary-Suche verwendet werden können
    • mov x16, #26 lieferte keine Treffer; die Suche nach mov w16, #26 ergab 4 Treffer
  • Zwei dieser Treffer passten bei den umliegenden Anweisungen nicht zum erwarteten Muster; beim dritten Treffer zeigte sich Code in folgender Form
    • MOV X0, #0x1F
    • MOV X1, #0
    • MOV X2, #0
    • MOV X3, #0
    • MOV W16, #0x1A
    • SVC 0x80
  • Der vierte Treffer war ein anderer Branch derselben Funktion, und diese Funktion wurde als die Stelle bestätigt, die das Debugger-Attachment blockiert

Die svc-Anweisung überspringen

  • Die im Disassembler bestätigten svc-Adressen sind 0x102A2BB14 und 0x102A2BB68
  • In lldb wird -s TopWidget angehängt, um die binarybezogene Adresse in die tatsächliche Ladeadresse zu übersetzen und Breakpoints zu setzen
    • br s -a 0x102A2BB14 -s TopWidget
    • br s -a 0x102A2BB68 -s TopWidget
  • Setzt man die Ausführung fort, wird der Breakpoint an der Stelle svc #0x80 getroffen
  • Die einfachste Umgehung besteht darin, per jump zur nächsten Befehlsadresse zu springen und den Systemaufruf selbst gar nicht auszuführen
    • Im Beispiel wird mit jump *0x10327bb18 zur Adresse 0x10327bb18, also direkt hinter der aktuellen Anweisung, gesprungen
  • Nach diesem Ablauf kann man bei angehängtem Debugger in die App gelangen

Das Verhalten, das das Telefon soft-rebootet

  • Auch nach Umgehung des Debugger-Attachments führt die App eine Schutzmaßnahme aus, die das Telefon soft-rebootet bzw. einen Respring auslöst
  • Diesmal ist lldb weiterhin attached, sodass man den Zustand, in dem der Prozess SIGKILL erhalten hat, und den Stacktrace prüfen kann
  • Im Stacktrace erscheint ein Ablauf, der den Bildschirminhalt erfasst
    • CARenderServerSnapshot aus QuartzCore
    • _UISnapshotScreenWindowsRectAfterCommit aus UIKitCore
    • ein unnamed symbol in der App TopWidget
  • Mit lldb image lookup wird die Runtime-Adresse in die binarybezogene Adresse 0x100041898 umgerechnet; anschließend wird die entsprechende Funktion im Disassembler geprüft
  • Das Decompilation-Ergebnis zeigt, dass die Funktion in einer Endlosschleife nur die folgenden Schritte wiederholt
    • Aufruf von +[UIScreen mainScreen]
    • Aufruf von snapshotViewAfterScreenUpdates: auf dem zurückgegebenen Bildschirmobjekt
    • Das Ergebnis wird nicht verwendet und freigegeben
  • snapshotViewAfterScreenUpdates: ist eine öffentliche API zum Erstellen eines View-Snapshots, doch diese App wiederholt den speicherintensiven Aufruf endlos
  • Im verlinkten Video wurde bestätigt, dass der Ursprung dieses Aufrufs mit der Notification com.apple.tw.twrr zusammenhängt und ein Respring erfolgt, wenn der Risk Check für das Telefon nicht bestanden wird
  • Die Umgehung ist möglich, indem man am Einstiegspunkt der betreffenden Funktion einen Breakpoint setzt und bei einem Treffer die Funktionsausführung mit thread return überspringt

Crash bei Code-Injection

  • Beim Debugging kann man komplexe Hilfsfunktionen, etwa zum Loggen von Accessibility-Informationen von Bildschirm-Buttons, in einem in die App injizierten Framework implementieren und sie dann aus dem Debugger aufrufen
  • Auch ohne Jailbreak-Gerät kann man die App zunächst erkunden, indem man Frida oder Flex injiziert
  • Üblicherweise erfolgt eine solche Injection mit Tools, die die App neu signieren; als Beispiel wird Sideloadly verwendet
  • Diese App crasht sofort, wenn sie nach dem Resigning gestartet wird
  • Betrachtet man die oberste Adresse des Crash-Stacks, 0x1002027D4, im Disassembler, findet man dort eine BRK-Anweisung, was zu einer absichtlichen Crash-Situation wie einem Force-Unwrap von nil passt
  • Der im decompilierten Code auffällige Aufruf ist containerURLForSecurityApplicationGroupIdentifier:
    • Diese Methode gibt die URL eines Ordners zurück, auf den Apps und Extensions derselben Group gemeinsam zugreifen können
    • Die App Group wird beim Code-Signing definiert
    • Durch Code-Injection mit anschließendem Resigning wird die ursprüngliche App-Signatur verworfen, und auch die App-Group-Berechtigung geht verloren
    • Daher wird wahrscheinlich statt einer URL nil zurückgegeben, und die App crasht, weil sie diesen Wert per Force-Unwrap entpackt
  • Da eine Widget-App die App Group mit der Extension teilen muss, die für die Anzeige des Home-Screen-Widgets zuständig ist, wirkt dieses Problem eher wie ein häufiges Code-Signing-Problem als wie eine absichtliche Abwehrmaßnahme

Umgehung des App-Group-Problems

  • Die einfachste Lösung besteht darin, die App nicht neu zu signieren
    • Auf einem Jailbreak-Gerät kann man ein Framework per Jailbreak-Tweak injizieren, ohne die App neu zu signieren
    • Es ist auch möglich, Code mit ungültiger Signatur auszuführen oder eine gewünschte App einer gewünschten Group hinzuzufügen
  • Wenn kein Jailbreak-Gerät vorhanden ist und Resigning nötig ist, kann man das Problem dennoch mit einem kleinen Framework umgehen, das eine Methode swizzlet
  • Der Beispielcode ersetzt containerURLForSecurityApplicationGroupIdentifier: von NSFileManager
    • Wenn die ursprüngliche Methode aufgerufen wird, läuft die replacement method
    • Die replacement method gibt nicht den shared container zurück, sondern temporaryDirectory
  • Dieser Ersatz ist nicht identisch mit dem ursprünglich gewünschten Verhalten der App
    • Die ursprüngliche App erwartet einen gemeinsamen Ordner, auf den App und Extension zusammen zugreifen können
    • Ein temporäres Verzeichnis ist kein solcher gemeinsamer Ordner
  • Wenn es jedoch nur darum geht, das Verhalten der Haupt-App zu untersuchen, kann das ausreichend sein
    • Ein solcher kleiner Patch kann die App Extension beschädigen
    • Wenn nur die Grundfunktionen der Haupt-App benötigt werden, ist das möglicherweise kein Problem
  • Eine umfangreichere Umgehung bestünde darin, eine neue App Group zu erstellen, die Haupt-App und alle Extensions für diese Group neu zu signieren und die relevanten Methoden so zu swizzlen, dass sie den neuen Group-Identifier verwenden
    • Das ist deutlich mehr Aufwand; falls es nicht unbedingt nötig ist, ist es womöglich besser, sich ein Jailbreak-Gerät zu besorgen
  • Wird dieses Framework zusammen mit einem Tool wie Flex injiziert, läuft die App auch auf einem normalen Gerät korrekt
  • Auf einem Jailbreak-Gerät müssen die zuvor genannten Anti-Debugging- und Respring-Schutzmaßnahmen erneut umgangen werden; danach kann man jedoch mit injiziertem Flex in die App gelangen

Endzustand

  • Am Ende ist die App in einem Zustand, in dem Debugger-Attachment, Umgehung der Jailbreak-Erkennung und Code-Injection alle möglich sind
  • Was tatsächlich innerhalb der App untersucht werden sollte, bleibt Thema des nächsten Beitrags

1 Kommentare

 
GN⁺ 2025-02-19
Meinungen auf Hacker News
  • Bryce Bostwick leistet wirklich coole und inspirierende Arbeit beim Debugging und Reverse Engineering von Apps.
    Ich bin über YouTube auf ihn gestoßen und habe, nachdem ich sein Video gesehen hatte, in dem er TikTok so umbaut, dass nur noch Katzenvideos erscheinen (https://youtu.be/YW3jL2gI9IE), versucht, Instagram so umzubauen, dass nur noch die Nachrichtenfunktion übrig bleibt, die ich nutze, und alles andere entfernt wird.
    Ich wollte mich schon länger tiefer damit beschäftigen, Windows im Stil von Windhawk (https://windhawk.net/) anzupassen, insbesondere mit Modding und Reverse Engineering. Bryce zeigt solche Arbeit auf iOS sehr gut in Schritt-für-Schritt-Videos in Echtzeit.

    • Wenn es auf Android-Seite jemanden Ähnliches gibt, würde ich gern mehr lernen.
      Ich habe gesehen, dass man mit Revanced Erstaunliches machen kann, aber es scheint nicht viele gute Guides zu geben, die erklären, wie man mit so etwas anfängt.
    • Ich will ausprobieren, was beschrieben wurde.
      Falls jemand den Ablauf dokumentiert hat, hätte ich Interesse.
      Ich hasse es, dass ich Reels ausgesetzt sein muss, selbst wenn ich nur Fotos hochladen oder mit Freunden chatten will.
  • Anti-Debugging und sogar Techniken, die wiederum das Verhindern von Anti-Debugging verhindern, sind in der DOS-/Windows-Welt schon lange verbreitet.
    In alten Materialien zu Cracking oder Unpacking wird das in unterschiedlicher Tiefe behandelt.
    Wie leicht Nutzer das Verhalten einer App kontrollieren können, ist umgekehrt proportional dazu, wie nutzerfeindlich eine Plattform ist.
    PT_DENY_ATTACH wirkt wie eine Funktion, die genau für diese Nutzerfeindlichkeit gebaut wurde.
    Soweit ich weiß, gibt es so etwas unter Windows nicht; stattdessen nutzt man Techniken, bei denen sich eine App an sich selbst anhängt.
    https://www.x86matthew.com/view_post?id=selfdebug
    https://anti-debug.checkpoint.com/techniques/interactive.htm...

    • Stimmt. PT_DENY_ATTACH ist eine Funktion, die Apple früher buchstäblich selbst als Teil der DRM-Lösung von iTunes gebaut hat.
  • Es überrascht mich etwas, dass Apples App-Store-Prüfung Apps, die direkte System Calls ausführen, nicht ablehnt.
    System Calls auf Apple-Plattformen sind keine stabile ABI, deshalb müssen alle System Calls über libSystem laufen; eine App, die direkte System Calls ausführt, ohne über libSystem zu gehen, tut im Grunde etwas, das sie nicht tun sollte.
    Ebenso frage ich mich, warum der Autor hier im Code nach mov w16, #26 gesucht hat statt nach svc 0x80.

    • svc 0x80 ist die Instruktion, die beliebige System Calls ausführt; welcher Call genau ausgeführt wird, hängt vom Register x16 ab.
      Die App wird sehr viele nicht relevante System Calls ausführen, daher wäre ein Breakpoint darauf nicht nützlich.
      Zumindest wurde es im Video so erklärt.
    • Compiler können System-Call-Wrapper gelegentlich inline setzen, daher ist das statisch nicht so leicht zu prüfen.
      Aus demselben Grund würde die Suche nach SVC-Instruktionen enorm viele Treffer liefern.
      Wenn man die genaue System-Call-ID findet, die nach X16 verschoben wird, findet man es sofort.
  • Ich bin der Autor des Beitrags. Wenn ihr Fragen habt, beantworte ich sie gern. Danke an xmprt fürs Teilen.

    • Ich habe das Video auf YouTube gesehen, es war interessant.
      Schön, dass es auch eine Textversion gab.
    • Ein sehr interessanter Artikel; solche Beiträge über Low-Level-Reverse-Engineering auf Smartphones würde ich gern öfter sehen.
      Ich hätte ein paar Fragen an den Autor: Wenn Guardsquare tatsächlich das bekannteste kommerzielle Tool ist, würde mich interessieren, ob es neue Dinge anbietet, die eine so einfache Disassemblierung verhindern.
      Außerdem würde mich interessieren, ob TopWidgets einen ähnlichen Schutz eingesetzt hat oder ob das eher eine selbstgebaute Lösung war.
    • Die Videos sind sehr interessant, und ich bin überrascht, dass sie nicht mehr Leute gesehen bzw. den Beitrag gelesen haben.
      Persönlich nutze ich Android, daher ist es technisch nicht direkt anwendbar, aber ich ziehe trotzdem viel Wert daraus, zu lernen, wie Low-Level-Debugging unter iOS funktioniert.
    • Ich frage mich, ob iOS etwas wie PTRACE_SYSCALL hat, mit dem man am Einstiegspunkt eines System Calls einhaken und Rückgabewerte ändern oder erkennen kann, wo ein SVC ausgelöst wird.
  • Das Video ganz oben gehört zu den besten Programmier-Videos, die ich je gesehen habe.
    Es hat ein hohes Tempo, setzt genau das richtige Maß an Vorwissen voraus und enthält hervorragende Demos, die den Fluss des Videos nicht unterbrechen.

  • Großartiger Artikel.
    Ich frage mich wirklich, ob das eine normale, aber übermäßig paranoide App war oder ob es eine App war, die von Anfang an als Malware verdächtigt und deshalb debuggt wurde.
    Falls nicht, wirkt der Aufwand ziemlich übertrieben.

    • Nach meiner Einschätzung ist es eher einfach eine übermäßig paranoide App.
      Sie macht mit Widgets ziemlich coole Dinge, und ich vermute, sie wollten das schützen. Allerdings haben solche Strategien ohnehin nach und nach angefangen durchzusickern.
      In der Binary gab es auch Interessantes.
      Eine Zeit lang versuchte ich herauszufinden, warum ich Code sah, der scheinbar eine Windows-.iso herunterlädt; tatsächlich stimmte das, und er wurde für ein Netzwerk-Speedtest-Widget verwendet.
    • Oder es könnte darum gegangen sein, Urheberrechtsverletzungen nachzuweisen, falls die App selbst mit einem anderen Logo usw. neu kompiliert wurde.
  • Es gibt noch einen schwierigeren Modus als „PT_DENY_ATTACH umgehen (Hard Mode)“.
    Ich habe früher unter macOS einmal den Kernel gepatcht, sodass PT_DENY_ATTACH nichts mehr tat.
    Auf dem Mac ist es tatsächlich ziemlich einfach, einen gepatchten Kernel auszuführen, aber unter iOS dürfte das wegen Dingen wie KTRR deutlich mühsamer sein.
    XNU ist technisch gesehen Open Source, aber es war einfacher, den Patch mit einem Hex-Editor anzuwenden, als ihn neu zu kompilieren.

    • Man könnte auch den kernel task port verwenden, um ein Bit in der proc-Struktur umzulegen.
      Damit kann man auch JIT möglich machen, indem man unsignierte Code-Pages und RWX erlaubt.
  • Wenn „mit aktiviertem Jailbreak ausgeführt und das ganze Telefon crasht“, sollte man es dann nicht im Store als Malware melden?
    Ein Telefon zum Absturz zu bringen, ist eindeutig Malware-Verhalten, und man sollte vermuten, dass es noch anderes bösartiges Verhalten gibt, das versteckt werden soll.
    War es nicht genau die Begründung für Apples geschlossenes Ökosystem, solchen Mist zu verhindern?

    • „Mit aktiviertem Jailbreak“ bedeutet, dass man sich außerhalb des geschlossenen Ökosystems befindet.
      Ich glaube nicht, dass Apple sich besonders dafür interessiert, wenn eine App auf einem gejailbreakten Telefon crasht.
  • Die Benachrichtigung com.apple.tw.twrr macht mich wirklich neugierig.
    Warum beginnt sie mit com.apple?
    Die hier fragliche App scheint keine Apple-App zu sein, sondern eine App namens Top Widgets.

    • Benachrichtigungsnamen sind beliebige Strings.
      Es ist üblich, solche „vollqualifizierten Namen“ zu verwenden, um Kollisionen zu vermeiden.
      In diesem Fall hat der Entwickler wohl zufällig dieses Präfix gewählt.
  • Abgesehen von den anderen Tools fühlt sich das exakt so an wie in den 1980ern, als man mit Boot-Tracing den Kopierschutz des Apple II geknackt hat.
    Manche Dinge ändern sich nicht.