Bluelinky, signature de requête en JS

Bluelinky, la signature de requêtes en JS

Cet article fait suite à ce qui est exposé dans ce premier article sur le mécanisme de signature de requête mis en place par Kia et Hyundai dans le cadre de leurs applications mobile. Plus précisément une suite du point de mai 2022 suite auquel un contrôle plus précis sur l'algorithme a été possible.

Previoussement

Le système de signature de requête fonctionne dans l'application android grâce à l'intégration d'une librairie native, externe (Java Native Interface : JNI). Elle réalise une signature AES permettant de valider coté serveur authenticité des requêtes. La clé utilisé dans le cadre de cette génération ne pouvant évidemment pas se trouver dans le client applicatif, est masquée par un système dit de whitebox. Ce mécanisme rend toute tentative d'extraction particulièrement pénible. La librairie n'étant disponible que pour la plateforme visée (ARM), une émulation est nécessaire pour utiliser la librairie sur une autre architecture.

La manière identifiée à l'époque pour utiliser la librairie et générer nos propres signatures a été d'utiliser le JNI au sein d'une application JAVA, packagée via Docker et de faire fonctionner l'ensemble dans un système d'émulation ARM nommé Qemu. Cela représente de nombreuses strates applicatives, mais fonctionne.

En Mai 2022, ManuBatBat arrive avec une intégration native en C permettant de rebattre toutes les cartes.

L'exploit'

Un algorithme en whitebox applique un certain nombre de permutations au message passé en paramètre pour obtenir le message de sortie. Pour cela, il utilise un jeu de données définissant l'impact de chaque permutation. La librairie externe contient donc deux parties, algorithme d'un côté et le jeu de données de l'autre.

Le premier est identifié, documenté, on en trouve des implémentations et est facilement réplicable. Le second est contenu dans la librairie même et il sera nécessaire de l'extraire afin de pouvoir l'utiliser. Nous connaissons la structure de ces données (car nous connaissons l'algorithme) et nous savons que ces données se suivent. Donc si nous trouvons le pointeur de début, nous pouvons extraire les données.

C'est ce que nous faisons, on commence par identifier l'emplacement des données puis de les extraire et on les exploite dans un algorithme standard qui retourne bien le résultat attendu.

Extraction

Nous avons la main sur l'ensemble du code et du jeu de sonnées. Plusieurs options s'offrent à nous.

La première est d'extraire le jeu de données (en JSON) et de l'utiliser dans le code Javascript de Bluelinky. Bien que ce soit probablement l'option la plus intéressante et la plus fiable, elle vient avec l'inconvénient suivant : le jeu de données représente ~2Mo de données.

En ajoutant cela au code, le module croit de ~4Mo ce qui est difficilement envisageable. On peut compresser ou même utiliser des données binaires pour réduire le volume, mais ça reste beaucoup (~1.2Mo).

Analyse

En regardant le code de plus près, on remarque que pour chaque block de 16 caractères, on initialise une clé de chiffrement (cypher). Cette dernière est utilisée pour appliquer un XOR au message d'entrée et générer le flux de sortie. On remarque aussi que chaque byte chiffré a un impact sur la forme du cypher appliqué au prochain bloc.

Le message que nous voulons chiffrer fait 50 caractères de long. Soit 3 blocs et 2 octets.

##################################################
|               |               |               | 
|______16_______|_______16______|______16_______|2

Les 37 premiers caractères sont connus, car ils représentent l'identifiant de l'application qui est constant. Le reste est un timestamp en millisecondes.

e7bcd186-a5fd-410d-92cb-6876a42288bd:XXXXXXXXXXXXX
|               |               |               | 
|______16_______|_______16______|______16_______|2

Le cypher étant déjà initialisé et connu pour le troisième bloc, est est possible de l'utiliser pour chiffrer cette partie du message.

e7bcd186-a5fd-410d-92cb-6876a42288bd:16145385064XX
|               |               |               | 
|______16_______|_______16______|______16_______|2

Il nous reste donc deux bytes à chiffrer et pour celà nous avons besoin de générer un nouveau cypher et donc du jeu de données complet 😔.

Utilisation

Le hasard étant ce qu'il est, il s'avère que ces deux derniers bits n'ont pas d'importance. Le serveur ne semble pas en tenir compte, soit seul la partie correspondant aux secondes est utilisée, soit le mécanisme qui analyse (parse) la chaîne ignore les caractères non-numériques, mais le fait est que ça marche 🎉.

En modifiant un peu le code, on peut extraire la clé utilisée pour le XOR (la suite des Cyphers) et l'utiliser dans n'importe quel langage. C'est ce qui est fait dans ce gist, mais aussi ce qui entrera en Bêta bientôt sur le projet principal.