AVERTISSEMENT : Ce blog a une visée éducative. Il n'endosse ni n'encourage en aucun cas le piratage logiciel.
Après près d'un an d'absence, il est temps de conclure cette aventure. La plupart de ce qui sera décrit ici semblera hors sujet si vous n'avez pas lu :
Résumé
Dans la Partie 1, nous avons déchiffré et récupéré l'intégralité du firmware du programmeur. Dans la Partie 2, nous avons reverse-engineeré le protocole de communication sur le lien USB. Ayant désormais toutes les cartes en main, nous pouvons atteindre notre objectif principal : contourner le système de licences en place !
Fonctions de licence
L'objectif ultime de l'exercice est de pouvoir utiliser le PowerVision sans licence valide. Cela peut être réalisé de plusieurs manières : forger une licence, désactiver la vérification à laquelle elle est soumise, ou supprimer/ignorer les verrous VIN. Pour les néophytes, le Vehicle Identification Number (numéro d'identification du véhicule) est un identifiant unique stocké dans l'ECU. Un verrou VIN est donc essentiellement un VIN stocké dans le programmeur, utilisé pour s'assurer que l'appareil ne sera pas utilisé pour programmer autre chose. Voici quelques exemples de VIN Harley Davidson :
1HD1KED10HB661265— 2017 Harley-Davidson FLHTK / Ultra Limited (1.8L), Touring1HD1BFV14EB015825— 2014 Harley-Davidson FXSB-103 Breakout (1.7L V2), Custom1HD1FC413AB618635— 2010 Harley-Davidson FLHTCU (1584CC), Touring
Licence
Pour le Dynojet PowerVision 1, un fichier de licence se présente sous la forme :
<PVLicense>
<Name>PV1</Name>
<Company>UNISEC</Company>
<Email>none</Email>
<LicenseCode>1234</LicenseCode>
<ExpireVer>2.1.0</ExpireVer>
<Cmd>VL:<VIN></Cmd>
<Signature>
q8EgYRN+XZ/88wEyYfAOQEkZ7GPoV/JbtvuYYsUEOhEWH1cyN1i9OvHPyaj945+f...
</Signature>
</PVLicense>
La partie cmd contient ici la commande VL, qui indique au PowerVision à quel VIN il est couplé. Contrôler la valeur de ce champ est le jackpot ultime, car cela permet de forger des licences pour des VIN arbitraires. La signature, cependant, est là pour empêcher exactement cela. Elle contient le SHA1 du fichier XML, chiffré avec la clé privée de Dynojet. Pour vérifier la signature, le PowerVision stocke la clé publique dans une base de données chiffrée. Il procède ensuite au hachage du fichier, et effectue un memcmp entre le hash résultant et celui obtenu par déchiffrement. Voici la vue d'ensemble de la fonction de vérification de licence :
En patchant le segment de code suivant, on peut facilement anticiper comment le contournement de la vérification pourrait être implémenté :
En reversant la fonction de vérification de licence, nous avons réalisé qu'il pouvait y avoir bien d'autres moyens de forger une licence que simplement changer le VIN. En effet, le champ cmd peut contenir plusieurs autres fonctions que celle de verrouillage VIN, et elles pourraient probablement être détournées aussi. De plus, l'une des manières les plus simples de contourner la restriction de VIN unique serait de créer une licence dealer, car elles ne sont pas soumises aux mêmes contraintes.
Verrous VIN
Une autre manière de résoudre notre problème serait de « démarier » le PowerVision. Pour cela, deux chemins s'offrent à nous :
- Localiser et modifier les verrous
- Patcher la fonction get_locks
Le premier choix a été rapidement abandonné pour la raison suivante : le PowerVision stocke les verrous en NVRAM. Nous avions déjà rencontré ce problème en essayant de localiser la clé de chiffrement du firmware, et la NVRAM ne peut pas être lue directement depuis /dev :
Elle utilise en réalité une API bas niveau, développée par le fabricant du matériel (Drew Technologies). Elle semble être liée aux fichiers :
usr/lib/libPP2534.solib/modules/2.6.30/kernel/drivers/char/ermine_arm7_ldisc.ko
On suppose qu'il faudrait utiliser l'API pour communiquer avec un module noyau capable de lire et écrire dans la NVRAM. Ça a l'air fastidieux, non ? Nous avons de meilleurs moyens...
Prenons le chemin de la flemme, et intéressons-nous aux fonctions qui interprètent les résultats récupérés depuis la NVRAM :
La fonction get_locks récupère les données depuis l'API NVRAM et stocke les données de verrouillage dans la structure suivante :
lockptr struc ; (sizeof=0x10)
active_mask DCD ?
used_mask DCD ?
first_unused DCD ?
total DCB ?
used DCB ?
free DCB ?
max DCB ?
lockptr ends
À partir de là, il devient assez facile de coder en dur certaines valeurs dans la fonction get_locks, de sorte qu'elle retourne toujours avec un certain nombre de verrous libres !
Micropatching
Voici quelques techniques de patching basiques utilisées dans certaines conditions. C'est loin d'être exhaustif.
Saut inconditionnel
Lors de la récupération du nombre de verrous VIN actifs, nous voudrions que le code saute toujours vers le bloc « Tuning: Not Locked to vehicle ».
Pour ce faire, nous remplaçons l'instruction Branch if equal par une instruction de Branch inconditionnelle :
Négation de condition
Lors de la vérification de la signature de la licence, nous voudrions que le code saute toujours vers l'emplacement « Verified: YES » :
Pour ce faire, on peut nier l'instruction Branch if not equal et la remplacer par Branch if equal :
Attention : c'est une méthode de patching relativement instable. Gardez à l'esprit que le code s'exécutera avec succès avec toute licence présentant une signature invalide, mais échouera si on lui présente une signature valide.
NOP
Après la vérification de licence, le registre R4 contient un booléen indiquant si la vérification a échoué ou non. Si elle a échoué, le code saute vers un gestionnaire d'erreur et interrompt l'exécution :
En remplaçant l'instruction Branch if Equal par un NOP, nous nous assurons que ce cas d'erreur n'est jamais atteint, et le code continue de s'exécuter après les instructions CMP/NOP :
Comme ce code est en ARM (little endian), le NOP utilisé était :
00 00 A0 E1 MOV R0, R0
Processus de mise à jour du firmware
Maintenant que nous avons une version patchée du binaire principal, nous pouvons l'empaqueter pour l'installer sur le PowerVision. Dans la Partie 1, nous avions analysé le contenu des fichiers PVU (PowerVision Update), ils contiennent :
- PVU_TYPE : firmware, recovery, tunes...
- PVU_FILE : fichier chiffré contenant
- NEW_ROOTFS.bin : système de fichiers squashfs, lecture seule
- NEW_KERNEL.bin : mise à jour du noyau
- PVU_CERT : signature du fichier de mise à jour (aïe)
Dans le code, nous avons découvert qu'il existe un contrôle d'intégrité sur les mises à jour de firmware uploadées. Les bases de données chiffrées contiennent une entrée UPDATES_PK, utilisée pour déchiffrer le fichier PVU_CERT. Bien sûr, nous aurions pu reconstruire une base de données embarquant notre propre clé, ou même contourner la vérification par micropatching. Mais encore une fois, nous sommes flemmards. En reversant la fonction de mise à jour du firmware, nous avons découvert un outil très utile :
0015B3A4 "ubiupdatevol /dev/ubi0_0 /flash/NEW_ROOTFS.BIN"
En nous connectant avec le shell obtenu précédemment, nous avons pu confirmer l'existence et l'utilité du binaire ubiupdatevol. Comme son nom l'indique, il est utilisé pour écrire directement sur les périphériques /dev/ubi, qui contiennent le système de fichiers squashfs que nous voulons patcher. La seule chose dont nous avons besoin est un moyen d'uploader un fichier sur le PowerVision. Et ça aussi, nous l'avons :
def send_file(path, content):
pvlink = CDLL("./PVLink.dll")
sendfile = pvlink.PVSendFile
r = sendfile(path, len(content), content)
if r != 0:
print("Error")
else:
print("Wrote /flash/storage/rootfs_patch.sqsh")
if __name__ == "__main__":
f = open("rootfs_patch.sqsh", "rb")
print("running...")
send_file("updates:rootfs_patch.sqsh", f.read())
Il ne nous reste plus qu'à nous connecter et utiliser l'outil ubiupdatevol avec le fichier patché que nous avons uploadé. Cela contourne la vérification de signature, puisque nous écrivons maintenant directement sur l'appareil !
Pour résumer, les étapes sont :
- Patcher le binaire principal
- Copier le binaire dans l'arborescence squashfs, et conserver les attributs du fichier de destination
- mksquashfs
- Envoyer le squashfs patché vers l'appareil via l'API PVLink
- Se connecter via UART et écrire sur le volume UBI
Après un peu de mise en forme, on peut voir que le PowerVision affiche bien le message « Tuning: Not Locked to vehicle » :
Comme certains d'entre vous l'ont peut-être deviné, ce message ne signifie pas nécessairement que nous avons contourné la vérification des verrous. En fait, il prouve seulement que nous sommes capables de modifier le firmware. Pour prouver que nos modifications ont des implications plus profondes, nous pouvons récupérer les informations de l'appareil avec pvinfo :
def do_soap(request):
pvlink = CDLL("./PVLink.dll")
dosoap = pvlink.PVDoSoapEx
dosoap.argtypes = [c_int, c_char_p, POINTER(c_char_p), c_char_p, c_int, c_int, c_int, c_int]
ref = c_char_p()
a = 0
point = ''
dosoap(len(request), request, byref(ref), point, 1, 5, a, a)
return ref.value
def reqtype(typestring):
req = "<request><type>" + typestring + "</type><ver>1</ver><summary></summary></request>"
return do_soap(req)
if __name__ == "__main__":
print(reqtype("pvinfo"))
Et voici une comparaison des deux réponses (avant et après patching) :
Conclusion
Le temps que nous avons consacré à cette analyse parle de lui-même (plus d'un an). Les protections en place sont assez robustes et bien pensées, et l'architecture globale du firmware est complexe et intéressante. Le PowerVision 1 a été un adversaire digne de ce nom, et nous avons pris beaucoup de plaisir à travailler dessus.
Cette dernière partie vise principalement à montrer des exemples de contournements logiciels (pour les licences, mais aussi les fonctionnalités de sécurité comme les fonctions anti-VM/anti-débogage) qui sont génériques. Le fait que nous ayons pu les réaliser sur cet appareil n'est pas une vulnérabilité en soi, mais simplement les possibilités résultant de deux facteurs :
- Être capable de lire et comprendre le firmware
- Être capable d'écrire le firmware
Nous encourageons les lecteurs intéressés par ce type de travail à signaler les vulnérabilités et contournements qu'ils trouvent aux éditeurs logiciels concernés. Le bon travail est toujours valorisé !
AVERTISSEMENT : Modifier le firmware du PV1 laissera l'appareil dans un état instable. Cette preuve de concept n'est pas destinée à être reproduite, et ne devrait JAMAIS être utilisée sur une vraie moto. Cela peut endommager l'ECU, et brickera très probablement le PowerVision.
Références
Je tiens à remercier tous les lecteurs, nous avons eu d'excellents retours et des discussions passionnantes au cours de l'année écoulée sur ce sujet. Restez classe, amateurs de cybersécurité !
Lire la Partie 1 · Lire la Partie 2
Besoin d'un audit de sécurité ou d'un accompagnement sur mesure ?
Découvrir nos services →