Bluelinky, la signature de requêtes

Bluelinky, la signature de requêtes

07/08/2021

Bluelinky​‌‍​‌​‌‍​‍ est un package Nodejs qui encapsule l’API Hyundai Bluelink et Kia UVO. Elle permet de se connecter à un véhicule, d’en connaître l’état et de réaliser des actions spécifiques. C’est une librairie que j’utilise dans le cadre de la construction du plugin Jeedom que je développe. En janvier, ce module a cessé de fonctionner pour l’ensemble de la zone europe suite à une mise à jour des systèmes de Kia et Hyundai.

Le développeur principal étant dans la zone US et dans les autres mainteneurs n’ayant pas le temps de s’en charger (he oui, c’est aussi une des joies Open Source), je me suis lancé dans la compréhension et éventuelle correction du problème.

Nature du problème

Un des membres de la communauté a déjà réussi à identifier une source potentielle, en utilisant un proxy et en analysant les requêtes HTTP qui transitent entre l’application mobile et les serveurs du fabricant, il remarque qu’une nouvelle ente est apparue dans les requêtes : Stamp.

C’est mon point de départ. Je télécharge APKExtractor sur mon téléphone et récupère le fichier de l’application sur mon PC. Les .apk étant une simple archive compressée, j’en extrais le contenu. On y retrouve les éléments classiques d’une application Android, avec en plus un répertoire lib contenant probablement des intégration bas niveaux.

Pour décompiler le code, je lance jadx, un outil permettant de décompiler et de lire du byte code. En faisant une recherche rapide sur le mot-clé "Stamp", je tombe directement sur les lignes qui m’intéressent.

La classe RemoteHttp décompilée

On y trouve premièrement, l’inclusion d’une librairie native 1 en Java cela s’appelle un JNI (Java Native Interface). Ensuite un appel à la méthode native du JNI 2 avec une clé suivi de la date courante en millisecondes. Et finalement une conversion en base64 pour l’injecter dans l’en-tête de la requête 3.

Dans le répertoire lib évoqué ci-dessus, on trouve plusieurs répertoires; un par architecture CPU ; et dans chaque, fichier libnative-lib.so correspondant au binaire associé au JNI.

L’utilisation de la commande file me permet d’en savoir plus sur le fichier :

1
2
> file x86_64/libnative-lib.so 
x86_64/libnative-lib.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=978b498e094fd7cc53102d36bbaad39837bc5e7e, stripped

C’est une librairie pour laquelle les informations de débogage ont été supprimées (stripped) et qui utilise d’autres librairies externes (dynamically linked).

En lisant l’en-tête de la librairie avec readelf, je trouve les éléments suivants :

1
> readelf -a libnative-lib.so
1
2
3
4
0x0000000000000001 (NEEDED)             Shared library: [liblog.so]
0x0000000000000001 (NEEDED) Shared library: [libm.so]
0x0000000000000001 (NEEDED) Shared library: [libdl.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]

Les librairies liblog, libm, libdl et libc sont nécessaire à son fonctionnement, et elle expose plusieurs fonctions, la quatrième semblant être celle qui nous intéresse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@LIBC (2)
4: 0000000000001360 370 FUNC GLOBAL DEFAULT 13 Java_stationdm_euapi_header_RemoteHttpHeader_stringFromJNI
5: 0000000000001070 150 FUNC GLOBAL DEFAULT 13 aes_whitebox_decrypt_cfb
6: 00000000000011a0 140 FUNC GLOBAL DEFAULT 13 aes_whitebox_decrypt_ofb
7: 00000000000007b0 143 FUNC GLOBAL DEFAULT 13 aes_whitebox_encrypt_cfb
8: 0000000000001110 140 FUNC GLOBAL DEFAULT 13 aes_whitebox_encrypt_ofb
9: 00000000000ba000 0 NOTYPE GLOBAL DEFAULT ABS _edata
10: 00000000000ba000 0 NOTYPE GLOBAL DEFAULT ABS _end
11: 0000000000001350 5 FUNC GLOBAL DEFAULT 13 aes_whitebox_decrypt_ctr
12: 0000000000001230 282 FUNC GLOBAL DEFAULT 13 aes_whitebox_encrypt_ctr
13: 00000000000ba000 0 NOTYPE GLOBAL DEFAULT ABS __bss_start

Sans chercher plus, je lance Ghidra pour voir ce que fait la fonction Java_stationdm_euapi_header_RemoteHttpHeader_stringFromJNI et manque en passant le fait que cette librairie propose des fonctionnalités AES de type whitebox (on verra ensuite ce que ça signifie).

N’ayant pas souvent l’occasion de faire de l’assembleur, j’arrive tant bien que mal à trouver une boucle de xor sur les blocks de la chaîne passé en paramètre, qui appelle lui-même une fonction interne de la librairie internalAES, qui elle-même en appelle plusieurs… Et surtout, impossible de trouver une éventuelle clé. J’ai perdu un temps considérable en tentant de comprendre l’intégralité de ce code décompilé avant d’aller me renseigner sur ce que signifiait whitebox dans le mode AES.

En chiffrement symétrique (comme avec AES), une même clé est utilisée pour chiffrer et déchiffrer le contenu du message. On s’attend donc à trouver cette clé quelque part dans le code ou dans la librairie. Afin d’éviter que la clé soit si facilement identifiable, la méthode dite de “boîte blanche” (whitebox) permet de prédéfinir la suite d’algorithmes à exécuter pour arriver au même résultat sans pour autant avoir la clé.

En résumé

  • Pour chaque appel HTTP réalisé par l’application mobile,
  • Une chaîne de caractères contenant une identifiant et une date sont,
    • Chiffrés via un algorithme AES en boîte blanche,
    • Converti en Base 64,
  • Puis sont ajoutés dans un en-tête de la requête.

On peut légitiment imaginer que le serveur utilise l’identifiant de l’application pour déchirer l’en-tête Stamp et vérifier que le contenu ainsi que la date sont valides.

L’hypothèse de la validation de la date par les serveurs n’a été implémentée par Kia/Hyundai que quelques mois plus tard.

Solution et contournement

L’idée est dans un premier temps de réussir à isoler cette partie du code et de la faire fonctionner sur un ordinateur. N’ayant pas l’expérience en C requise pour tenter un chargement directe de la librairie, je fabrique une application Java simplissime qui a pour but de charger de prendre une chaîne en entrée, de gérer l’intégration d’un JNI et de rendre le résultat.

Le code ne fonctionne pas dans un premier temps, et à juste titre, les librairies liblog, libm, libdl et libc de ma station de travail Linux ne sont pas compatibles avec la signature attendue par libnative. Un petit coup de moteur de recherche me permet de tomber sur un dépot git contenant plusieurs versions du NDK, et après plusieurs tests, je trouve finalement le bon.

Reste à les charger pour ce programme sans altérer mon système d’exploitation. Le runtime Java met à disposition une paramètre java.library.path permettant de définir le lieu où se trouvent les JNI (Le fichier libnative-lib.so en ce qui me concerne). Ensuite, il est nécessaire de spécifier au système d’exploitation, où il doit chercher les dépendances externes (ici liblog, libm, libdl et libc), ce que Linux permet de faire facilement en utilisant la variable d’environnement LD_LIBRARY_PATH.

Une fois compilé, j’obtiens donc cette exécution qui fonctionne à merveille :

1
2
> libs="./lib/x64"
> LD_LIBRARY_PATH=$libs java -Djava.library.path=$libs -jar ./target/main-1.0.jar "test"

Le résultat produit est effectivement compatible avec ce qu’attend l’API et l’authentification passe. Avant toute chose, j’adapte mon projet pour le faire fonctionner dans une image Docker. Reste à savoir comment on peut intégrer ce résultat à la librairie Node.js existante.

Intégration

La solution doit pouvoir fonctionner sur tous les systèmes d’exploitation et sans dépendances particulières, donc node-gyp ou une intégration Docker sont malheureusement exclus.

Janvier 2021

Le choix a été fait de générer une grande quantité de signatures en incrémentant la partie timestamp. Les informations sont ensuite intégrées au module dans un fichier JSON, le code piochant au hasard une signature dans la liste.

Juin 2021

Au mois de juin, le fichier de signatures aléatoires n’a plus suffi. Hyundai et Kia ont légitimement commencé à valider la partie date de la signature, la limitant à 24 h. Le choix a donc été fait de récupérer dynamiquement une liste de signatures générées pour les heures à venir et de l’utiliser. Cette liste est stockée sur un autre dépôt GIT, et un simple accès http permet de la lire. Elle est générée et mit à jour via un CRON.

Juillet 2021

En juillet Kia à mi à disposition sa nouvelle application mobile. La clé utilisée pour la signature change et les fichiers libnative-lib.so ne sont disponible que pour les architectures ARM. J’ai utilisé le même procédé que celui décris ci-dessus, en faisant tourner le tout dans un docker sur ARM et en utilisant l’émulateur Qemu pour faire tourner le tout.

Conlusion

Même si ce compte-rendu semble simple à répliquer, les phases d’analyse et de contournement n’ont pas été instantanées. Elles ont requis la découverte et la prisent en main d’un bon nombre d’outils, un bon nombre de tentatives pas toujours concluantes ni réjouissantes et finalement un temps de simplification, d’adaptation et d’encapsulation afin que ces travaux puissent être repris par la communauté.

Il est aussi probable qu’a votre prochain passage sur cet article, de nouvelles phases dans la rubrique intégration soient présentes. Au fil des mises à jour de l’application mobile, de nouveaux comportements peuvent apparaître.