Article
Création de logiciels
17.6.2016
Perturbation continuum temporel
34 minutes
Temps d'un café
Décomposer Pokémon Go
Décomposer Pokémon Go
Remy Stère
Note : Ce contenu a été créé avant que Fabernovel ne fasse partie du groupe EY, le 5 juillet 2022.

Pour une application qui n’était disponible que dans 3 pays à sa sortie (Etats-Unis, Australie et Nouvelle-Zélance), elle a quand même réussi à battre des grands noms, comme Twitter en nombre d’utilisateurs actifs ou Facebook en temps passé par jour. C’est devenu le meilleur lancement d’un jeu mobile aux Etats-Unis depuis la mise en place des app stores, en prenant la place de Candy Crush Saga. L’application a prouvé son intérêt, pas seulement pour ses développeurs (qui toucheraient 1 million de dollars d’achat in-app par jour), mais aussi pour les petits commerces qui ont remarqué une augmentation du trafic et commencé à réfléchir sur comment profiter de ce phénomène, et enfin pour Nintendo, qui a vu son cours s’envoler de 90% en une semaine.

Comme le jeu est sorti depuis peu, il est aussi la source de nombreuses rumeurs et légendes urbaines, qui sont autant de motivations à regarder ce qui se passe vraiment à l’intérieur. Cet article se concentrera pour l’instant sur comment extraire des informations sur une application Android, à partir du reverse-engineering de son code et de ses échanges réseaux.

Reverse Engineering de l’application

La première étape pour toute tentative de reverse-engineering est évidemment de mettre la main sur un exemplaire de l’application (un fichier .apk pour Android). Il semblerait que cette tâche soit relativement simple pour Pokémon Go (étant donné le nombre d’installations constatées dans des pays où le jeu n’avait pas encore été officiellement publié), donc nous ne discuterons pas ici de comment se le procurer.

Par contre, installer un apk provenant de source inconnue est un énorme risque de sécurité. Le Play Store exécute un certain nombre d’analyses sur les applications qu’il fournit, dans l’objectif de minimiser ce risque, et le contourner vous rend beaucoup plus vulnérable. Mais dans le cas du reverse-engineering, tomber sur un apk malicieux serait une très bonne opportunité pour apprendre comment ils fonctionnent. Il faut juste que vous vous assuriez d’avoir compris tout le contenu de l’apk que vous avez récupéré avant de l’installer sur un téléphone.

Dans notre cas, nous avons travaillé à partir d’un apk ayant la version 0.29.0, datant du 7 Juillet.

Ouvrons une autre parenthèse pour discuter de ce que nous ne serons pas capables de voir avec du reverse-engineering. Nous sommes restreints à ce qui est présent à l’intérieur de l’application, donc il y aura des parties du projet qui resteront hors d’atteinte. Voici quelques exemples :

  • tout ce qui a trait à la phase de build : nous avons seulement le résultat du build, pas son déroulement ou ses étapes
  • tout ce qui a trait aux tests et à l’intégration continue
  • les autres variantes de l’application (par exemple, versions de développement) : quand vous écrivez une application, certaines fonctionnalités spécifiques au développement ne sont pas visibles en production, et comme nous travaillons sur une application venant du store, ces features ne devraient pas apparaître
  • le code source du back-end. C’est probablement une évidence, mais autant le préciser pour tous les joueurs qui auraient aimé en apprendre plus sur les algorithmes qui décident, par exemple, quels pokémons apparaissent à quel endroit. Ceci est une décision qui a lieu sur les serveurs. Nous pouvons seulement découvrir comment communiquer avec ces serveurs, pas leurs processus internes.

A l’intérieur de l’apk

Maintenant, regardons ce qui se trouve dans un apk. En fait, un apk est juste une archive zip dont le contenu ressemble à ça :

Voici une brève description de ce à quoi chaque fichier (en vert) ou dossier (en rouge) sert:

  • le Manifest est juste le Manifest Android de l’application. Il sert de fiche d’identité et fournit le nom, l’icône, la version, les permissions, les spécifications minimales et les différents composants de l’application. Il sert aussi de point d’entrée dans l’application et est lu par le système à l’installation ou à chaque mise à jour.
  • classes.dex est le fichier dans lequel le code Java est compilé. Techniquement, vous pouvez en avoir plusieurs (le processus s’appelle Multidex), mais cela n’a aucune importance pour nous.
  • lib est un dossier contenant les librairies natives.
  • res et assets sont des correspondances directes des ressources et assets du projet.
  • resources.arsc est un fichier spécifique à Android, qui fait le lien entre le code et les ressources (autrement dit, il sert au code à manipuler les ressources). C’est le résultat de la compilation du fichier R.java.
  • META-INF est un dossier contenant des méta-données et n’a aucun intérêt pour nous.

Donc voila ce que vous vous attendez à trouver quand vous décompressez un apk. Le premier fichier que nous allons étudier est le classes.dex.

Extraction du code

L’extension dex correspond à Dalvik Executable (Dalvik étant le nom de l’ancienne machine virtuelle sur laquelle Android reposait, la nouvelle étant ART, pour Android Runtime, mais l’extension de fichier n’a pas changé). C’est un format de fichier spécifique à Android, qui n’est pas très accessible. Il y a deux stratégies principales pour interagir avec des fichiers dex : la première consiste à convertir son contenu en un bytecode plus lisible appelé smali (qui est un langage créé explicitement pour ce cas d’usage) et la seconde est de convertir ce fichier en un autre plus traditionnel dans l’univers Java.

Nous allons utiliser la seconde stratégie et utiliser dex2jar pour, comme son nom l’indique, convertir le dex en un jar (un jar est juste une archive zip qui contient du bytecode Java dans un ensemble de fichiers .class). Maintenant que nous avons un jar, nous avons beaucoup plus d’outils à notre disposition. Le prochain outil dont nous allons nous servir est un décompilateur, capable de convertir le bytecode contenu dans les .class en un code source Java. Il y a de nombreux compilateurs disponibles, avec chacun leurs forces et leurs faiblesses, mais qui restent mineures dans notre cas. Nous allons utiliser Jadx, mais vous pouvez utiliser celui que vous voulez. Vous pouvez même trouver des décompilateurs en ligne.

Nous devrions maintenant avoir quasiment tout le code lisible par n’importe quel développeur Java (qui sont beaucoup plus courants que des développeurs Smali). “Quasiment tout”, parce que les décompilateurs ont leurs limites et ne sont pas forcément capables de décompiler tout le code qu’on leur donne. Il y a une discussion fascinante sur Procyon (un autre décompilateur Java) sur les raisons pour lesquelles le bytecode converti depuis un dex par dex2jar peut parfois se montrer difficile à décompiler.

Une remarque importante : ce code n’est PAS le code que les développeurs de l’application ont écrit. Une bonne analogie serait d’utiliser Google Translate pour traduire un texte d’anglais en français, puis de français en anglais. Vous obtiendriez un nouveau texte (dans un anglais à la grammaire probablement douteuse) qui contiendrait le même nombre de phrases, et les mêmes motifs mais ne serait quand même pas le texte originel. En effet, quand le texte est traduit en français, certains choix seront faits quant à la meilleure manière de traduire un mot ou une tournure de phrase, et un autre ensemble de choix seront faits quand le texte sera de nouveau traduit en français. Et comme la deuxième traduction est faite uniquement à partir du français, il faudrait un miracle pour que les choix soient exactement les mêmes à chaque endroit. Pour du code, le raisonnement est le même : nous obtenons à la fin un code qui se comporte de la même manière, mais n’est pas le code écrit par le développeur à la base pour autant. Par exemple, parmi les morceaux que nous allons définitivement perdre se trouvent les commentaires ou les noms des méthodes, variables et champs, qui ne sont pas présents dans le bytecode et ne peuvent donc pas être récupérés.

Reprenons maintenant le code décompilé. Le premier élément qu’on peut remarquer est qu’il ne semble pas y avoir d’obfuscation. Tous les noms des dossiers ont été conservés, ce qui nous permet de voir quelles librairies sont utilisées. Voici une liste des librairies que nous avons pu identifier :

Maintenant, si vous êtes un développeur Android, quelques points peuvent vous paraître étranges. Deux parseurs JSON ? Programmation réactive, injection de dépendance et event bus en même temps ? C’est généralement un signe de dépendances transitives : vous déclarez juste deux-trois dépendances, et elles-mêmes déclarent leurs propres dépendances, etc. Et parfois, vous vous retrouvez avec un nombre de dépendances beaucoup plus important que ce à quoi vous vous attendiez. Vous pouvez en apprendre plus sur les dépendances transitives et comment les analyser dans un de nos articles précédents.

Après avoir nettoyé toutes ces dépendances, nous nous retrouvons avec cette liste de dépendances directes :

  • Gson
  • Crittercism
  • Upsight
  • Admob/firebase-ads
  • Google VR SDK, Unity et associés
  • qui est tout de suite une liste beaucoup plus raisonnable.

En fait, la plupart des imports que nous voyions étaient dus à Upsight, qui a un nombre impressionnant de dépendances (la liste, avec leur nombre de méthodes : RxAndroid (4k), Dagger (~200), Commons IO (1k), Jackson (10k), Otto (~50), various Play Services (12k)), en plus de leur propre code (3k méthodes):

+--- com.upsight.android:all:4.1.3 | +--- io.reactivex:rxandroid:1.0.1 | | \--- io.reactivex:rxjava:1.0.13 | +--- com.upsight.android:analytics:4.1.3 | | +--- io.reactivex:rxandroid:1.0.1 (*) | | +--- com.google.dagger:dagger:2.0.2 | | | \--- javax.inject:javax.inject:1 | | +--- com.upsight.android:core:4.1.3 | | | +--- io.reactivex:rxandroid:1.0.1 (*) | | | +--- com.google.dagger:dagger:2.0.2 (*) | | | +--- commons-io:commons-io:2.4 | | | +--- com.fasterxml.jackson.core:jackson-databind:2.6.3 | | | | +--- com.fasterxml.jackson.core:jackson-annotations:2.6.0 | | | | \--- com.fasterxml.jackson.core:jackson-core:2.6.3 | | | \--- com.squareup:otto:1.3.8 | | +--- commons-io:commons-io:2.4 | | +--- com.fasterxml.jackson.core:jackson-databind:2.6.3 (*) | | \--- com.squareup:otto:1.3.8 | +--- com.google.dagger:dagger:2.0.2 (*) | +--- com.upsight.android:google-advertising-id:4.1.3 | | +--- io.reactivex:rxandroid:1.0.1 (*) | | +--- com.upsight.android:analytics:4.1.3 (*) | | +--- com.google.dagger:dagger:2.0.2 (*) | | +--- com.android.support:support-v4:23.2.1 (*) | | +--- com.google.android.gms:play-services-ads:8.4.0 -> 9.2.0 (*) | | +--- com.upsight.android:core:4.1.3 (*) | | +--- com.upsight.android:marketing:4.1.3 | | | +--- io.reactivex:rxandroid:1.0.1 (*) | | | +--- com.upsight.android:analytics:4.1.3 (*) | | | +--- com.google.dagger:dagger:2.0.2 (*) | | | +--- com.upsight.android:core:4.1.3 (*) | | | +--- commons-io:commons-io:2.4 | | | +--- com.fasterxml.jackson.core:jackson-databind:2.6.3 (*) | | | \--- com.squareup:otto:1.3.8 | | +--- commons-io:commons-io:2.4 | | +--- com.fasterxml.jackson.core:jackson-databind:2.6.3 (*) | | \--- com.squareup:otto:1.3.8 | +--- com.upsight.android:google-push-services:4.1.3 | | +--- io.reactivex:rxandroid:1.0.1 (*) | | +--- com.upsight.android:analytics:4.1.3 (*) | | +--- com.google.dagger:dagger:2.0.2 (*) | | +--- com.android.support:support-v4:23.2.1 (*) | | +--- com.google.android.gms:play-services-gcm:8.4.0 -> 9.2.0 (*) | | +--- com.upsight.android:core:4.1.3 (*) | | +--- com.upsight.android:marketing:4.1.3 (*) | | +--- commons-io:commons-io:2.4 | | +--- com.fasterxml.jackson.core:jackson-databind:2.6.3 (*) | | \--- com.squareup:otto:1.3.8 | +--- com.upsight.android:managed-variables:4.1.3 | | +--- io.reactivex:rxandroid:1.0.1 (*) | | +--- com.upsight.android:analytics:4.1.3 (*) | | +--- com.google.dagger:dagger:2.0.2 (*) | | +--- com.upsight.android:core:4.1.3 (*) | | +--- commons-io:commons-io:2.4 | | +--- com.fasterxml.jackson.core:jackson-databind:2.6.3 (*) | | \--- com.squareup:otto:1.3.8 | +--- com.upsight.android:marketing:4.1.3 (*) | +--- com.upsight.android:core:4.1.3 (*) | +--- commons-io:commons-io:2.4 | +--- com.fasterxml.jackson.core:jackson-databind:2.6.3 (*) | \--- com.squareup:otto:1.3.8

Donc une seule dépendance peut vous amener transitivement des milliers de méthodes, tout ceci juste pour des analytics.

Maintenant que la liste de dépendances est significativement plus courte (et en oubliant temporairement les librairies d’analytics, monitoring, remontée de crash et éventuelles publicités), la principale dépendance qui nous reste est Unity, le moteur de jeu sur lequel Pokémon Go est basé. Unity est la raison pour laquelle quand vous lancez l’application, vous avez un écran de lancement avec le logo de Niantic (alors que les splashscreens sont à éviter sur Android) pour laisser le temps au moteur de jeu de se lancer. Ensuite vous avez un autre chargement (ce coup-ci avec un niveau de progression) qui permet au moteur de charger toutes les ressources dont il a besoin. Ensuite, tout votre temps sera passé à l’intérieur de Unity (ce qui est la raison pour laquelle vous ne voyez pas d’interface native Android).

Cela nous amène à une autre dépendance qui a fait couler beaucoup d’encre: le SDK de Google pour la réalité virtuelle. Il y a eu quelques articles pendant la bêta de Pokemon Go où plusieurs personnes ont remarqué la mention de Cardboard et de réalité virtuelle dans le code (en utilisant les mêmes techniques que celles exposées ici), ou cette semaine quand l’application a été mise à jour et que Cardboard a été mentionné dans les licences utilisées. J’ai peur que notre analyse du code ne révèle en fait aucune planification de réalité virtuelle ou de compatibilité avec Cardboard. En effet, le code Cardboard qui a été importé dans l’application est uniquement utilisé pour faire le lien entre le framework Android et Unity. Pokémon Go, comme les applications Cardboard, a besoin de faire communiquer ces deux parties, et donc les développeurs ont inclus quelques morceaux de code open source pour faciliter la tâche. Mais, selon moi, il n’y a absolument rien dans le code qui indique une future version en réalité virtuelle de Pokémon Go.

A ce stade, nous avons passé pas mal de temps dans le code, mais nous n’avons toujours pas un projet fonctionnel, parce que nous avons ignoré les ressources et assets. Donc pour en finir avec cette partie et passer à la suite, nous avons nettoyé les artefacts restants de la décompilation (méthodes qui n’ont pas pu être décompilées, confusion entre code Java et AIDL, code Java incorrect, fichiers en trop : plusieurs instances de BuildConfig.java, Manifest.java et XXXR.java…). Nous pouvons maintenant passer aux ressources et assets.

Ressources et assets

Heureusement, accéder aux ressources et assets va être beaucoup plus simple que pour le code source. En effet, les assets ne sont pas modifiés par la phase de build, et sont inclus tels quels dans l’application. Mais pratiquement tous les assets de Pokémon Go sont pour Unity, donc nous n’allons pas y toucher pour le moment.

Dans un projet Android, les ressources sont beaucoup plus intéressantes. Elles contiennent les icônes, mises en page et textes de votre application.

Mais contrairement aux assets, les ressources sont affectés par le build (plus spécifiquement par aapt, l’Android Asset Packaging Tool qui, malgré son nom, manipule les ressources et non les assets). Plus spécifiquement les layouts xml (et le Manifest) sont convertis dans un format xml binaire, ce qui rend impossible la lecture ou édition par un humain. Aussi, les images 9-patch sont transformées et perdent les lignes et colonnes qui indiquent la manière dont elles se comportent quand elles sont redimensionnées.

La bonne nouvelle est que nous avons un autre outil à notre disposition : apktool, qui nous permet de défaire ces transformations. L’option decode (apktool d) prend un apk et nous donne quasiment un projet Android fonctionnel (avec Manifest et ressources dans un format plus accessible). La raison pour laquelle nous ne l’avons pas utilisé plus tôt est qu’apktool transforme le fichier classes.dex en fichiers source Smali, alors que nous préférons des classes Java.

Nous pouvons maintenant inclure les ressources et Manifest décompilés ainsi que les assets à notre projet et, étant donné que nous avons nettoyé le code java plus tôt, nous avons maintenant tout le nécessaire pour avoir un projet Android fonctionnel.

Compilation et lancement

Pour transformer ces sources en un nouvel apk, nous avons encore besoin de créer un projet et de lui donner des instructions de build. Comme mentionné plus tôt, ces étapes ne peuvent pas être déduites par reverse-engineering vu qu’elles ne sont pas présentes dans l’apk. Nous avons pris la solution la plus simple (et officielle) : Gradle.

Comme nous avions identifié toutes les dépendances nécessaires un peu plus haut, les redéclarer dans le nouveau projet est trivial.

L’élément le plus intéressant que révèle ce processus concerne les spécifications minimum. Actuellement, l’application sur le store demande à ce que vous ayiez au moins Android KitKat (Android 4.4, API level 19). Mais la dépendance la plus contraignante du projet est le Google VR SDK, qui ne requiert que API level 16 (JellyBeans, ou Android 4.1). A partir du code, je n’ai rien trouvé qui justifie que l’application sur le Play Store requiert 3 versions de plus. Cette restriction exclut 20% des utilisateurs d’Android (d’après les chiffres fournis par Google). Soit c’était une décision volontaire de leur part (par exemple, s’ils prévoient une future version qui demandera effectivement d’avoir au moins API level 19 et ne veulent pas que les utilisateurs de l’application ne puissent pas mettre à jour), soit ça peut être une erreur de leur part (par exemple, en spécifiant 19 plus tôt quand c’était justifié et en oubliant de le retirer quand les dépendances ont changé).

Mais la bonne nouvelle, c’est que nous avons maintenant un projet capable d’être lancé sur un vrai téléphone. Si vous voulez installer une application que vous avez reverse-engineeré de cette façon, ma première recommendation serait de change l’id de l’application (dans le build.gradle et dans les composants et permissions déclarées dans le Manifest) pour pouvoir installer les deux applications (la vôtre et l’officielle) en parallèle et qu’elles n’interfèrent pas. Quelle que soit l’application sur laquelle vous travaillez, il y a de bonnes chances pour que vous vouliez garder l’application officielle à côté, qui elle continuera à être mise à jour.

Malheureusement, vous ne serez pas capables d’aller très loin avec cette version : vous resterez bloqués à l’écran d’authentification. La première option sur cet écran (qui est très probablement la plus utilisée, de loin) est le Sign-In Google. Mais quand vous y faites appel, il y a une vérification qui est faite pour s’assurer que l’application faisant la requête est bien celle qu’elle prétend être. Plus précisément, elle vérifie le certificat avec lequel l’application a été signée. Etant donné que nous n’avons manifestement pas le même certificat que les développeurs de l’application, cette étape échouera (vous aurez une exception GoogleAuthException: INVALID_AUDIENCE). Si vous étiez capables de contourner cette vérification, ce serait une énorme faille de sécurité (pas juste pour cette application, mais pour tous les Sign-In Google dans toutes les applications), donc probablement hors de notre portée. Nous pourrions contourner le problème en enregistrant notre projet en tant que nouvelle application dans la Google Developer Console (avec notre propre id et certificat) ce qui nous permettrait de nous authentifier, mais ne nous donnerait pas un token que nous pourrions utiliser pour interagir avec le back-end (qui serait justifié de rejeter nos requêtes).

Mais il y a une autre option d’authentification, dont je doute qu’elle vérifie le certificat de l’application : en passant par un compte Pokémon Trainer Club. Malheureusement, à cause de la popularité de Pokémon Go, ce processus a été désactivé face à la trop grande demande de création de comptes. Quand il sera de nouveau disponible, nous pourrons vérifier si nous sommes capables d’utiliser notre application en passant par cette option d’authentification.

Analyse du code

Cette partie sera un bref aperçu de ce que nous pouvons apprendre à partir du code auquel nous avons désormais accès. Le but principal de cet article est de parler de “reverse-engineering en général”, mais cette section est extrêmement variable d’une application à l’autre, donc ne nous fournirait que peu d’enseignements sur comment analyser d’autres applications. Nous laisserons la suite de l’analyse de Pokémon Go aux fans motivés.

Comme nous l’avons vu précédemment, la plus grande partie de l’application vit à l’intérieur de Unity. Le bénéfice pour les développeurs est que Unity est cross-platform. Donc tout code écrit pour Unity pourra être lancé sur iOS, Android ou tout autre plateforme supportée par Unity. Donc le code restant correspond aux parties spécifiques à Android, qui n’avaient pas leur place dans Unity. Le code auquel nous avons accès couvre :

  • Authentication / Création de compte (dans le package com.nianticlabs.nia.account)
  • Achats in-App (dans com.nianticlabs.nia.iap)
  • Interaction avec les éléments gérant la géolocalisation, réseau et capteurs (dans com.nianticlabs.nia.location/network/sensors)
  • Communication via Bluetooth avec le Pokémon Go Plus (dans com.nianticproject.holoholo.sfida)

A première vue, la partie la plus intéressante sera le code responsable de géolocalisation/réseau/capteurs (si votre objectif est de tricher en simulant votre position ou vitesse, ou en effectuant les requêtes qui vous arrangent : capture automatique des pokémons au premier essai, récupération des positions exactes et natures, durées des leurres, …).

Mais je pense que le code responsable de l’interaction avec le Pokémon Go Plus est probablement beaucoup plus intéressant pour les joueurs. La promesse de cet appareil est de vous notifier quand vous êtes à proximité d’un PokéStop (et de retrouver les objets contenus) ou de Pokémons capturables, tout en ayant votre téléphone dans votre poche ou votre sac. Cela veut dire qu’il y a déjà du code capable d’interagir avec le jeu en arrière-plan, alors que le téléphone est verrouillé. Etre capable de déclencher ce comportement sans un Pokémon Go Plus et pouvoir être prévenu par une notification sur le téléphone simplifiera considérablement la jouabilité du jeu pour la vaste majorité des joueurs. Si vous combinez ceci avec l’analyse des requêtes réseaux, vous pourriez par exemple être capable d’être notifiés uniquement des pokémons autour de vous qui vous intéressent : vous pourriez avoir l’application tournant en arrière-plan en continu et seriez notifiés uniquement le jour où vous croisez le pokémon qui manquait à votre collection.

Voici un aperçu de quelques méthodes présentes dans une interface décrivant la communication avec le Pokémon Go Plus :

boolean notifyCancelDowser(); boolean notifyError(); boolean notifyFoundDowser(); boolean notifyNoPokeball(); boolean notifyPokeballShakeAndBroken(String str); boolean notifyPokemonCaught(); boolean notifyProximityDowser(String str); boolean notifyReachedPokestop(String str); boolean notifyReadyForThrowPokeball(String str); boolean notifyRewardItems(String str); boolean notifySpawnedLegendaryPokemon(String str); boolean notifySpawnedPokemon(String str); boolean notifySpawnedUncaughtPokemon(String str); boolean notifyStartDowser();

Ce type d’informations est extrêmement précieux ! Et vous avez probablement accès à assez d’informations pour construire votre propre appareil compatible, si vous le souhaitez :

Interception des requêtes réseaux

Il y a une autre catégorie de reverse engineering qui ne demande pas de lire le code ou même de mettre la main sur un fichier de l’application : regarder comment une application interagit avec le reste du monde. Le principe de cette méthode n’est pas spécifique à Android et peut être appliqué à n’importe quel logiciel.

Toute application digne de ce nom va interagir avec son environnement de plusieurs manières. Les applications les plus basiques vont interagir avec l’écran (à travers l’affichage et la récupération des évènements au toucher), qu’il est possible de tromper à travers les options d’accessibilité (ce sujet sera le thème d’un de nos futurs articles). Mais beaucoup d’applications vont interagir avec d’autres composants : le système de fichiers, les capteurs, les interfaces réseaux, …

Ici nous nous intéresserons surtout aux requêtes réseaux. Comme mentionné précédemment, les décisions les plus importantes concernant le jeu sont prises sur les serveurs. L’application a besoin d’interagir avec ces serveurs pour être tenue au courant de l’évolution du jeu. Si nous sommes capables d’intercepter ces requêtes et de comprendre comment elles fonctionnent, nous serons peut-être capables d’apprendre à faire nos propres échanges avec les serveurs sans devoir passer par l’application (qui est un outil extrêmement limité de manipulation de l’ensemble des données fournies par les serveurs).

Soit dit en passant : sur le sujet des requêtes, Pokémon Go fonctionne sur un modèle appelé Optimistic Models (ou plus exactement, une forme réduite de ce modèle). Le fonctionnement de ce modèle est qu’à chaque action que vous effectuez, l’application n’attend pas de confirmation du serveur et passe directement à la suite. S’il n’y a pas d’erreur et que le serveur confirme bien a posteriori, l’application paraît beaucoup plus rapide et réactive. S’il y a effectivement une erreur, (ce qui devrait être exceptionnel quand les serveurs ne sont pas surchargés), l’application prévient l’utilisateur qu’il y a eu une erreur et vous ramène à un état précédent. Vous pouvez constater ce comportement quand vous essayez de transférer un pokémon : il n’y a pas d’indicateur de chargement, vous êtes uniquement notifiés en cas d’échec. Ceci est particulièrement pertinent pour une application mobile, où les requêtes réseaux peuvent prendre un certain temps. Actuellement, ce modèle est à ses limites à cause de la saturation des serveurs (ce qui provoque plus d’erreurs que normalement), mais ceci devrait revenir à la normale d’ici quelques semaines.

Donc, comment allons nous intercepter ces requêtes ? La manière la plus simple consiste à se mettre comme intermédiaire entre l’application et le serveur, à l’aide d’un proxy. Mais un proxy basique est limité : si la communication se fait via HTTPS, les requêtes et réponses seront cryptées. Donc vous serez capables de récupérer quelques métadonnées sur les échanges, mais les parties les plus importantes (les contenus) resteront inaccessibles.

L’étape d’après est une attaque de type man-in-the-middle. Le principe est que votre proxy se fait passer pour le serveur auprès de l’application et pour l’application auprès du serveur. Etant donné que vous êtes à l’extrémité de chaque communication (et non plus une étape intermédiaire), vous êtes l’élément avec lequel le serveur et l’application vont effectuer leurs échanges de clés cryptographiques. Autrement dit, quand vous recevez une requête de l’application, vous pouvez la décrypter avec votre clé côté application, ré-encrypter la requête avec votre clé côté serveur, attendre une réponse du serveur, la décrypter avec votre clé côté serveur, la ré-encrypter avec votre clé côté application et l’envoyer à l’application. Vous avez accès à tous les contenus de la requête et de la réponse mais ni l’application ni le serveur ne sont au courant qu’il y avait une tierce partie impliquée dans le processus.

Evidemment, si l’histoire s’arrêtait là, toute encryption sur internet (et HTTPS) seraient des solutions de sécurité quasi inutiles vu que toute personne ayant accès à votre routeur pourrait mettre en place un proxy de ce type et voir tout ce que vous faites sur internet. La racine du problème dans le scénario précédent est que nous échangeons des clés avec des inconnus. Une des solutions possibles serait d’identifier les personnes de confiance avant d’échanger les clés. Cette solution est implémentée via des certificate authorities, qui servent d’intermédiaires de confiance. Votre téléphone ou navigateur web connait avec quelques uns de ces intermédiaires avant que vous l’ouvriez pour la première fois, ce qui vous permet d’interagir avec les serveurs en lesquels ces intermédiaires ont confiance (c’est comme ça que HTTPS, ou plus précisément TLS, se comporte). Si vous essayez d’interagir avec un inconnu, vous recevrez un avertissement ou un message d’erreur.

Mais comme nous avons la main sur le téléphone, si nous voulons toujours intercepter les requêtes réseaux, nous pouvons ajouter notre propre certificat sur le téléphone, ce qui aura pour effet que le téléphone aura confiance en notre proxy. Donc même si le trafic passe par HTTPS (ce qui est le cas pour Pokémon Go), il y a toujours des possiblités pour intercepter et étudier les échanges réseaux.

Il y a une vaste collection d’outils disponibles pour cette tâche, comme mitmproxy. Un autre outil peut-être plus accessible (mais payant) est Charles. Après configuration de Charles et de notre téléphone, nous pouvons maintenant voir les échanges. Voici un exemple d’une session quand vous lancez l’application :

Nous pouvons déjà tirer un certain nombre d’enseignements de cette séquence d’évènements. Elle confirme notamment une bonne partie de ce que nous avions remarqué quand nous avions regardé le contenu de l’apk. Jetons un coup d’oeil aux premières requêtes :

Nous pouvons constater que l’application interagit principalement avec https://pgorelease.nianticlabs.com/plfe/ (si vous allez à cette adresse, vous pourrez voir une page avec le message suivant: “Dude, this is the Player Frontend.” et qui a comme titre “Holoholo Player Frontend”: nous avons déjà vu “holoholo” dans le code, via le package qui contenait les interactions avec le Pokémon Go Plus: com.nianticproject.holoholo.sfida).

Le nombre suivant dans l’URL (dans le screenshot: 226) est déterminé par la première requête (qui ne contient pas de nombre dans l’url). Je soupçonne que ce nombre sert pour la répartition de charge : dans la première requête, nous demandons à être assignés à un serveur (identifié par ce nombre), et toutes les requêtes suivantes (pour cette session) sont faites à ce même serveur.

Enfin, la dernière partie de l’URL de la requête est “rpc”, qui veut probablement dire que l’application communique avec le serveur via Remote Procedure Call. Ceci est cohérent avec le fait que toutes les requêtes sont faites sur la même URL (ce qui ne serait pas le cas si les échanges étaient faits via REST).

Maintenant, si nous regardons le contenu, il ne semble pas être en JSON ou en XML. Mais il n’est pas compressé ou crypté non plus : nous pouvons clairement détecter des UUIDs et du texte brut (“pm0015” et autres). En connaissant le faible de Google pour les protocol buffers (ou leurs variantes, comme les flat buffers), c’est probablement le format de sérialisation qui a été choisi. Et en fait, Charles peut nous aider sur ce sujet en nous montrant les contenus des requêtes et des réponses vus en tant que protocol buffers (si vous préférez travailler en ligne de commande, mitmproxy gère aussi les protocol buffers, ou vous pouvez utiliser la commande protoc --decode_raw après avoir installé la librairie de protocol buffers). Et notre hypothèse était bonne, étant donné que nous pouvons passer de :

5‚€€€€ÉßÛS#pgorelease.nianticlabs.com/plfe/226:[ @nrÝZ†¡Ï¯½”'ëXÖÐ_}Î~—ñ÷0'@…Ít‘›-C÷‰ <j8y”Êvâ–9~Ă/§¾ñ¶,s^å†ïúÞ*$Äß.¸ñŒD©nz»fM¢¢

à :

1: 53 2: 6032429073588813826 3: "pgorelease.nianticlabs.com/plfe/226" 7 { 1: "nr\026\335Z\206\241\317\257\275\224\'\353X\326\320_}\220 \316~\227\361\3670\'@\205\315t\221\233-C\367\211\r<j8y\024 \224\312v\342\2269~\304\202/\036\247\276\361\266,\033s\027\006\f^" 2: 1468599616357 3: "$\002\304\337.\034\270\361\214D\251nz\273fM" } 100 { } 100 { }

Ceci est le contenu de la réponse à la première requête faite à pgorelease.nianticlabs.com/plfe/rpc. Et comme nous pouvons le constater, la réponse contient bien l’adresse URL qui sera utilisée pour les requêtes suivantes : pgorelease.nianticlabs.com/plfe/226. Et nous avons aussi une idée de la structure, même si nous n’en comprenons pas encore tous les détails.

Si vous demandez ce que sont tous ces “\xxx”, il s’agit d’une représentation appelée “octal escaping”. C’est une manière de représenter les octets qui ne peuvent pas être transcrits en caractères lisibles. Si nous utilisons un décodeur en ligne, nous pouvons voir que :

nr\026\335Z\206\241\317\257\275\224\'\353X\326\320_}\220 \316~\227\361\3670\'@\205\315t\221\233-C\367\211\r<j8y\024 \224\312v\342\2269~\304\202/\036\247\276\361\266,\033s\027\006\f^

devient :

nr5Z617754\'3X60_}06~7170\'@55t13-C71\r<j8y42v269~42/7616,s\f^

(qui, je vous l’accorde, n’est pas beaucoup plus lisible).Mais nous pouvons déjà déduire quelques conclusions à partir de cette réponse. Elle ressemble à une liste d’objets, indexés par leurs UUIDs et suivis de plusieurs attributs, parmi lesquels on peut trouver “pm0015”. Je suis prêt à parier que le contenu de cette réponse est une liste des pokémons présents aux alentours (les UUIDs servant à identifier les apparitions), et dont les types sont indiqués par le texte que nous pouvons voir (pm0015 serait le pokémon n°15: Beedrill, pm0120 serait le pokémon 120: Staryu, et ainsi de suite). Le reste des données contient probablement les coordonnées et attributs du pokémon.

En fait, cette hypothèse semble être confirmée par les autres requêtes, par exemple : https://storage.googleapis.com/cloud_assets_pgorelease/bundles/android/pm0126. Il semblerait que cette requête serve à récupérer les assets décrivant un pokémon (depuis le stockage dans le cloud de Google). Donc cette requête nous donnerait l’asset pour un Magmar, pour qu’il puisse être affiché dans l’écran de capture.

Si nous continuons à creuser, nous pouvons détecter d’autres informations. Par exemple, cet extrait semble faire partie d’un réponse qui contient les informations sur un joueur :

100 { 1: 1 2 { 1: 1467925951134 2: "REDACTED: player name" 7: "\000\001\003\004\a" 8 { 8: 1 } 9: 250 10: 350 11 { } 12 { } 13 { } 14 { 1: "POKECOIN" } 14 { 1: "STARDUST" 2: 500 } } }

Une valeur devrait sauter aux yeux des développeurs les plus confirmés : le début et la longueur de 1467925951134 nous évoque un timestamp Unix, avec une précision à la milliseconde. Et la date associée est 07/07/2016 21:12 (qui est beaucoup trop proche du jour courant pour être un accident). Vu le contexte, je suppose que cette date indique la création du compte de l’utilisateur. Vous pouvez trouver ce type de timestamps dans toutes les requêtes et réponses. Il y a une inconsistence par contre : le précision est parfois jusqu’à la milliseconde et parfois jusqu’à la nanoseconde (même si cette précision n’est que théorique : le nombre de nanosecondes vaut toujours 0). Par exemple, 1467338276561000 correspond au 1er Juillet.

En creusant un peu plus, nous pouvons constater l’apparition régulière d’une paire de valeurs l’une après l’autre, par exemple : 0x40486ddc40000000, 0x4002d99520000000. Ces valeurs varient d’une instance à l’autre mais sont toujours proches des autres paires (et sont systématiquement en paires). Etant donné que nous nous attendons à trouver des coordonnées dans la plupart des échanges, et la consistence des valeurs, il y a de bonnes chances que ces nombres représentent des coordonnées GPS. Mais le format est un peu plus compliqué. Ce ne sont pas de gigantesques nombres entiers écrits en hexadécimal, mais des doubles selon la norme IEEE 754. La paire précédente représente les valeurs décimales suivantes :

Hé mais ce sont les coordonnées de nos bureaux ! Avec un peu de scripts, nous pouvons extraire toutes les coordonnées contenues dans une réponse, et les afficher sur une carte. Voila les données contenues dans une seule réponse, qui contenait plusieurs types de points. Notre hypothèse quant à ce que les différents types représentent : la position de l’utilisateur (en jaune), les points d’intérêt / PokéStops (en rouge) et les points d’apparition potentiels (en vert).

PokemonGo - Google My Maps

PokemonGo

Avec ceci, nous sommes désormais capables d’accéder aux échanges réseaux, nous avons leur format de sérialisation (protocol buffers), et nous avons identifié quelques ids, timestamps et coordonnées GPS.

Ici encore, nous arrêterons notre analyse et laisserons les fans motivés poursuivre.

Conclusion : comment limiter le reverse engineering

Il se peut qu’après avoir lu tout ceci, vous vous demandiez s’il y a quoi que ce soit que les développeurs auraient pu faire pour limiter ou empêcher ce type d’analyse. Il se trouve qu’il y a plusieurs options disponibles.

La plus évidente aurait été d’obfusquer le code Java, en utilisant Proguard. Cet outil permet de remplacer tous les noms de packages, de méthodes et de champs par des noms aléatoires, qui aurait certainement ralenti notre analyse de manière significative. Si vous vouliez reverse-engineerer une application qui a été obfusquée, la meilleure stratégie est de partir des classes du framework (qui ne peuvent pas être obfusquées) et de reconstruire à partir de celles ci (par exemple, trouver les classes qui héritent de : Activity, Fragment, View, etc…). Aussi, Proguard ne se contente pas juste de faire de l’obfuscation mais est aussi capable d’éliminer le code et les ressources inutilisés, ce qui aurait eu pour effet de réduire la taille de l’application (même si pour Pokémon Go, l’effet aurait été très limité, puisque l’essentiel de la taille de l’application est consommée par Unity et ses assets, sur lesquels Proguard n’a aucun effet). Proguard est aussi très simple à mettre en place et je suis persuadé que les prochaines versions s’en serviront.

Une autre possibilité aurait été de réduire la quantité de code écrit en Java (qui pourra toujours être reverse engineeré étant donné qu’il doit être interprété par la JVM). Par exemple, compiler les parties critiques de votre application en librairies natives rend toute analyse significativement plus compliquée. Mais, contrairement à Proguard, cette méthode a un impact sur le développement et, s’il y a trop d’échanges entre Java et les librairies natives, cela peut aussi avoir un impact négatif sur les performances de votre application.

Enfin, nous avons été capables d’intercepter les échanges réseaux uniquement parce que l’application ne faisait pas appel au certificate pinning (autrement dit : ne pas faire confiance en des tiers, mais inclure dans votre application un certificat qui sera le seul moyen de communiquer avec le serveur). C’est une procédure courante, et très bien documentée, que vous fassiez vos échanges réseaux via les classes Android de base, ou via OkHttp. Comme l’obfuscation, cela n’arrêtera pas des personnes extrêmement motivées (qui peuvent accéder à votre certificat par reverse-engineering), mais peut certainement les ralentir.

En conclusion, malgré sa longueur, cette analyse est plutôt basique, reposant uniquement sur des outils sur étagère, accessibles à tous. Nous n’avons pas dévoilé de secrets qui pourraient impacter le jeu, ou publié des outils de triche, ou donné aux lecteurs un avantage déloyal sur leurs adversaires. Mais si vous êtes un développeur d’application, vous vous devez d’être au courant de ces techniques et avoir conscience que des personnes motivées y feront appel si vous ne faites rien pour les en empêcher.

Voici un court résumé de ce que nous avons trouvé :

  • Le code n’est pas obfusqué, ce qui rend les tentatives de reverse-engineering beaucoup plus simples
  • Nous sommes capables de reconstruire un projet fonctionnel
  • Les dépendances ne sont pas gérées au mieux
  • Aucun indice sur une future version en réalité augmentée ou compatible avec Google Cardboard
  • Il est peut-être possible de réduire les spécifications minimales
  • Nous avons un accès direct à un certain nombre d’éléments : code gérant la position, le réseau ou les capteurs, et gérant la communication avec le Pokémon Go Plus
  • Les requêtes réseaux peuvent être facilement interceptées grâce à l’absence de certificate pinning
  • Les échanges semblent passer par une version de RPC utilisant des protocol buffers

Vous pouvez trouver notre projet issu du reverse-engineering sur Github.

No items found.
Pour aller plus loin :