DEV Community

Cover image for Reverse Engineering d'APIs privées
DURAND Malo
DURAND Malo

Posted on

Reverse Engineering d'APIs privées

Boujours à vous. Aujourd'hui, je vous parle de reverse engineering (rétro-ingénieurie en français). Le reverse engineering est très populaire dans le monde des applications et est souvent associé aux langages d'assembleurs. Ce n'est pas ce dont nous allons parler aujourd'hui (dans un autre article peut-être). À la place, je vous propose un dérivé tout aussi passionnant qui concerne les APIs privées de sites web.

Introduction :

Si vous avez déjà essayé de faire une application liée à un site web tel qu'Instagram, vous avez sûrement dû vous confronter à un mur : une demande d'accès API.

En effet, il est très commun que ces services vous demandent une description détaillée sur l'application que vous comptez développer, pour savoir si l'autorisation d'accéder à leur API vous sera cédée.

Le but du reverse engineering d'API est de contourner ce protocol beaucoup trop bureaucratique, chronophage et incertain.

À la place, nous allons comprendre comment notre navigateur est capable de récupérer les informations d'un compte Instagram pour nous les afficher. Cerise sur le gâteau, on sera capable de réaliser un tel projet sans avoir besoin d'un compte Instagram.

Le point de départ :

Pour commencer, nous allons ouvrir un navigateur en navigation privée pour ne pas avoir de cookie. On se rend sur la page d'un compte public quelconque. Dans notre cas, il s'agira de la fanpage de Drake.

Ensuite, on va ouvrir l'outil de développeur avec le raccourcis <ctrl+shift+i> (ou <cmd+option+i> sur Mac). On se rend dans la catégorie Réseau et on rafraîchit la page. Vous devriez voir beaucoup de requêtes web, ce qui normal pour que la page fonctionne correctement.

La phase de reconnaissance :

La première requête consistera toujours en la page elle-même. Parfois, elle contient des informations codées en dur, et se révèle utile, parfois elle ne sert à rien. Il s'agit toujours de quelque chose à garder sous la main.

Ensuite, on s'attaque aux Filtres de requêtes. Il faut cocher XHR/Récupérer. Les requêtes de ce type correspondent généralement à des appels d'APIs.

À l'heure où j'écris cette article, et je suis obligé de le mentionner car Instagram change très souvent le fonctionnement de son API, vous devriez voir les requêtes suivantes :

  • get_ruling_for_content : Pouvons-nous accéder au contenu du compte ? Le compte nous a-t-il restreint ?
  • web_profile_info : Les méta-données liées au compte : nombre de suiveurs/suivis, biographie, nombre de posts, avatar, etc...
  • query : Cette requête est spéciale et nous y reviendrons plus tard dans cet article.

Vous verrez d'autres requêtes mais nous allons nous attarder sur ces dernières uniquement.

La requête get_ruling_for_content :

Comment récupérer les informations de cette requête ? En premier lieu, il nous faut comprendre comment elle est envoyée. Notre navigateur nous permet d'observer les en-têtes de requête qui, chez moi, ressemblent à ça :

GET https://www.instagram.com/api/v1/web/get_ruling_for_content/?content_type=PROFILE&target_id=1619264285
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.9
Connection: keep-alive
Cookie: csrftoken=aYoyV2XC9Zk4HULLXfhk4NIbfx1ZXF8v; ig_did=5835D093-DF51-4C54-B58B-20D056658E52; mid=ZSUSvgAEAAFZQhE_YWAMSBixFRhf
Host: www.instagram.com
Referer: https://www.instagram.com/drake/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15
X-ASBD-ID: 129477
X-CSRFToken: aYoyV2XC9Zk4HULLXfhk4NIbfx1ZXF8v
X-IG-App-ID: 936619743392459
X-IG-WWW-Claim: 0
X-Requested-With: XMLHttpRequest
Enter fullscreen mode Exit fullscreen mode

Dans l'URL de la requête, on remarque le paramètre target_id qui vaut 1619264285. Il s'agit de l'identifiant du compte. Pour le trouver, on recherche 1619264285 dans le code HTML de la page.

Les en-têtes qui nous intéressent ici sont uniquement ceux commençant par un X-, ainsi que Cookie. Il s'agit d'une pseudo authentification de notre navigateur pour Instagram. Pour trouver comment sont générés ces en-têtes, il faudra fouiller dans la page et dans ses scripts.

Par exemple, X-CSRFToken. Si on cherche sa valeur dans le code de la page, on trouve où elle se situe. Idem pour X-IG-App-ID. Aussi, pour les valeurs de Cookie, on peut copier les clés (csrftoken, ig_did) et trouver leurs valeurs dans la page.

Pour la valeur du cookie mid, j'ai du comprendre le code d'Instagram. Il s'agit d'une valeur générée aléatoirement en base36 (le code est disponible dans la partie Conclusion).

La seule valeur qui ne nous est pas accessible est X-ASBD-ID. Pour ça, il nous faudra aller fouiller dans les scripts de la page.

En ce qui concerne X-IG-WWW-Claim et X-Requested-With, il s'agit de constantes. Elles n'ont pas pour vocation à changer.

Pour trouver la valeur de X-ASBD-ID, listez les URLs de tous les scripts de la page. Ouvrez-les et cherchez la valeur, ici 129477, pour trouver quel script la comporte.

Une fois trouvées, nous avons toutes les valeurs nécessaires pour reproduire le comportement de notre navigateur et effectuer une requête.

La requête web_info_profile :

Comme au dessus, nous allons observer les en-têtes de la requête. Voici les miens :

GET https://www.instagram.com/api/v1/users/web_profile_info/?username=drake
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.9
Connection: keep-alive
Cookie: csrftoken=aYoyV2XC9Zk4HULLXfhk4NIbfx1ZXF8v; ig_did=5835D093-DF51-4C54-B58B-20D056658E52; mid=ZSUSvgAEAAFZQhE_YWAMSBixFRhf
Host: www.instagram.com
Referer: https://www.instagram.com/drake/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15
X-ASBD-ID: 129477
X-CSRFToken: aYoyV2XC9Zk4HULLXfhk4NIbfx1ZXF8v
X-IG-App-ID: 936619743392459
X-IG-WWW-Claim: 0
X-Requested-With: XMLHttpRequest
Enter fullscreen mode Exit fullscreen mode

Par chance, il semble qu'ils soient identiques. En revanche, il faut bien faire attention au paramètre username qui correspond au nom d'utilisateur du compte cible. Cette fois-ci, c'est vous qui remplacez par le nom d'utilisateur que vous souhaitez, tant que le compte est public.

La requête query :

L'URL ayant pour endpoint query est pariculière parce qu'elle permet d'accéder à différentes resources en fonction des paramètres d'URL. Exemple :

GET https://www.instagram.com/graphql/query/?query_hash=d4d88dc1500312af6f937f7b804c68c3&user_id=1619264285&include_chaining=false&include_reel=true&include_suggested_users=false&include_logged_out_extras=true&include_live_status=false&include_highlight_reels=true
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.9
Connection: keep-alive
Cookie: csrftoken=aYoyV2XC9Zk4HULLXfhk4NIbfx1ZXF8v; ig_did=5835D093-DF51-4C54-B58B-20D056658E52; mid=ZSUSvgAEAAFZQhE_YWAMSBixFRhf
Host: www.instagram.com
Referer: https://www.instagram.com/drake/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15
X-ASBD-ID: 129477
X-CSRFToken: aYoyV2XC9Zk4HULLXfhk4NIbfx1ZXF8v
X-IG-App-ID: 936619743392459
X-IG-WWW-Claim: 0
X-Requested-With: XMLHttpRequest
Enter fullscreen mode Exit fullscreen mode

Les deux paramètres d'URL qui nous intéressent ici sont user_id (qui a la même valeur que target_id), et query_hash. Le paramètre query_hash définit la resource à laquelle vous tentez d'accéder. Ici, il s'agit de d4d88dc1500312af6f937f7b804c68c3. Vous l'aurez probablement compris mais il s'agit d'un hash.

Si on observe la réponse de la requête, on peut déterminer à quelle resource un query_hash est liée. En l'occurence, on voit beaucoup d'informations sur les stories, savoir si l'utilisateur est en live, ou encore des mentions aux highlights. Il s'agit d'un complément de méta-données de la requête web_info_profile.

Pour trouver le query_hash qui correspond à ce que vous souhaitez, observez la réponse. Baptisez-la en fonction. Exemple : d4d88dc1500312af6f937f7b804c68c3 => stories_and_highlights_metadata.

Gardez à l'esprit que ces hash sont voués à changer. Il faut donc trouver comment obtenir les valeurs à n'importe quel moment.

Et les posts dans tout ça ?

Et oui, nous pouvons aussi trouver les posts. Si vous scrollez en bas de la page, de nouveaux posts devraient charger. Sinon, cliquez sur le bouton qui apparait. Continuez d'observer les requêtes. Vous devriez voir une nouvelle requête query. Pour autant, elle semble différente de la précédente et c'est tout à fait normal. Voici la mienne :

GET https://www.instagram.com/graphql/query/?doc_id=17991233890457762&variables=%7B%22id%22%3A%221619264285%22%2C%22after%22%3A%22QVFEOUxJa2d6YWw5OFBDT1c2aXgwTkFZS0dlbER5UHJCc2FJSTBzRGxLNndjb3MtSDJRXzBPOVNhM2pySjFNU3NSRHZfbTA4VG5QOHB0LVpDdVFTRWpNRg%3D%3D%22%2C%22first%22%3A12%7D
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: fr-FR,fr;q=0.9
Connection: keep-alive
Cookie: csrftoken=aYoyV2XC9Zk4HULLXfhk4NIbfx1ZXF8v; ig_did=5835D093-DF51-4C54-B58B-20D056658E52; mid=ZSUSvgAEAAFZQhE_YWAMSBixFRhf
Host: www.instagram.com
Referer: https://www.instagram.com/drake/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15
X-ASBD-ID: 129477
X-CSRFToken: aYoyV2XC9Zk4HULLXfhk4NIbfx1ZXF8v
X-IG-App-ID: 936619743392459
X-IG-WWW-Claim: 0
X-Requested-With: XMLHttpRequest
Enter fullscreen mode Exit fullscreen mode

Pour clarifier l'URL, voici les paramètres de façon plus lisible :

  • doc_id=17991233890457762,
  • variables={"id":"1619264285","after":"<chaîne_ou_rien>","first":12}

La différence entre cette requête et la précédente est qu'au lieu d'avoir un hash, on a un identifiant. Comme tout à l'heure, on cherche la valeur dans les scripts pour savoir où le trouver par la suite.

En ce qui concerne le paramètre variables, c'est simple :

  • id : identifiant du compte (AKA user_id ou encore target_id),
  • after : système de pagination pour obtenir les first prochains résultats,
  • first : le nombre d'éléments renvoyés par requête.

Pour chaque réponse de la requête, vous devriez trouver un nouveau after qui correspond à la prochaine page. Si vous voulez commencer par le début, ne mettez rien comme valeur. Vous aurez ainsi accès aux first premiers posts.

Chercher efficacement les valeurs :

Depuis tout à l'heure je vous parle de trouver les valeurs dans les scripts, et ça peut sembler aléatoire / abstrait.

La pire chose à faire pour ce genre de projet c'est de coder les informations en dur. L'URL de script, les valeurs en elles-mêmes, etc... parcequ'Instagram, et probablement les autres services que vous pourriez rétro-ingénieurer, sont très réactifs et changent souvent les URLs, l'ordre dans lequels ils apparaissent, ainsi que les valeurs des query_hash ou des doc_id par exemple.

Pour palier à ce problème, il y a deux étapes.

Trouver rapidement la bonne valeur :

Pour se faire, il faut d'abord trouver où la requête se fait, comment elle se construit.

Les navigateurs fournissent généralement un champs Initiateur. Il s'agit de l'endroit où la requête a été formulée.

Vous pouvez ainsi remonter la piste pour savoir où le formatage de paramètres à lieu et les traquer un à un.

Une fois que vous avez compris leur formatage, vous devrez automatiser leur recherche grâce à des outils comme un parser HTML, des RegExp, un parser JSON, etc...

La force brute :

Une fois que vous avez trouvé comment cibler vos valeurs avec un context autour, rien de plus simple. Vous cherchez toutes les URLs de scripts chargés par la page. Vous les parcourez un à un et vous appliquez vos fonctions de recherches jusqu'à trouver toutes les composantes nécessaires à vos requêtes.

Éviter les limites de requêtes :

Le soucis avec ma méthode est que vous pouvez rapidement vous retrouver face à un mur : le Ratelimit.

Il s'agit d'un mécanisme qui protège les services web contre les attaques de type force brute, ou encore des attaques par déni de service. Vous êtes autorisé à effectuer X requêtes dans une plage de temps Y. Vous voulez ainsi éviter le plus possible de faire des requêtes inutiles.

Pour éviter les requêtes inutiles, établissez le plus vite possible un profil. Ce profil sera généralement un objet dans lequel vous pourrez trouver toutes les informations nécessaires aux requêtes. Le mieux est de l'enregistrer dans un fichier JSON pour le charger les fois d'après, en cas de crash par exemple.

Cette méthode permet de ne pas avoir à re-parcourir tous les scripts à la recherche de valeurs que vous aviez déjà en votre possession.

Conclusion :

Le reverse engineering d'API n'est pas sorcier. Il suffit simplement de charger une page en navigation privée, de voir comment votre navigateur se comporte, et automatiser les requêtes par vous-même.

Pour éviter les limites de requêtes, faites attention à utiliser un cache par exemple, ou des fichiers de sauvegardes.

Si vous voulez savoir plus en détail comment vous pourriez automatiser les fonctions de recherches des valeurs et des en-têtes, je vous conseille d'aller jeter un œil à mon projet Instagram API. Le langage utilisé est du Python ce qui est à la portée de tous.

Si vous avez des questions, n'hésitez pas à me le faire savoir, j'essaierai d'y répondre.

Top comments (0)