Epsilon 16 est sorti il y a deux jours. Beaucoup de personnes veulent essayer de comprendre comment fonctionnent le bootloader et le kernel de cette nouvelle version, qui change complètement la manière de fonctionner d'Epsilon. Nous allons donc voir comment mettre en place un environnement de Reverse Engineering prêt pour Epsilon 16 avec Ghidra.
Le Reverse Engineering est dans une zone grise juridique. La béta d'Epsilon 16 étant mise à disposition sans licence, elle doit être considérée comme étant sous strong copyright. Si vous découvrez des failles dans le kernel, nous vous encourageons à ne pas les rendre publiques, mais à contacter redgl0w, quentinguidee, moi-même, M4x1m3 ou critor.
1 - Ghidra, c'est quoi ?
Ghidra est un logiciel de Software Reverse Engineering libre, développé par la NSA. Il intègre notamment un désassembler supportant l'architecture Cortex-M (l'architecture matérielle de la Numworks) et un décompileur très puissant. Pour windows, l'installation se passe tout simplement depuis le site officiel. L'outil nécessite l'installation de Java. Pour linux, ghidra est inclue dans les dépôts de pas mal de distros.
2 - Obtention des binaires du firmware
Les binaires sont disponible au format DFU depuis le site de Numworks. Le format DFU étant un format conteneur, il faut effectuer une manipulation afin d'obtenir des fichiers .bin que l'on peut charger dans Ghidra.
Je vous mets donc à disposition un petit script python pour ça :
Show/Hide spoilerAfficher/Masquer le spoiler
- Code: Select all
#!/bin/env python3
import struct
import sys
def read(fo, fw):
l = struct.calcsize(fo);
return struct.unpack(fo, fw.read(l));
def read_header(fw):
magik, = read("5s", fw)
if (magik != b"DfuSe"):
return False, "Invalid magik number!", 0, 0
version, = read("B", fw)
if (version != 1):
return False, "Unsupported file version!", 0, 0
imagesize, = read("<I", fw);
imagecount, = read("B", fw)
return True, "OK!", imagecount, imagesize
def read_target(fw):
magik, = read("6s", fw)
if(magik != b"Target"):
return False, "Invalid magik number!", "", 0, 0
alternate, = read("B", fw)
named, = read("<I", fw)
name, = read("255s", fw)
name = name.split(b'\0', 1)[0].decode("utf-8") if named != 0 else ""
size, = read("<I", fw)
elements, = read("<I", fw)
return True, "OK!", name, size, elements
def read_element(fw):
address, = read("<I", fw)
size, = read("<I", fw)
data, = read(str(size) + "s", fw)
return address, size, data
internal_size = 64 * 1024
internal_addr = 0x8000000
internal = bytearray([255] * internal_size)
external_size = 8 * 1024 * 1024
external_addr = 0x90000000
external = bytearray([255] * external_size)
with open("firmware.dfu", "rb") as fw:
ok, message, count, size = read_header(fw);
if not ok:
print("Error reading header:", message)
sys.exit(1)
print("Found", count, "image(s), total size:", size)
for i in range(count):
ok, message, name, size, elements = read_target(fw);
if not ok:
print("Error reading image:", message)
sys.exit(2)
print(" Image", i)
print(" Found", elements, "element(s), total size:", size)
for j in range(elements):
addr, size, data = read_element(fw)
if (addr >= internal_addr and addr + size <= internal_addr + internal_size):
print(" Element", j, ": Address:", hex(addr), "Size:", hex(size), "<internal>")
for k in range(size):
internal[k + addr - internal_addr] = data[k]
elif (addr >= external_addr and addr + size <= external_addr + external_size):
print(" Element", j, ": Address:", hex(addr), "Size:", hex(size), "<external>")
for k in range(size):
external[k + addr - external_addr] = data[k]
else:
print(" Element", j, ": Address:", hex(addr), "Size:", hex(size), "<unknown>")
print("Saving...")
with open("internal.bin", "wb") as f:
f.write(internal);
with open("external.bin", "wb") as f:
f.write(external);
print("Done!")
Il s'utilise en le mettant dans le même dossier que le fichier .dfu, nommé
firmware.dfu
. Il produit deux fichiers binaires, internal.bin
et external.bin
, qui correspondent respectivement à la flash interne et à la flash externe.3 - Installation de SVD-Loader
Nous allons maintenant installer le plugin SVD-Loader, qui va nous permettre de charger la description de tous les registres du processeur dans Ghidra, afin de grandement faciliter la compréhension du code. Commençons par télécharger le plugin. Nous devons extraire le contenue du dossier
SVD-Loader-Ghidra-master
de l'archive dans le dossier ghidra_scripts
dans votre dossier utilisateur (créez le si il n'existe pas). Pensez aussi à télécharger le fichier STM32F730.sdv, il nous servira plus tard.4 - Création du projet
Nous pouvons maintenant ouvrir Ghidra, et créer un nouveau projet, en allant dans File -> New Project. Nous choisirons un "Non-Shared Project", un dossier adéquat et l'appellerons Epsilon16, par exemple. Nous pouvons maintenant glisser le fihcier
internal.bin
dans le projet, pour le charger dans Ghidra. Le format se mettra normalement en "Raw Binary". Cliquons sur "..." à côté de Langage. Une fenêtre s'ouvre. Dans filter, taper "cortex", et choisir la ligne "ARM Cortex 32 Little", et faire OK. Cliquer sur "Options...", mettre "internal" dans "block name", et faire "OK", puis valider l'ajout (encore "OK).Nous pouvons maintenant double cliquer sur "internal" pour ouvrir internal. Le désassembleur se lance, et vous propose d'analyser le fichier, dites non, car nous devons d'abord ajouter les miroirs d'internal et external.
4.1 - Memory Map
Nous allons ajouter external.bin au projet, en allant dans "File->Add to program...", et en choisissant notre fichier
externa.bin
. Allons dans "Options...", nommons le block "external" et définissons la Base Address à 90000000, puis faisons "OK" et validons l'ajout.Nous allons maintenant ouvrir l'éditeur de mappage mémoire ("Window->Memory Map", ou l'icon en forme de stick de RAM).
Chez moi, internal s'est chargé avec comme nom de block "ram", si c'est votre cas, double-cliquez dessus et renommez le internal
Commençons par ajouter les miroirs d'internal. Boutton "+", mettre "Block Types" à "Byte Mapped" et laisser l'adresse source à 0. nommer le block "internal.mirror1", adresse de début mettre "200000" et taille mettre "0x10000". Cocher Read, Write et Execute. Faire la même chose en nommant "internal.mirror2" avec adresse de début "8000000", même taille et même coches. Ajoutons aussi la RAM, nommer le block "sram", adresse de début "20000000", taille à "0x40000". Cocher Read, Write et Execute et Uninitialized. Ajoutons aussi la mémoire OTP, block nommé "otp", adresse de début à "1FF07800", taille à "0x210". Cocher Read, Write et Uninitialized.
Vous devriez maintenant avoir cette configuration :
Si c'est le cas, bravo. Sinon, jouez au jeu des 7 différences. Bonne chance
4.2 - SVD-Loader
Nous allons maintenant utiliser SDV-Loader pour charger le fichier SDV téléchargé plus haut. Ouvrons le gestionnaire de scripts (Window->Scripts manager, ou le bouton flèche blanc sur verte), et cliquons sur le dossier "leveldown-security", à gauche. Double-cliquons ensuite sur "SDV6Loader.py" pour l’exécuter. Il va vous demander un fichier SVD, choisissez le fichier STM32F730.svd téléchargé avant. SVD-Loader va charger le fichier. Cela peut prendre jusqu'à deux minutes. Vous pouvez fermer le Script Manager.
4.3 - Analyse
Maintenant que tout est prêt, nous allons demander à Ghidra d'analyser le firmware. Cliquer sur "Analysis->Auto analyse...". Cochez, en plus de ce qui l'est déjà, "Decompiler Parameter ID", et cliquer sur Analyze. Cette étape peut prendre beaucoup de temps, vue la taille du firmware.
Une fois que l'analyse est terminée, vous pouvez commencer à travailler. Référez vous à la documentation de Ghidra pour savoir comment vous en servir.
5 - Ce qui a déjà été compris
Grâce à mes recherches sur ce firmware, j'ai pu déduire plusieurs choses :
5.1 - Bootloader
Le bootloader n'est présent qu'une fois, dans la flash interne. Son rôle est d'initialiser les périphériques et la board, puis de charger le kernel. Il vérifie une signature (algorithme que je n'ai pas encore reconnu) et charge le kernel, sinon initialise l'écran et charge un stack USB. Le kernel est présent deux fois, en flash externe. Le bootloader jump sur le kernel une fois qu'il a vérifié sa signature, toujours en mode privilégié.
5.2 - Kernel
Le kernel est présent deux fois en flash externe, une fois en 0x90010000 et une autre fois en 0x90410000. La flash est donc coupée en deux. Le kernel s'accompagne de son userspace, le premier étant en 0x90050000, et le deuxième en 0x90450000. Le rôle du kernel est d'initialiser l'écran et de vérifier la signature de l'userland, de passer en mode user et de jump dedans. Il est aussi responsable de l'affichage des messages en cas de firmware tiers ou d'apps externes.
5.3 - Similitudes avec Epsilon 15
Le bootloader et le kernel intègrent tout les deux Ion. Il n'est pas improbable que, lorsqu'ils seront distribués sous formes de sources, les deux soient inclus dans le repo principal d'epsilon. On peut reconnaître plusieurs fonctions d'ion, notamment Ion::Board::init tout au début du bootloader.
5.4 - Cryptographie
L'algorithme utilisé est ED25519. C'est donc de la vérification de signature via SHA512 + cryptographie asymétrique par courbes éliptiques.
6 - Difficultés rencontrés
Le firmware a été compilé avec l'argument
-Oz
, le code est donc très optimisé pour le gain de place, ce qui le rend imbitable. Il m'a fallu plusieurs heures pour trouver la fonction qui gère la vérification de signature dans le bootloader. Je ne partagerais pas les adresses, pour des raisons de droits d'auteurs.Le code du bootloader et du kernel sont de très bas niveau, interagisseant directement avec les registres du proco. Armez vous de la documentation de celui-ci (trouvable sur le site de Numworks) et de beaucoup de patience et de café.
Bon courage.