Reverse Engineering d'APIs privées
Tags: api, reverseengineering, programming, python
Published: 2023-10-11 13:35:05

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](https://www.instagram.com/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 :

```http
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
```

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 :

```http
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
```

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 :

```http
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
```

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 :

```http
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
```

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](https://www.github.com/Ximaz/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.