1 points par GN⁺ 2025-02-19 | 1 commentaires | Partager sur WhatsApp
  • La rétro-ingénierie d’apps iOS nécessite de pouvoir observer et manipuler l’app en cours d’exécution, mais cette app de widgets combine blocage du débogueur, blocage de l’injection de code et détection du jailbreak
  • Le blocage principal peut être implémenté avec PT_DENY_ATTACH de ptrace, ou via un appel système direct ayant le même effet (svc #0x80), ce qui ne se détecte pas avec un simple breakpoint sur ptrace
  • L’appel système direct se contourne en recherchant dans le binaire le motif mov w16, #26, en plaçant un breakpoint à l’emplacement de svc, puis en sautant cette instruction avec lldb jump
  • Le comportement qui déclenche un soft reboot/respring du téléphone après détection du jailbreak était lié à une fonction appelant snapshotViewAfterScreenUpdates: en boucle infinie ; on le saute avec thread return au début de la fonction
  • Le crash après injection de code ressemblait davantage à une perte des droits App Group qu’à une vérification séparée du framework au runtime ; en remplaçant containerURLForSecurityApplicationGroupIdentifier: par un répertoire temporaire via swizzling, l’app peut aussi s’exécuter sur un appareil non jailbreaké

Une app de widgets iOS qui empile plusieurs protections

  • La cible est une app de widgets de l’App Store, qui inclut des comportements défensifs plus poussés que ceux d’une app de widgets classique
    • Blocage de l’attach d’un débogueur
    • Fermeture de l’app en cas d’injection de code
    • Soft reboot/respring de tout le téléphone si elle est lancée dans un environnement jailbreaké
  • Il n’est pas rare que des apps iOS intègrent des protections comme la détection de jailbreak ou l’obfuscation de code, mais celle-ci combine plusieurs méthodes
  • D’autres comportements intéressants à l’intérieur de l’app sont réservés pour un prochain article

Blocage de l’attach du débogueur avec PT_DENY_ATTACH

  • Sur un appareil jailbreaké, on peut généralement se connecter en ssh, lancer debugserver, puis se connecter depuis lldb sur un autre ordinateur pour déboguer l’app
  • En attachant debugserver à cette app de la même manière, une Segmentation fault se produit et l’attach échoue
  • La cause est liée à la requête PT_DENY_ATTACH de ptrace
    • ptrace est une API privée sur iOS et une API publique sur macOS
    • PT_DENY_ATTACH définit un flag qui refuse ensuite le traçage par le processus parent
    • Si le processus est déjà en cours de traçage, l’app se termine avec l’état ENOTSUP
    • Si le parent tente de tracer un processus sur lequel ce flag est défini, une segmentation violation se produit côté parent
  • Une implémentation simple consiste à appeler ptrace(PT_DENY_ATTACH, 0, 0, 0)
    • Comme il s’agit d’une API privée iOS, l’appel réel doit trouver le symbole ptrace de libsystem_kernel.dylib via dlopen et dlsym
    • Cette méthode se contourne assez facilement en plaçant un breakpoint sur ptrace, puis en sautant l’appel avec thread return

Pourquoi le contournement facile n’a pas fonctionné

  • PT_DENY_ATTACH ne bloque le débogueur qu’après avoir été appelé ; si l’on s’attache avant l’exécution du code de l’app, on peut créer un point de contournement
  • Au lieu d’attacher directement debugserver au processus, on le lance, puis depuis lldb on attend le lancement de l’app avec process attach --name TopWidget --waitfor, ce qui permet de s’attacher avant l’exécution du code de l’app
  • Mais dans cette app, le breakpoint b ptrace n’était pas résolu au départ et, même lorsqu’il l’était ensuite, l’app se terminait sans que le breakpoint ne soit effectivement atteint
  • C’est parce que l’app utilisait un appel système direct produisant le même effet, plutôt qu’un appel à la fonction ptrace

Trouver l’emplacement de l’appel système direct

  • Le désassemblage de la fonction ptrace contient essentiellement l’appel système svc #0x80
    • x0 reçoit la valeur 31, celle de PT_DENY_ATTACH
    • x1, x2 et x3 reçoivent les arguments inutilisés 0
    • x16 reçoit le numéro d’appel système de ptrace, soit 26
  • L’app peut ne pas appeler la fonction ptrace, mais définir les mêmes valeurs de registres en inline assembly, puis exécuter directement svc #0x80
    • Cette méthode évite les recherches d’API privées suspectes comme dlopen ou dlsym
    • Elle est difficile à détecter avec une approche consistant à placer un breakpoint sur la fonction commune ptrace
  • Pour le contourner, il faut ouvrir le binaire déchiffré de l’app dans un désassembleur et trouver l’emplacement de l’appel système ptrace
  • La cible de recherche est mov x16, #26, ou mov w16, #26, qui est la vue 32 bits du même registre
    • armconverter.com permet d’obtenir les octets 50 03 80 D2 de mov x16, #26, utilisables pour rechercher dans le binaire
    • mov x16, #26 n’a donné aucun résultat, tandis que la recherche de mov w16, #26 en a donné 4
  • Parmi eux, deux résultats ne correspondaient pas au motif attendu dans les instructions voisines, et le troisième contenait du code de la forme suivante
    • MOV X0, #0x1F
    • MOV X1, #0
    • MOV X2, #0
    • MOV X3, #0
    • MOV W16, #0x1A
    • SVC 0x80
  • Le quatrième résultat était une autre branche de la même fonction, confirmée comme l’endroit qui bloque l’attach du débogueur

Sauter l’instruction svc

  • Les adresses svc identifiées dans le désassembleur sont 0x102A2BB14 et 0x102A2BB68
  • Dans lldb, on ajoute -s TopWidget pour définir les breakpoints en convertissant l’adresse relative au binaire en adresse de chargement réelle
    • br s -a 0x102A2BB14 -s TopWidget
    • br s -a 0x102A2BB68 -s TopWidget
  • En reprenant l’exécution, le breakpoint se déclenche à l’emplacement de svc #0x80
  • Le contournement le plus simple consiste à jump à l’adresse de l’instruction suivante, afin de ne pas exécuter l’appel système lui-même
    • Dans l’exemple, la commande jump *0x10327bb18 saute à l’adresse 0x10327bb18, qui suit l’instruction courante
  • Après cette étape, on peut entrer dans l’app avec le débogueur attaché

Le comportement qui déclenche un soft reboot du téléphone

  • Même après avoir contourné l’attach du débogueur, l’app exécute encore un comportement de protection qui déclenche un soft reboot/respring du téléphone
  • Cette fois, lldb est toujours attaché, ce qui permet d’observer l’état où le processus a reçu SIGKILL ainsi que la stacktrace
  • La stacktrace montre un flux qui capture le contenu de l’écran
    • CARenderServerSnapshot de QuartzCore
    • _UISnapshotScreenWindowsRectAfterCommit de UIKitCore
    • Un symbole non nommé de TopWidget, interne à l’app
  • lldb image lookup permet de convertir l’adresse runtime en adresse relative au binaire 0x100041898, puis d’examiner la fonction correspondante dans le désassembleur
  • D’après le résultat de la décompilation, la fonction ne fait que répéter les opérations suivantes dans une boucle infinie
    • Appel de +[UIScreen mainScreen]
    • Appel de snapshotViewAfterScreenUpdates: sur l’objet écran renvoyé
    • Le résultat n’est pas utilisé et est libéré
  • snapshotViewAfterScreenUpdates: est une API publique qui crée un snapshot de vue, mais cette app répète indéfiniment cet appel coûteux en mémoire
  • Dans la vidéo liée, l’origine de cet appel est associée à la notification com.apple.tw.twrr, et il est confirmé qu’un respring se produit si le contrôle de risque du téléphone échoue
  • Le contournement consiste à placer un breakpoint au début de cette fonction et, lorsqu’il est atteint, à sauter son exécution avec thread return

Le crash provoqué par l’injection de code

  • Pendant le débogage, on peut implémenter des utilitaires complexes, comme la journalisation des informations d’accessibilité des boutons à l’écran, dans un framework injecté dans l’app, puis les appeler depuis le débogueur
  • Même sans appareil jailbreaké, l’injection de Frida ou de Flex permet une exploration initiale de l’app
  • En général, ce type d’injection est réalisé avec un outil qui resign l’app ; Sideloadly est utilisé ici comme exemple
  • Cette app crash immédiatement lorsqu’elle est lancée après resign
  • Dans la pile de crash, l’adresse supérieure 0x1002027D4 correspond dans le désassembleur à une instruction BRK, ce qui concorde avec un crash volontaire comme le force-unwrap d’une valeur nil
  • Dans le code décompilé, l’appel qui semble poser problème est containerURLForSecurityApplicationGroupIdentifier:
    • Cette méthode renvoie l’URL d’un dossier accessible conjointement aux apps et extensions d’un même groupe
    • L’App Group est défini pendant le processus de signature du code
    • Après l’injection de code et le resign, la signature d’origine de l’app est supprimée, tout comme les droits App Group
    • Il est donc très probable que nil soit renvoyé au lieu d’une URL, puis que l’app crash en effectuant un force-unwrap
  • Comme une app de widgets doit partager un App Group avec l’extension chargée d’afficher le widget sur l’écran d’accueil, ce problème ressemble davantage à un problème courant de signature de code qu’à un comportement défensif intentionnel

Contourner le problème d’App Group

  • La solution la plus simple consiste à ne pas resign l’app
    • Sur un appareil jailbreaké, on peut injecter un framework avec un jailbreak tweak sans resign l’app
    • Il est aussi possible d’exécuter du code dont la signature n’est pas valide, ou d’ajouter l’app voulue au groupe voulu
  • Même sans appareil jailbreaké, lorsqu’un resign est nécessaire, on peut contourner le problème en swizzlant une méthode avec un petit framework
  • Le code d’exemple remplace containerURLForSecurityApplicationGroupIdentifier: de NSFileManager
    • Lorsque la méthode originale est appelée, la méthode de remplacement s’exécute
    • La méthode de remplacement renvoie temporaryDirectory au lieu du shared container
  • Ce remplacement n’est pas équivalent au comportement attendu par l’app à l’origine
    • L’app voulait initialement un dossier partagé accessible à la fois par l’app et par l’extension
    • Le répertoire temporaire n’est pas un tel dossier partagé
  • Toutefois, cela peut suffire si l’objectif est uniquement d’observer le comportement de l’app principale
    • Ce genre de petit patch peut casser l’app extension
    • Si seules les fonctions de base de l’app principale sont nécessaires, cela peut ne pas poser problème
  • Un contournement plus complet consisterait à créer un nouvel App Group, à resign l’app principale et toutes les extensions avec ce groupe, puis à swizzler les méthodes concernées pour qu’elles utilisent le nouvel identifiant de groupe
    • C’est beaucoup plus lourd ; si ce n’est pas indispensable, il peut être préférable de se procurer un appareil jailbreaké
  • En injectant ce framework avec un outil comme Flex, l’app s’exécute normalement même sur un appareil non jailbreaké
  • Sur un appareil jailbreaké, il faut à nouveau contourner les protections anti-débogage et anti-respring décrites plus haut, mais on peut ensuite entrer dans l’app avec Flex injecté

État final

  • Au final, l’app peut être utilisée avec un débogueur attaché, la détection de jailbreak contournée et l’injection de code active
  • Ce que l’auteur cherchait réellement à examiner dans l’app sera le sujet d’un prochain article

1 commentaires

 
GN⁺ 2025-02-19
Commentaires Hacker News
  • Bryce Bostwick fait un travail vraiment excellent et inspirant sur le débogage d’apps et la rétro-ingénierie.
    Je l’ai découvert sur YouTube, et après avoir vu sa vidéo où il modifie TikTok pour n’afficher que des vidéos de chats (https://youtu.be/YW3jL2gI9IE), j’ai essayé de modifier Instagram pour ne garder que la fonction de messagerie que j’utilise et supprimer tout le reste.
    Depuis longtemps, j’avais envie d’explorer davantage les façons de modifier Windows à la manière de Windhawk (https://windhawk.net/), surtout la modification et la rétro-ingénierie ; Bryce présente très bien ce genre de travail sur iOS, en vidéo étape par étape et en temps réel.

    • S’il existe quelqu’un de similaire côté Android, j’aimerais beaucoup en apprendre davantage.
      J’ai vu qu’on pouvait faire des choses incroyables avec Revanced, mais il ne semble pas y avoir beaucoup de bons guides expliquant comment se lancer dans ce genre de travail.
    • Je vais essayer de reproduire ce qu’il a expliqué.
      Si jamais quelqu’un documente la procédure, ça m’intéresse.
      Je déteste devoir être exposé aux Reels même quand je veux simplement publier une photo ou discuter avec des amis.
  • Les techniques d’anti-débogage, et même celles qui empêchent de neutraliser l’anti-débogage, existent depuis longtemps côté DOS/Windows.
    Les vieux documents sur le cracking ou l’unpacking traitent de ces sujets avec des niveaux de profondeur variés.
    La facilité avec laquelle l’utilisateur peut contrôler le comportement d’une app est inversement proportionnelle au caractère hostile à l’utilisateur de la plateforme.
    PT_DENY_ATTACH ressemble précisément à une fonctionnalité conçue pour cette hostilité envers l’utilisateur.
    À ma connaissance, Windows n’a pas de fonctionnalité de ce type ; à la place, on utilise des techniques qui font que l’application s’attache à elle-même.
    https://www.x86matthew.com/view_post?id=selfdebug
    https://anti-debug.checkpoint.com/techniques/interactive.htm...

    • Exact. PT_DENY_ATTACH est littéralement une fonctionnalité qu’Apple a créée à l’époque dans le cadre de la solution DRM d’iTunes.
  • Je suis un peu surpris que la revue de l’App Store d’Apple ne rejette pas les apps qui effectuent des appels système directs.
    Sur les plateformes Apple, les appels système ne constituent pas une ABI stable : tous les appels système doivent passer par libSystem, et une app qui effectue des appels système directement sans passer par libSystem fait, en quelque sorte, quelque chose qu’elle ne devrait pas faire.
    De même, je me demande pourquoi, ici, l’auteur a cherché mov w16, #26 dans le code plutôt que svc 0x80.

    • svc 0x80 est l’instruction qui exécute n’importe quel appel système ; l’appel exact exécuté dépend du registre x16.
      L’app effectuera énormément d’appels système sans rapport, donc placer un point d’arrêt dessus ne serait probablement pas utile.
      C’est du moins ainsi que c’était expliqué dans la vidéo.
    • Le compilateur peut parfois inliner les wrappers d’appels système, ce qui rend la vérification statique pas si simple.
      Pour la même raison, chercher l’instruction SVC produirait énormément de résultats.
      Si l’on cherche l’ID exact de l’appel système déplacé dans X16, on le trouve immédiatement.
  • Je suis l’auteur de l’article. Je répondrai si vous avez des questions. Merci à xmprt de l’avoir partagé.

    • J’ai vu la vidéo sur YouTube, c’était intéressant.
      J’ai aussi apprécié qu’une version écrite soit fournie.
    • C’était un article très intéressant, et j’ai toujours envie de voir davantage d’articles de rétro-ingénierie bas niveau sur mobile de ce genre.
      J’aimerais poser quelques questions à l’auteur : si Guardsquare est bien l’outil commercial le plus connu, je me demande s’il propose de nouvelles protections contre ce type de désassemblage facile.
      Je me demande aussi si TopWidgets utilisait une protection similaire, ou si c’était plutôt quelque chose développé en interne.
    • Les vidéos sont très intéressantes, et je suis surpris que davantage de gens ne les aient pas vues ou n’aient pas lu l’article.
      Personnellement, j’utilise Android, donc ce n’est pas directement applicable techniquement, mais j’en retire tout de même beaucoup pour comprendre comment fonctionne le débogage bas niveau sur iOS.
    • Je me demande si iOS dispose d’une fonctionnalité semblable à PTRACE_SYSCALL, permettant de s’accrocher au point d’entrée d’un appel système pour modifier la valeur de retour, ou de détecter où un SVC se produit.
  • La vidéo tout en haut fait partie des meilleures vidéos de programmation que j’aie vues jusqu’ici.
    Le rythme est rapide, elle suppose exactement le bon niveau de connaissances préalables, et elle contient d’excellentes démonstrations qui n’interrompent pas le fil de la vidéo.

  • Excellent article.
    Je me demande vraiment s’il s’agissait d’une app normale mais excessivement paranoïaque, ou d’une app qu’on déboguait parce qu’elle était soupçonnée d’être un malware dès le départ.
    Sinon, l’effort fourni semble assez disproportionné.

    • D’après mon jugement, c’est plutôt simplement une app excessivement paranoïaque.
      Elle fait des choses assez sympas avec les widgets, et je pense qu’ils ont voulu protéger cela. Cela dit, ces stratégies ont fini de toute façon par commencer à fuiter petit à petit.
      Il y avait aussi des choses intéressantes dans le binaire.
      À un moment, j’ai essayé de comprendre pourquoi je regardais du code qui semblait télécharger un .iso Windows ; en réalité c’était bien le cas, et c’était utilisé pour un widget de test de débit réseau.
    • Ou alors, il s’agissait peut-être de prouver une violation de copyright dans le cas où l’app aurait été recompilée avec un autre logo, etc.
  • Il existe un mode encore plus difficile que « contourner PT_DENY_ATTACH (mode difficile) ».
    Il y a quelque temps, sur macOS, j’ai patché le noyau pour que PT_DENY_ATTACH ne fasse rien.
    Sur Mac, exécuter un noyau patché est en fait assez simple, mais sur iOS cela doit être beaucoup plus pénible à cause de choses comme KTRR.
    Même si XNU est techniquement open source, il était plus simple d’appliquer le patch avec un éditeur hexadécimal que de le recompiler.

    • On peut aussi utiliser le kernel task port pour inverser un bit dans la structure proc.
      On peut également autoriser les pages de code non signées et RWX, ce qui rend le JIT possible.
  • Si « quand on l’exécute avec le jailbreak activé, tout le téléphone plante », ne suffit-il pas de le signaler au store comme malware ?
    Faire planter un téléphone est clairement un comportement de malware, et il faut se demander quelles autres actions malveillantes il pourrait tenter de cacher.
    N’était-ce pas censé être la justification de l’écosystème fermé d’Apple, empêcher ce genre de saloperies ?

    • « Avec le jailbreak activé » signifie que l’on est en dehors de l’écosystème fermé.
      Je doute qu’Apple se soucie beaucoup des apps qui plantent sur des téléphones jailbreakés.
  • La notification com.apple.tw.twrr m’intrigue vraiment.
    Pourquoi commence-t-elle par com.apple ?
    L’app en question ici ne semble pas être une app Apple, mais une app appelée Top Widgets.

    • Le nom d’une notification est une chaîne arbitraire.
      Par convention, on utilise ce genre de « nom complet » pour éviter les collisions.
      Dans ce cas, le développeur a probablement choisi ce préfixe par hasard.
  • Outils mis à part, cela donne exactement la même impression que lorsqu’on cassait les protections anticopie de l’Apple II dans les années 1980 par traçage du démarrage.
    Certaines choses ne changent pas.