Déboguer une app indébogable
(bryce.co)- 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 surptrace - L’appel système direct se contourne en recherchant dans le binaire le motif
mov w16, #26, en plaçant un breakpoint à l’emplacement desvc, puis en sautant cette instruction aveclldb 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 avecthread returnau 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, lancerdebugserver, puis se connecter depuislldbsur 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_ATTACHdeptraceptraceest une API privée sur iOS et une API publique sur macOSPT_DENY_ATTACHdé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
ptracedelibsystem_kernel.dylibviadlopenetdlsym - Cette méthode se contourne assez facilement en plaçant un breakpoint sur
ptrace, puis en sautant l’appel avecthread return
- Comme il s’agit d’une API privée iOS, l’appel réel doit trouver le symbole
Pourquoi le contournement facile n’a pas fonctionné
PT_DENY_ATTACHne 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
debugserverau processus, on le lance, puis depuislldbon attend le lancement de l’app avecprocess 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 ptracen’é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
ptracecontient essentiellement l’appel systèmesvc #0x80x0reçoit la valeur31, celle dePT_DENY_ATTACHx1,x2etx3reçoivent les arguments inutilisés0x16reçoit le numéro d’appel système deptrace, soit26
- L’app peut ne pas appeler la fonction
ptrace, mais définir les mêmes valeurs de registres en inline assembly, puis exécuter directementsvc #0x80- Cette méthode évite les recherches d’API privées suspectes comme
dlopenoudlsym - Elle est difficile à détecter avec une approche consistant à placer un breakpoint sur la fonction commune
ptrace
- Cette méthode évite les recherches d’API privées suspectes comme
- 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, oumov w16, #26, qui est la vue 32 bits du même registre- armconverter.com permet d’obtenir les octets
50 03 80 D2demov x16, #26, utilisables pour rechercher dans le binaire mov x16, #26n’a donné aucun résultat, tandis que la recherche demov w16, #26en a donné 4
- armconverter.com permet d’obtenir les octets
- 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, #0x1FMOV X1, #0MOV X2, #0MOV X3, #0MOV W16, #0x1ASVC 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
svcidentifiées dans le désassembleur sont0x102A2BB14et0x102A2BB68 - Dans
lldb, on ajoute-s TopWidgetpour définir les breakpoints en convertissant l’adresse relative au binaire en adresse de chargement réellebr s -a 0x102A2BB14 -s TopWidgetbr 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 *0x10327bb18saute à l’adresse0x10327bb18, qui suit l’instruction courante
- Dans l’exemple, la commande
- 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,
lldbest toujours attaché, ce qui permet d’observer l’état où le processus a reçuSIGKILLainsi que la stacktrace - La stacktrace montre un flux qui capture le contenu de l’écran
CARenderServerSnapshotdeQuartzCore_UISnapshotScreenWindowsRectAfterCommitdeUIKitCore- Un symbole non nommé de
TopWidget, interne à l’app
lldb image lookuppermet de convertir l’adresse runtime en adresse relative au binaire0x100041898, 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é
- Appel de
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
0x1002027D4correspond dans le désassembleur à une instructionBRK, 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
nilsoit 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:deNSFileManager- Lorsque la méthode originale est appelée, la méthode de remplacement s’exécute
- La méthode de remplacement renvoie
temporaryDirectoryau 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
Flexinjecté
É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
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.
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.
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...
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, #26dans le code plutôt quesvc 0x80.svc 0x80est 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.
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 aussi apprécié qu’une version écrite soit fournie.
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.
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.
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é.
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
.isoWindows ; en réalité c’était bien le cas, et c’était utilisé pour un widget de test de débit réseau.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 é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 ?
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.
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.