Reverse Enginnering the BasicFit QRCode
Introduction
BasicFit est une chaine de salle de sport mondiale qui propose un système d’accès aux salles par QRcode.
Les QRcode changent toutes les 7 secondes et les screens et enregistrements d’écran sont bloqués, afin de permettre l’accès à la salle de sport depuis seulement un unique appareil. Dans cet article nous allons voir comment il est possible de trouver l’algorithme de génération des QRcode afin de le répliquer en dehors de l’application mobile.
Avertissement
Les informations et techniques présentées dans cet article sont fournies à des fins éducatives uniquement. Je décline toute responsabilité quant à l’utilisation de ces informations à des fins autres que celles de l’apprentissage et de la compréhension. Veuillez respecter toutes les lois et règlements applicables.
Analyse du QRcode
Après avoir scanné un QRcode et avoir récupéré le texte qui se trouve à l’intérieur, voila ce que nous obtenons :
gm2:V001111111:2DQ:1731690331:02F19709
gm2
: ne change jamais (surement pour indiquer au serveur de basic fit que c’est un QRcode d’accès)V001111111
: le numéro de ma carte d’abonnement BasicFit2DQ
: aucune idée pour l’instant1731690331
: semble être un timestamp (logique étant donné que le QRcode change toutes les 7 secondes)02F19709
: aucune idée pour l’instant
Analyse des requêtes HTTP lors de la génération de QRcode
Sur IOS on peut utiliser cette application : HTTP CATCHER pour analyser les requêtes HTTP(s) et on remarque que cette requête est répétée toutes les 2/3 secondes :
GET /api/AccessControlResult?comId=2DQ&card=QV001111111 HTTP/1.1
La réponse est vide tant que le QRcode n’a pas été scanné à la salle de sport, on en déduit que c’est une requête qui permet à l’application de savoir quand l’utilisateur a scanné le QRcode pour lui afficher le message “Bonne séance”.
On sait maintenant que 2DQ
est un identifiant surement généré aléatoirement pour éviter que l’application ne détecte un scan de QRcode d’un autre jour.
Analyse de l’APK BasicFit
On peut déjà décompresser l’application en utilisant la commande apktool d com.basicfit.trainingApp.apk
Lien de apktool : apktool.org
On remarque dans les fichiers de l’application que dans le dossier assets
se trouve un fichier index.android.bundle
, ce qui nous indique que c’est une application React Native
.
Ce fichier est le Javascript packagé de l’application :
- Il est généré par Metro bundler, l’outil de React Native pour packager le code.
- Il est chargé à l’exécution par le moteur JavaScript intégré dans l’application (souvent Hermes ou JSC, selon la configuration).
- Dans notre cas c’est Hermes
On utilise donc hermes-dec pour désassembler le bytecode de l’application avec la commande :
hbc-disassembler assets/index.android.bundle /output.hasm
En ouvrant le fichier output.hasm
et après quelques recherches, on tombe sur cette fonction :
A la fin de la fonction on peut voir le string suivant :
On sait donc que c’est cette fonction qui s’occupe de générer le QRcode.
On retrouve notamment le string GM2 ainsi que le numéro de carte, le séparateur, etc…
En fouillant un peu plus dans la fonction on peut voir un petit algorithme de chiffrement en SHA256
avec une conversion en HEX
, ainsi qu’une fonction substring
appliquée avec comme paramètre 8.
On peut voir que le device identifier joue un role dans le chiffrement, c’est celui de mon appareil qui sert à générer le QRcode d’accès, car ce QRcode est limité à un seul appareil.
En analysant les requêtes HTTP lors du lancement de l’application on peut voir qu’il y a une API qui répond avec mes informations et notamment mon device identifier :
Ce qui la fonction appelle guid
c’est les 3 caractères aléatoire générés au lancement pour éviter les collisions de session, dans notre cas il s’agit de 2DQ
.
Le chiffrement s’effectue comme suit :
- Le numéro de carte, le guid, le timestamp (iat) et le device identifier sont concaténés pour former une chaine de caractère
- Cette chaine est hashée en sha256 en format de sortie hexadécimal
- 8 caractères sont extraits de ce hash
Si on essaye de reproduire ce chiffrement avec nos données on obtient ceci :
Je rappelle qu’au début, nous avions ces informations dans notre QRcode :
gm2:V001111111:2DQ:1731690331:02F19709
Et on remarque que les 8 derniers caractères du hash obtenu grâce à l’algorithme correspondent à notre donnée manquante :
- hash : ac7727ec41c64ed0f4e40b47796b59500dec66e83eb6651510eabc040
2f19709
- donnée manquante :
02F19709
Nous avons donc tous les élements pour pouvoir récréer un QRcode sans être sur le bon appareil.
Étapes de reproduction du QRcode
Au début de l’algorithme :
- On récupère notre device identifier lié à notre compte
- On génère 3 caractères aléatoires qu’on utilisera comme guid
- On récupère notre numéro de carte basicfit
Et ensuite toutes les 7 secondes on applique cet algorithme :
- On récupère le timestamp actuel
- On concatène le numéro de carte, le guid, le timestamp et le device identifier
- On applique un chiffrement sha256 sur cette chaine
- On récupère les 8 derniers caractères du hash, qu’on appelera result
- On génère un QRcode avec les données formattées comme suit :
gm2:card_number:guid:timestamp:result
Et voila ! On peut désormais générer un QRcode d’accès valide sans utiliser l’application BasicFit.
Conclusion
Pour la conclusion je vous donne un petit code python qui correspond à l’algorithme :
import qrcode
import hashlib
import time
import random
import string
import base64
from io import BytesIO
def generate_random_code():
characters = string.ascii_uppercase + string.digits
return ''.join(random.choices(characters, k=3))
def generate_qrcode(card_id, device_identifier):
random_code = generate_random_code()
timestamp = str(int(time.time()))
content = card_id + random_code + timestamp + device_identifier
hash_object = hashlib.sha256(content.encode('utf-8'))
hex_dig = hash_object.hexdigest()
hex_obj = hex_dig[-8:]
return "gm2:" + card_id + ":" + random_code + ":" + timestamp + ":" + hex_obj.upper()
cardID = "V01111111" # Remplacer par le numéro de carte basicfit
deviceID = "d8af1441-19a8-4a9c-8406-0867b6674cba" # Remplacer par votre device identifier
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=1,
)
qr.add_data(generate_qrcode(cardID, deviceID))
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = BytesIO()
img.save(buffer, format="PNG")
buffer.seek(0)
# Obtenir l'image du QRcode en base64
qr_code_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
print(qr_code_base64)
J’espère que vous avez apprécié cet article.