Une application bancaire ou de monnaie électronique concentre plus de risque par utilisateur que presque tout autre logiciel grand public. En zone UEMOA, elle est souvent le seul accès au compte : pas d'agence à proximité, pas de site web pratique, pas de carte. Le téléphone est la banque.
Auditer ces applications depuis l'extérieur, sans code source ni environnement de test, c'est de l'analyse statique d'APK. C'est utile, c'est rapide, et c'est redoutablement facile à faire dire n'importe quoi.
Cette note décrit la méthode que j'applique et, surtout, les endroits où un audit naïf produit un faux verdict. Elle ne contient aucun constat sur une application identifiable : les résultats d'audit appartiennent aux organisations auditées.
Les quatre axes
Le découpage suit le MASVS de l'OWASP, restreint à ce qu'un APK seul permet d'établir.
1. Stockage. Ce que l'application écrit sur l'appareil et qui peut le relire. android:allowBackup, les répertoires exposés, la présence de bases non chiffrées, les fichiers de préférences. Le point d'attention : allowBackup vaut true par défaut. Son absence du manifeste n'est pas un silence neutre, c'est une permission accordée.
2. Réseau. Le trafic en clair, la configuration TLS, l'épinglage de certificat. android:usesCleartextTraffic, le networkSecurityConfig, les domaines déclarés en exception.
3. Fuite. Ce qui sort par des canaux non prévus : journaux, presse-papiers, captures d'écran, composants qui exposent des données à d'autres applications.
4. Surface d'attaque. Les composants exportés — activités, services, receveurs, fournisseurs de contenu — et les permissions demandées, en particulier celles qui n'ont aucun rapport avec la fonction du produit.
Jusqu'ici, rien que de très classique. C'est la suite qui compte.
Piège 1 : l'épinglage de certificat ne se lit pas dans le manifeste
Un vérificateur naïf cherche network_security_config.xml et une balise <pin-set>. S'il ne trouve rien, il conclut « pas d'épinglage ».
C'est faux dans les deux sens.
La majorité des applications Android sérieuses épinglent dans le code, via le CertificatePinner d'OkHttp ou un TrustManager maison. Rien de tout cela n'apparaît dans le manifeste. Une application parfaitement épinglée sera déclarée vulnérable.
L'inverse est tout aussi trompeur. Trouver la chaîne CertificatePinner dans les chaînes du DEX ne prouve rien : elle peut venir d'une bibliothèque tierce embarquée et jamais instanciée, viser un domaine de télémétrie et pas l'API bancaire, ou être du code mort laissé par une version antérieure.
La seule question qui compte — le trafic vers l'API de production est-il épinglé ? — ne se répond pas statiquement. Elle se répond par interception, sur un appareil de test, sous mandat.
Ce que l'analyse statique peut honnêtement produire ici : « épinglage déclaratif absent ; présence de primitives d'épinglage dans le code ; vérification dynamique requise ». Trois états, pas deux.
Piège 2 : usesCleartextTraffic dépend du targetSdk et se fait écraser
L'attribut android:usesCleartextTraffic ressemble à une réponse binaire. Il n'en est pas une.
- En dessous de
targetSdkVersion 28, sa valeur par défaut esttrue. Son absence
autorise donc le HTTP en clair.
- À partir de 28, la valeur par défaut est
false. La même absence signifie
l'inverse.
- Et dès qu'un
networkSecurityConfigest déclaré, l'attribut est ignoré : c'est
le fichier de configuration qui tranche, domaine par domaine.
Un vérificateur qui lit l'attribut sans lire le targetSdk ni suivre le networkSecurityConfig se trompe dans les deux directions, et se trompe en silence. Il faut résoudre les trois ensemble, et signaler explicitement les exceptions par domaine — c'est très souvent là que se trouve le vrai problème : un cleartextTrafficPermitted="true" pour un sous-domaine oublié.
Piège 3 : exported a une valeur par défaut qui dépend du contenu
Le cas le plus coûteux, parce qu'il touche à la surface d'attaque.
android:exported n'est pas simplement vrai ou faux. Historiquement, un composant sans l'attribut est exporté si et seulement si il déclare un <intent-filter>. Depuis targetSdkVersion 31, l'attribut est obligatoire sur tout composant filtré — mais un parc applicatif réel contient encore des versions ciblant plus bas.
Conséquence : un outil qui compte les composants portant littéralement exported="true" rate tous les composants implicitement exportés. Il rendra un verdict rassurant sur une application dont une activité sensible est atteignable par n'importe quelle autre application installée.
La règle correcte se calcule :
exporté = si attribut présent → sa valeur
sinon → présence d'un <intent-filter>
C'est trois lignes. Leur absence transforme l'audit en décoration.
Piège 4 : FLAG_SECURE ne vit pas dans le manifeste non plus
Bloquer les captures d'écran sur un écran de solde ou de code à usage unique se fait à l'exécution, fenêtre par fenêtre :
getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
Chercher FLAG_SECURE dans les chaînes du DEX dit qu'une constante existe quelque part. Cela ne dit ni quelles activités sont protégées, ni si l'écran qui affiche le solde en fait partie. Une application peut protéger son écran de connexion et laisser l'historique des transactions en clair devant le gestionnaire de tâches.
Même conclusion que pour l'épinglage : l'analyse statique oriente la vérification dynamique, elle ne la remplace pas.
Le principe qui tient tout
Ces quatre pièges ont une structure commune. Dans chaque cas, l'outil peut produire « rien trouvé » pour deux raisons totalement différentes :
- la propriété est correctement implémentée, ou l'absence est bénigne ;
- l'outil n'était pas capable de la voir.
Un audit qui écrit la même chose dans les deux cas ne mesure rien. Il fabrique de la confiance, et une confiance fabriquée sur une application bancaire coûte plus cher que pas d'audit du tout — parce qu'elle clôt le sujet.
D'où la règle que j'applique sans exception : trois états, jamais deux.
| État | Sens |
|---|---|
CONFORME | vérifié, et le résultat est bon |
NON CONFORME | vérifié, et le résultat est mauvais |
NON VÉRIFIÉ | hors de portée de cette méthode — dit explicitement |
Un rapport où la troisième colonne est vide est un rapport qui ment par omission. Et le nombre de lignes en NON VÉRIFIÉ est en soi une information : il dit au commanditaire ce qu'il n'a pas encore acheté.
Ce qu'un APK seul permet réellement d'établir
Pour finir, la liste honnête. Sans appareil de test ni mandat d'interception, un APK donne des conclusions fermes sur :
android:debuggableen production — constat sec, sans ambiguïtéallowBackupet l'exposition des données à la sauvegarde système- les permissions demandées, et celles qui n'ont aucun rapport avec la fonction
- les composants exportés, à condition de calculer la valeur par défaut
- les exceptions de trafic en clair déclarées par domaine
- les bibliothèques tierces embarquées et leurs versions
Et il ne permet pas de conclure sur l'épinglage effectif, la protection réelle des écrans sensibles, la solidité du stockage chiffré, ni le comportement à l'exécution.
Cette frontière n'est pas une faiblesse de la méthode. C'est la méthode.
L'analyseur de manifeste utilisé pour ce travail est public : github.com/rolandsanou/axmlite. Il lit le XML binaire d'Android avec la seule bibliothèque standard, et il calcule exported comme décrit plus haut — une histoire de seize octets explique pourquoi je ne fais plus confiance à un analyseur que je n'ai pas testé contre le format lui-même.