Article
Développement mobile
22.11.2013
Perturbation continuum temporel
8 minutes
Temps d'un café
Sécurité des Applications iOS - Compromission de binaire
Remy Stère
Note : Ce contenu a été créé avant que Fabernovel ne fasse partie du groupe EY, le 5 juillet 2022.

Nous avons vu dans notre un précédent article qu’un débogueur peut être utilisé pour déchiffrer une application et avoir accès à sa table des symboles. Grâce à cette table des symboles nous pouvons changer le comportement de l’application car nous connaissons son architecture interne. Aujourd’hui, nous allons nous intéresser à un des nombreux moyens de modifier le comportement d’une application : la compromission de binaire.

Nous allons présenter dans cet article comment tirer profit de la modification de l’assembleur contenu dans le fichier exécutable d’une application.

Un fichier exécutable d’une application iOS est un fichier Mach-O qui contient toutes les informations sur l’application (voir Mach-O File Format Reference) : architecture, librairies dynamiques, etc. En particulier, ce fichier Mach-O contient le code assembleur de l’application.

Les applications iOS sont compilées pour les architectures ARM. Cela signifie que vous devez partiellement connaître l’assembleur ARM pour modifier le binaire d’un programme. Pour le moment, gardez juste en tête que nous appelons opcode le code hexadécimal d’une opération assembleur. Par exemple, en ARM, l’opération nop (“no operation”) est associée à l’opcode 0xbf00.

Pour contourner les sécurités d’une application, il suffit de localiser la protection dans le binaire et de changer les opcodes associés. Par exemple, nous pouvons modifier le résultat d’une condition if en changeant simplement un opcode bne(branch if not equal) en un opcode beq (branch if equal). Ou nous pouvons charger des valeurs spécifiques dans les registres pour modifier la valeur de retour d’une fonction.

Modifier les opcodes assembleurs

Nous présentons ici un exemple simple de compromission de binaire. Rappelons-nous la fonction ptrace discutée en détail dans précédemment. Cette fonction désactive tout débogueur attaché à l’application quand elle est appelée avec le paramètre PT_DENY_ATTACH. Maintenant, imaginons que nous voulions supprimer un appel à ptrace dans une application et le remplacer par “rien”, c’est-à-dire une opération nop. De cette manière, l’application n’appellera jamais la fonction ptrace et nous pourrons alors lui attacher un débogueur.

Imaginons que nous ayons trouvé l’adresse exacte (0x5f5b2) où l’appel à ptrace est fait dans le fichier exécutable :

0x5f5ae: dd f8 0c 90 f8dd900c ldr.w r9, [sp, #12] 0x5f5b2: c8 47 47c8 blx r9 # call to ptrace 0x5f5b4: 0a 99 990a ldr r1, [sp, #40] 0x5f5b6: 01 90 9001 str r0, [sp, #4]

Pour contourner cet appel à ptrace, nous devons remplacer avec un éditeur hexadécimal le code 0x47c8 contenu à l’adresse 0x5f5b2 par le code 0xbf00 qui représente l’opcode d’une opération nop. Après modification, le binaire ressemblera à ça :

0x5f5ae: dd f8 0c 90 f8dd900c ldr.w r9, [sp, #12] 0x5f5b2: 00 bf bf00 nop # changed with nop 0x5f5b4: 0a 99 990a ldr r1, [sp, #40] 0x5f5b6: 01 90 9001 str r0, [sp, #4]

Quand cette partie du code sera exécutée dans l’application, l’opération nop ne fera rien et substituera l’appel à ptrace. De cette manière, l’application ne sera plus en mesure de détecter et de désactiver les débogueurs qui lui sont rattachés.

Calculer une signature du binaire

Une attaque au niveau binaire est difficile à contrer. Si un hacker est capable de réécrire du code assembleur, il sera certainement capable de contourner toutes les sécurités que nous avons implémentées dans nos applications iOS.

Mais quelque chose d’intéressant à savoir à propos des programmes Objective-C, c’est qu’ils sont capables d’inspecter leurs propres binaires. Une solution pour vérifier l’intégrité des opcodes assembleurs consite donc à parcourir les load commands du fichier Mach-O, à trouver la section assembleur (c’est-à-dire la section __text du segment __TEXT) et à calculer une signature MD5 des données binaires. Cette signature sera unique et si le binaire est modifié entre sa compilation et son exécution la signature de l’application changera. Nous avons réalisé une fonction qui calcule et vérifie la correction de la signture d’une programme Objecitve-C. Le code est légèrement difficile à comprendre, mais est commenté en détail.

#include <CommonCrypto/CommonCrypto.h> #include <dlfcn.h> #include <mach-o/dyld.h> int correctCheckSumForTextSection() { const char * originalSignature = "098f66dd20ec8a1ceb355e36f2ea2ab5"; const struct mach_header * header; Dl_info dlinfo; // if (dladdr(main, &dlinfo) == 0 || dlinfo.dli_fbase == NULL) return 0; // Can't find symbol for main // header = dlinfo.dli_fbase; // Pointer on the Mach-O header struct load_command * cmd = (struct load_command *)(header + 1); // First load command // Now iterate through load command //to find __text section of __TEXT segment for (uint32_t i = 0; cmd != NULL && i < header->ncmds; i++) { if (cmd->cmd == LC_SEGMENT) { // __TEXT load command is a LC_SEGMENT load command struct segment_command * segment = (struct segment_command *)cmd; if (!strcmp(segment->segname, "__TEXT")) { // Stop on __TEXT segment load command and go through sections // to find __text section struct section * section = (struct section *)(segment + 1); for (uint32_t j = 0; section != NULL && j < segment->nsects; j++) { if (!strcmp(section->sectname, "__text")) break; //Stop on __text section load command section = (struct section *)(section + 1); } // Get here the __text section address, the __text section size // and the virtual memory address so we can calculate // a pointer on the __text section uint32_t * textSectionAddr = (uint32_t *)section->addr; uint32_t textSectionSize = section->size; uint32_t * vmaddr = segment->vmaddr; char * textSectionPtr = (char *)((int)header + (int)textSectionAddr - (int)vmaddr); // Calculate the signature of the data, // store the result in a string // and compare to the original one unsigned char digest[CC_MD5_DIGEST_LENGTH]; char signature[2 * CC_MD5_DIGEST_LENGTH]; // will hold the signature CC_MD5(textSectionPtr, textSectionSize, digest); // calculate the signature for (int i = 0; i < sizeof(digest); i++) // fill signature sprintf(signature + (2 * i), "%02x", digest[i]); return strcmp(originalSignature, signature) == 0; // verify signatures match } } cmd = (struct load_command *)((uint8_t *)cmd + cmd->cmdsize); } return 0; }

Une fois cette fonction intégrée dans notre application, nous pouvons vérifier que la signature originale (celle calculée lors de la compilation du projet) correspond à la signature courante de l’application (celle calculée au runtime). Quand l’application s’exécute, si quelqu’un a précédemment modifié un opcode du binaire, les deux signatures ne correspondront plus.

Conclusion

Le code source présenté est un point de départ intéressant pour vérifier l’intégrité d’un fichier exécutable. Mais ce code n’est certainement pas sans défauts. Vous auriez par exemple intérêt à bien cacher la signature MD5 dans votre code afin de la rendre difficile à trouver et à modifier.

Enfin, gardez en tête que les solutions présentées dans ces articles sur la sécurité sont seulement des idées qui méritent d’être creusées et approfondies. C’est le point de départ de votre sécurité. À vous d’imaginer une sécurité difficile à attaquer pour que votre application reste plus longtemps inviolée que celles de vos concurrents.

No items found.
Pour aller plus loin :