Niji utilise des cookies techniques pour le bon fonctionnement du site et des cookies de mesure d’audience pour analyser le trafic. Des cookies sont également déposés par des tiers pour vous permettre de partager des contenus sur les réseaux sociaux. Si les cookies techniques ne nécessitent pas votre consentement, vous pouvez choisir d’accepter, de personnaliser ou de refuser les autres cookies. Votre choix est conservé pendant un an, il est modifiable à tout moment via le lien « Gérer vos cookies ». L’absence de choix sera considérée comme un refus.

Pour en savoir plus sur ces cookies, consultez notre politique de gestion des données via le lien « Vos données ».

11 octobre 2021

Sécurité applicative mobile : recommandations et bonnes pratiques

Sécurité applicative mobile : recommandations et bonnes pratiques

Ce contenu propose une synthèse cadrée et rédigée par Marc Peysale, Lead Mobile chez Niji.

 

Contexte

La sécurité applicative est une partie du développement d’une application qu’il ne faut pas négliger, pour éviter des fuites de données, l’usurpation de comptes clients, ou l’affichage de données faussées sur les applications.

L’attention portée à ce domaine doit concerner à la fois les équipes côté client et serveur, et la plupart des bonnes pratiques consistent en la création d’un lien fort entre le client et les API qu’il va consommer.

Différents types d’attaques existent, et il est important de s’en prémunir le plus efficacement possible, dans le but de ralentir ou bloquer les tentatives d’attaque. En France, la CNIL prévoit des sanctions en cas de défaut de sécurisation d’un système informatique pouvant aller jusqu’à 10 millions d’Euros, ou 2 % du CA de l’entreprise, sans parler des conséquences sur l’image de marque d’une entreprise responsable de la fuite des données de ses clients.

Cet article présente premièrement quelques attaques classiques, et des moyens pour s’en prémunir. Il ne s’agit ni d’une liste exhaustive, ni d’un guide à appliquer à la lettre afin de se sentir protégé, les attaques évoluent rapidement, et les techniques de protection doivent donc être mises à jour. Ensuite, il rappelle quelques bonnes pratiques à appliquer dans tous les projets de manière à limiter la surface d’attaque et à prévoir des coupe-circuit en cas d’attaque avérée.

Son but n’est pas de présenter une vision alarmiste de l’ensemble des possibilités pour un attaquant de casser le fonctionnement nominal d’une application, mais de présenter quelques techniques simples à mettre en place pour éviter des situations de vol de données ou de pertes de service. Bien évidemment, la présence d’une faille de sécurité ne signifie pas qu’elle sera exploitée. L’immense majorité des apps présentes sur le marché ne sera jamais attaquée, les cibles privilégiées étant les apps à forte audience et pour lesquelles des outils d’analyse automatisée montrent des signes de faiblesse, par exemple, MobSF est un outil open-source qui permet d’analyser des vulnérabilités classiques sur une app. Une app présentant des faiblesses facilement exploitables sera ainsi plus ciblée par des attaquants.

Étant moi-même développeur mobile natif, iOS et Android, les exemples donnés dans le reste de l’article sont basés sur des développements natifs, cependant, les concepts présentés s’appliquent également sur des technologies cross-platform telles que React-Native et Flutter.

 

Description de quelques attaques répandues

1. Man In the middle (mitm)

Un vecteur d’attaques classique est celui de l’homme du milieu (man-in-the-middle en anglais).

 

Description de l’attaque

Cette attaque consiste pour l’attaquant à se placer entre le client et le serveur auquel il se connecte afin d’écouter/modifier les communications.

Dans un contexte classique, les appels entre le client et le backend se font en HTTPS, ce qui implique que les communications sont chiffrées par un certificat SSL, le client envoie des données chiffrées via la clé publique du certificat, et seul le backend peut les décoder via sa clé privée. Ce chiffrage asymétrique ne permet pas à une personne écoutant les paquets transitant sur le réseau d’en décoder le contenu.

L’attaque de type mitm consiste à placer l’attaquant entre le client et le serveur, via l’installation d’un proxy par exemple, il va alors intercepter les requêtes d’échange du certificat SSL entre le client et le backend, et remplacer le certificat SSL du backend par son propre certificat. Par la suite, pour chaque requête, le client va signer sa requête via la clé publique de l’attaquant (et non plus du backend) qui pourra aisément déchiffrer toutes les requêtes, puis les transmettre au backend, et resigner les réponses du backend avant de les transmettre au client. Tout le trafic est alors connu de l’attaquant, et peut être modifié sans qu’aucune des parties ne puisse s’en apercevoir.

 

 

Conséquences possibles

Une personne mal intentionnée peut alors falsifier le contenu affiché par l’application et afficher par exemple un faux reçu dans le cas d’une application dédiée au click and collect, ou tout simplement écouter l’ensemble du trafic émis par le client et récupérer des mots de passe ou des jetons d’accès.

 

Remédiation

La solution pour se prémunir de ces attaques est de créer un couplage entre le backend et les clients lourds qui le consomment en embarquant dans les apps la clé publique du certificat SSL utilisé par le backend. Ce mécanisme s’appelle le certificate-pinning.

La plupart des librairies d’appels réseau le proposent (Alamofire pour iOS, OKHTTP pour Android…).

En cas de tentative d’attaque, le client détecte que le certificat SSL falsifié par l’attaquant ne correspond pas à celui qui est embarqué dans le build et la requête n’est pas jouée.

Sa mise en place nécessite d’anticiper les renouvellements de certificats SSL afin de publier de nouveaux builds compatibles : un mois avant l’expiration du certificat SSL, on publie une mise à jour comprenant deux clés publiques : l’actuelle et la future. La publication suivant la bascule de certificat peut revenir sur un seul certificat embarqué.

 

2. Décompilation

Description de l’attaque

Les applications mobiles natives reposent sur des langages compilés (Java et Kotlin génèrent du Bytecode après compilation, qui sera exécuté dans la JVM Android, et Objective-C et Swift génèrent des binaires directement). Le binaire est ensuite intégré dans un paquet (APK Android, IPA iOS) qui contient l’ensemble des ressources nécessaires à l’application : images, fichiers embarqués, Manifest Android, Info.plist sur iOS). Ces paquets sont des simples archives au format Zip, et il est très facile d’en extraire le contenu. Aucun secret (clé d’API, mot de passe de chiffrement…) ne doit donc être stocké dans ces fichiers.

Quant aux binaires, le Bytecode est très facile à décompiler, Android Studio le propose directement dans l’IDE pour accéder à l’implémentation des librairies intégrées dans le projet. L’Objective-C et le Swift sont plus complexes à décompiler, mais de nombreux outils existent.

 

Conséquences possibles

La décompilation d’une application permet à un attaquant d’obtenir plusieurs informations : il peut obtenir les chaînes de caractères embarquées en dur dans le code de l’application : beaucoup de wording, mais également des constantes, des clés d’API, des mots de passe d’accès à certains services etc… Dans le cas d’applications avec des traitements complexes côté client, la décompilation peut également engendrer un vol de propriété intellectuelle en rendant lisibles des algorithmes potentiellement sensibles ou brevetés.

 

Remédiation

Il est impossible d’empêcher une application d’être décompilée. On peut cependant compliquer la tâche en appliquant des techniques d’obfuscation de code, c’est-à-dire en rendant le code compilé difficile à lire une fois décompilé. Plusieurs niveaux d’obfuscation existent, on peut remplacer les variables/méthodes/classes par des lettres à la manière d’un script de minification Javascript pour retirer toute aide sémantique à la compréhension du code par un humain. On peut également ajouter des instructions inutiles qui nuisent à la rétro-ingénierie des algorithmes implémentés.

L’obfuscation n’a pas d’effet sur les chaînes de caractères statiques présentes dans le code. Pour les protéger on peut mettre un place une méthode de chiffrement symétrique ou asymétrique : on embarque des valeurs encodées à la compilation qu’on vient déchiffrer au runtime.

Sur Android, un outil appelé R8 permet d’appliquer un premier niveau d’obfuscation au code. Sur iOS, cette technique est plus compliquée : Apple impose que le code soit déchiffrable dans l’ensemble, hormis pour des raisons précises (algorithme breveté, ou partie sensible du code). En cas de détection d’obfuscation par les équipes d’App Review, la soumission d’une app sur l’App Store peut alors être refusée. Si le motif est légitime, il ne faut pas hésiter à faire appel de la décision, mais seules les parties sensibles doivent être obfusquées, par exemple le chiffrement des secrets de l’app.

 

3. SDK tiers corrompus

L’intégration de librairies tierces dans une application permet de répondre à plusieurs besoins : accélérer les développements en utilisant des briques existantes ou intégrer des fonctionnalités développées par des partenaires (SDK Marketing, Analytics, outils développeurs comme Firebase de Google etc…). Cependant, inclure dans son projet du code que l’on ne maîtrise pas peut présenter des risques. Il est important de vérifier la source de ces librairies, et de les intégrer comme recommandé par leurs auteurs, en utilisant des sources sécurisées, et non des binaires trouvés sur le Web qui peuvent contenir du code malveillant. Le protocole utilisé est également à vérifier : la plupart des gestionnaires de dépendances reposent sur l’hébergement du code via Git sur des dépôts open source : l’utilisation de Git implique derrière une communication sécurisée via SSH, cependant certains SDK reposent sur des binaires dont le code source n’est pas publié. Le transfert de ces binaires peut se faire via des connexions dangereuses. Felix Krause, le créateur des outils Fastlane bien connus des développeurs iOS, a écrit un article complet sur le sujet des SDK tiers, et a également proposé des MR sur le projet Cocoapods afin d’afficher un Warning lors de l’intégration de ces SDK potentiellement dangereux.

Trusting third party SDKs · Felix Krause (krausefx.com)

 

Bonnes pratiques

Après ce descriptif de quelques attaques, je vais désormais présenter plusieurs bonnes pratiques à appliquer lors du développement de projets mobiles et de leurs API correspondantes.

 

1. Cloisonnement

Le cloisonnement d’une API est important pour éviter qu’un utilisateur authentifié ne puisse accéder aux données d’un autre. Par exemple, imaginons un service permettant de consulter les données d’un compte utilisateur. Ce service propose un point d’entrée d’authentification qui forge et retourne un jeton d’accès en cas d’authentification réussie, et un point d’entrée de consultation qui retourne les infos personnelles d’un utilisateur lorsqu’il est connecté.

Les deux points d’entrée ont la signature suivante : POST /authentication : nécessite des identifiants en entrée, et retourne un jeton signé comprenant l’identifiant utilisateur si les identifiants sont valides.

GET /user/:id, nécessitant un jeton d’accès pour s’assurer que le consommateur est bien authentifié.

Si le service ne vérifie pas que l’identifiant contenu dans le jeton correspond bien à celui passé dans la requête, alors n’importe quel utilisateur authentifié peut récupérer les données d’autres utilisateurs en changeant l’identifiant transmis dans la requête. Un défaut de cloisonnement peut donc être utilisé par un attaquant afin d’aspirer le contenu d’une base utilisateurs, ou bien pour effecteur des modifications sur d’autres utilisateurs que lui-même. Si les identifiants sont auto-incrémentés, il est très facile de les deviner : il est donc toujours recommandé de n’exposer que des identifiants uniques type UUID au sens de la RFC 4122.

 

2. Désactivation de versions d’apps obsolètes

Au cours de la vie d’une application, de nombreuses versions se succèderont, apportant des nouvelles fonctionnalités, des correctifs de bugs, ou des réécritures complètes. Bien qu’iOS et Android proposent tous les deux des process de mise à jour automatique des apps installées, certains utilisateurs désactivent cette fonctionnalité, ou sont dans des situations où l’installation ne peut se faire automatiquement.

Dans ces cas, il est intéressant que l’app puisse proposer son propre mécanisme invitant ses utilisateurs à se mettre à jour, pour ne pas conserver une app obsolète, et bénéficier des dernières fonctionnalités, mais surtout des derniers correctifs de bugs, et des dernières améliorations en termes de sécurité.

La mise à jour forcée d’une app est très pratique pour les développeurs, mais doit être réfléchie en termes d’UX pour l’utilisateur. En effet, rien de plus frustrant lorsque l’on lance une app que d’être bloqué dès le lancement. L’idéal est de ne forcer la mise à jour qu’en cas de nécessité, et de laisser la possibilité à l’utilisateur de lancer l’app pendant quelques jours avant de le bloquer s’il n’est toujours pas à jour.

 

3. Jetons d’accès, Clés API, signature des requêtes

Un bon moyen d’assurer la sécurité d’un SI est de créer un couplage entre les consommateurs autorisés et les API consommées. Ce couplage peut prendre plusieurs formes : la transmission d’une clé d’API dans l’ensemble des requêtes, ce qui permet d’éliminer les appelants ne disposant pas de clé : cette clé d’API permet également de monitorer les différents systèmes appelants pour mettre en place une facturation ou des quotas d’accès.

Cette technique n’est cependant pas très robuste face à une décompilation : en effet, la clé d’API est facilement visible parmi les chaînes de caractères statiques de l’app. Pour complexifier un peu le mécanisme, on peut utiliser cette clé pour forger une signature de chaque requête : par exemple générer un hash comprenant : URL d’appel, timestamp de l’appel, paramètres GET, hash du body de la requête, le tout chiffré avec une clé partagée entre le client et le serveur. A la réception de chaque requête, le serveur calcule la même signature, et si elle diffère, la requête est ignorée. Ici, le couplage client/serveur est donc au niveau du secret partagé, ainsi que de l’algorithme de calcul de la signature : il faut donc décompiler l’app et comprendre cet algorithme qui peut être obfusqué, ce qui rend la tâche plus complexe.

 

4. Versions d’OS

Chaque mise à jour d’OS contient son lot de correctifs de sécurité propagés par Apple et Google. En rendant une app compatible avec des versions obsolètes des systèmes d’exploitation mobiles, on s’expose également à des failles supplémentaires. Il convient donc de trouver un juste milieu entre le parc d’utilisateurs à couvrir et la nécessité de conserver un niveau satisfaisant de sécurité. Au démarrage du développement d’une nouvelle application, ou à la sortie d’une refonte, mieux vaut ne pas couvrir trop de versions d’OS : au cours de la vie de l’application, les nouvelles versions d’OS vont se succéder (environ 1 par an pour iOS et Android), et le parc de versions à couvrir se verra élargi : le fait de retirer des versions d’OS compatibles en cours de vie d’une appli crée de la frustration auprès des utilisateurs. Mieux vaut donc ne supporter que le strict minimum : par exemple, 1 à 2 versions d’iOS, et 3 à 4 versions d’Android. A l’heure de l’écriture ce cet article, supporter Android 7 et versions supérieures permet de cibler un parc de plus de 71 % des appareils. Supporter iOS 14 et 15 (la dernière en date) permet de cibler plus de 80 % des appareils du parc.

 

5. Chiffrement des échanges

En plus de l’intégration d’un mécanisme de certificate-pinning permettant de se prémunir contre les attaques man-in-the-middle, il est assez facile de mettre en place un chiffrement des appels WS depuis les clients via un mécanisme de chiffrement asymétrique (type RSA) à clé privée et clé publique.

Le serveur peut générer un couple de clés privée et publique, et transmettre la clé publique aux clients qui l’utiliseront pour chiffrer le corps des requêtes HTTP. Le serveur peut alors déchiffrer le contenu via sa clé privée. En cas de compromission de cette clé, un nouveau couple de clés peut être généré.

Attention cependant, ce chiffrement est facile à implémenter, mais peut être gourmand en ressources CPU, notamment sur des requêtes dont le corps est volumineux (échanges de fichiers média par exemple). Dans ce cas, il peut être intéressant de ne pas chiffrer certaines données peu sensibles pour limiter la charge de calcul.

 

Conclusion

Aucune solution n’est infaillible, et la sécurité applicative est un domaine qui évolue à une vitesse fulgurante. L’idée de cet article est de présente quelques bonnes pratiques qui permettent de ne pas être trop vulnérable face à des attaques opportunistes, et de permettre de communiquer aux clients en cas d’attaque, de limiter les possibilités d’intrusion et de vol de données, voire de couper le service en cas d’attaque importante.

Des tests d’intrusion (penetration tests) doivent être réalisés régulièrement afin de s’assurer de la perméabilité du SI et des briques logicielles. OWASP met à disposition des équipes de développement un référentiel de bonnes pratiques : Mobile Application Security Verification Standard (MASVS) à respecter dans la mesure du possible, ainsi qu’une checklist qui permet à contrôler régulièrement.

 

https://github.com/OWASP/owasp-mstg/tree/master/Checklists

https://github.com/OWASP/owasp-masvs

 

Ces deux outils proposent deux niveaux de sécurité : L1 et L2, le premier étant destiné à l’ensemble des applications, le second plus adapté à des applications manipulant des données critiques (secteur bancaire notamment), ainsi qu’un niveau supplémentaire traduisant la résilience face à des décompilations d’app, et permettant de mesurer le niveau de durcissement des configurations (Conf Hardening).

Cet article n’est évidemment pas exhaustif, et laisse de côté des techniques importantes telles que la mise à jour régulière des dépendances afin de bénéficier des derniers correctifs et améliorations, le chiffrement des données stockées en local (à la manière du chiffrement des échanges avec les backends présenté dans les bonnes pratiques), ainsi que plusieurs techniques spécifiques à des catégories d’apps : le fait de vider le presse-papier en sortie d’app lorsque l’on manipule des mots de passe ou des OTP, la détection de claviers non officiels pouvant faire office de key-logger, la détection de téléphones rootés/jailbreakés… Ces différentes pistes doivent être explorées le cas échéant lorsque les spécifiés de l’app à développer l’exigent.

Contact Relations Presse
Frédéric PAYEN
Directeur Marketing et Communication
presse@niji.fr