< TOUTES LES NOTES

Le bug qui rendait vide chaque manifeste Android

4 min de lecture

Un décalage de seize octets dans le format binaire d'AndroidManifest.xml renvoyait zéro attribut sur toute application conforme. Aucune exception, aucun avertissement — juste un audit qui déclarait « propre » ce qu'il n'avait pas lu.

androidanalyse de malwareformat binairetests

Un APK ne transporte pas son manifeste en XML lisible. Le fichier AndroidManifest.xml à l'intérieur de l'archive est compilé au format de ressources binaire d'Android. Tout outil qui veut savoir ce qu'une application déclare — composants exportés, permissions, filtres d'intention, liens profonds, politique de trafic en clair — doit d'abord décoder ce format.

C'est une brique banale. C'est aussi là qu'un audit peut mentir sans jamais planter.

Le symptôme

Un analyseur maison renvoyait des applications sans le moindre attribut. Pas une erreur, pas une exception : un dictionnaire vide là où exported, debuggable et usesCleartextTraffic auraient dû se trouver.

Un audit de manifeste bâti là-dessus aurait rendu le même verdict sur n'importe quelle application : propre. Une application avec une activité exportée sans protection, une autre autorisant le trafic HTTP en clair vers un serveur bancaire — même conclusion rassurante, parce que le code ne lisait rien du tout.

Un outil muet n'est pas un feu vert. Un outil qui répond « rien à signaler » alors qu'il n'a rien examiné est pire qu'un outil absent : il produit de la confiance.

La cause

La disposition d'un nœud XML binaire est celle-ci :

ResXMLTree_node                      offset 0
    en-tête de chunk   type, headerSize, size      8 octets
    lineNumber                                     4 octets
    comment                                        4 octets
ResXMLTree_attrExt                   offset 16
    ns, name                                       8 octets
    attributeStart, attributeSize, ...            12 octets
attributs                            offset 16 + attributeStart

attributeStart est documenté dans le ResourceTypes.h d'AOSP comme un « décalage en octets depuis le début de cette structure ». Toute la question tient dans « cette structure » : il s'agit de ResXMLTree_attrExt, pas du chunk. Les manifestes réels y écrivent 20, donc les attributs commencent à l'offset 36.

Le code mesurait depuis le début du chunk et lisait à l'offset 20. Seize octets trop tôt. À cet endroit se trouvent des octets parfaitement valides — ceux de l'en-tête — donc rien ne signalait l'erreur. Le compteur d'attributs lu là valait zéro, et une boucle qui tourne zéro fois ne lève aucune exception.

Pourquoi les tests l'ont laissé passer

C'est la partie intéressante, et la seule qui se généralise.

La suite de tests reposait sur un aller-retour : un encodeur écrivait un manifeste, l'analyseur le relisait, on comparait. Le test passait.

Il passait parce que l'encodeur avait été écrit par la même personne, le même jour, avec la même lecture erronée de la spécification. Il écrivait 36 là où l'analyseur lisait 36. Les deux se trompaient dans le même sens, donc ils étaient d'accord.

Un test aller-retour ne prouve jamais que le code est conforme à un format. Il prouve que le code est cohérent avec lui-même. Sur un format inventé maison, c'est suffisant. Sur un format défini par quelqu'un d'autre — AOSP, une RFC, une norme bancaire — cela ne vaut rien.

Ce qui a remplacé ça

Trois couches, dans l'ordre croissant de ce qu'elles démontrent réellement :

CoucheCe qu'elle prouve
Aller-retour avec l'encodeur embarquéL'analyseur est d'accord avec nous. Peu coûteux, le plus faible.
Vecteurs binaires exactsL'analyseur est d'accord avec le format. Les offsets sont écrits en clair, avec la structure AOSP en commentaire, pour qu'un relecteur puisse les vérifier à la lecture.
Entrées hostiles et fuzzingIl survit à des données écrites pour le casser.

La couche du milieu est celle qui manquait. Un vecteur binaire exact est un tableau d'octets écrit à la main, dont on sait ce qu'il doit produire parce qu'on l'a dérivé de la spécification, pas de son propre code.

S'y ajoute un test qui vérifie qu'un encodeur écrivant le mauvais offset produit une sortie visiblement fausse. Autrement dit : la couche aller-retour ne peut plus jamais certifier discrètement une erreur partagée.

Le manifeste est une donnée hostile

Une dernière chose, qui vaut au-delà de ce bug. Un manifeste soumis à un analyste est fréquemment malformé volontairement : compteurs de chaînes qui débordent, offsets pointant hors du tampon, tailles de chunk à zéro qui font tourner une boucle naïve indéfiniment.

Les analyseurs écrits pour des entrées bien formées plantent, se figent ou épuisent la mémoire précisément sur les échantillons qu'on veut le plus lire. Chaque longueur et chaque offset extraits du fichier doivent être validés contre la taille réelle du tampon avant d'être utilisés — et ce contrat se vérifie par fuzzing, pas à l'œil.

Le code est public : github.com/rolandsanou/axmlite. Il lit le XML binaire Android avec la seule bibliothèque standard de Python, pour la machine de triage isolée où pip install n'est pas une option.

OP_CENTER
STATUT : CHIFFRÉ
SYS_BOOT.exeDOSSIER.exeREGISTRY.exeNOTES.logSERVICES.exeFORMATIONS.dirCV_DATA.dirSECURE_CONNECT