Reverse engineering logiciel d'un drone Parrot
Analyse du firmware d'un drone parrot dans le cadre du projet CPEFly
Le
Cet article fait suite à Reverse engineering des moteurs d'un drone Parrot (CPEFly), disponible ici.
L'objectif de cette partie est d'en apprendre plus sur le fonctionnement du protocole de contrôle des moteurs. Pour rappel, de ce qu'on l'on sait, les moteurs sont contrôlés par un programme sur la carte mère (processeur ARMv8) et les commandes sont transmises par une liaison UART avec un protocole propriétaire Parrot inconnu et non documenté.
Extraction du firmware
La première étape consistait à obtenir le firmware de l'AR DRONE 2, afin de pouvoir l'analyser.
Un firmware est un logiciel spécial de bas niveau intégré directement au matériel d'un appareil (comme une puce ROM, EPROM, ou Flash). Il fournit les instructions essentielles qui permettent au matériel de fonctionner, de communiquer avec d'autres composants.
Il existe deux méthodes pour acquérir un firmware :
- L'extraction physique directement depuis les puces de mémoire du matériel.
- L'extraction à partir d'un fichier de mise à jour (update file).
J'ai opté pour la seconde option. Il est beaucoup plus simple et rapide de télécharger un fichier de mise à jour directement en ligne si celui-ci est disponible que d'effectuer une extraction matérielle complexe. Encore faut-il qu'il soit facile d'en extraire le firmware.
Les fichiers relatifs aux mises à jour peuvent être téléchargés directement et gratuitement sur le site officiel de Parrot.

Le fichier de mise à jour est au format PLF (d'après son extension), un autre format propriétaire Parrot.
Étude du format de fichier PLF
La première étape consiste à vérifier si le contenu du fichier est chiffré.
En extrayant les chaines de caractères présentes dans le fichier (via la commande strings), on obtient une série de chaines de caractères lisibles et bien formée, ce qui indique que le firmware n'est pas entièrement chiffré.
21 stream tcp nowait root ftpd ftpd -w /data/video
5551 stream tcp nowait root ftpd ftpd -w /update
On peut vérifier si le fichier n'est pas chiffré en analysant son entropie avec binwalk.
L'entropie H d'un fichier mesure la quantité minimale d'information (en bit) nécessaire pour coder un symbole (un octet) de ce fichier. Elle est calculée en fonction de la fréquence d'apparition de chaque octet dans le fichier :
Où pi est la probabilité (la fréquence) d'apparition de l'octet de valeur i, et N est le nombre total de valeurs d'octets possibles (256 pour 8 bits).
$ binwalk -E ardrone_2_update.plf
DECIMAL HEXADECIMAL ENTROPY
--------------------------------------------------------------------------------
0 0x0 Falling entropy edge (0.712636)
12288 0x3000 Rising entropy edge (0.961492)
2068480 0x1F9000 Falling entropy edge (0.733456)
2195456 0x218000 Rising entropy edge (0.991415)
4470784 0x443800 Rising entropy edge (0.976645)
5439488 0x530000 Rising entropy edge (0.976732)
10119168 0x9A6800 Rising entropy edge (0.992391)

On remarque que le fichier a une entropie forte (proche de 1), ce qui signifie qu'une grande partie de son contenu est chiffré ou compressé. On peut vérifier si il s'agit de compression en réutilisant l'outil binwalk, cette fois pour détecter des données compressées en se basant sur les magic bytes.
Les Magic Bytes sont des séquences d'octets spécifiques (des signatures) situées généralement au début d'un fichier et utilisées pour identifier de manière catégorique son format (y compris les types de compression et les systèmes de fichiers intégrés), indépendamment de l'extension.
binwalk ardrone2_update.plf

Binwalk détecte beaucoup de données compressées, ce qui est un très bon signe. Si le firmware avait été chiffré (ce qui est courant), il aurait fallu trouver un moyen de le déchiffrer, ce qui aurait considérablement complexifié la suite de la tâche.
Extraction d'un premier filesystem
L'outil binwalk a révélé que le fichier de mise à jour contenait beaucoup de données compressées, dont deux zImage.
zImage is compressed version of the Linux kernel image that is self-extracting.
J'ai commencé par extraire chaque zImage et les données compressées qui les suivaient dans deux fichiers distincts, PLF_PART_1 et PLF_PART_2, en utilisant les offset indiqué par binwalk.

dd if=ardrone2_update.plf bs=1 skip=772 count=2179912 of=PLF_PART_1 # (1)
dd if=ardrone2_update.plf bs=1 skip=2180684 of=PLF_PART_2 #(2)
J'ai ensuite extrait les deux premières données compressées (GZIP) de la première partie du fichier (PLF_PART_1) dans deux fichiers distincts (PLF_PART_1_gz_1.gz et PLF_PART_1_gz_2.gz). Je les ai décompressés individuellement puis je les ai concaténés.
# Extraction des deux premiers GZIP
dd if=../PLF_PART_1 bs=1 skip=13069 count=1183519 of=PLF_PART_1_gz_1.gz
dd if=../PLF_PART_1 bs=1 skip=1196588 of=PLF_PART_1_gz_2.gz
# Décompression des données
gunzip PLF_PART_1_gz_1.gz
gunzip PLF_PART_1_gz_2.gz
# Concaténation dans un nouveau fichier
cp PLF_PART_1_gz_1 PLF_PART_1_decompressed
cat PLF_PART_1_gz_2 >> PLF_PART_1_decompressed
J'ai ensuite lancé l'outil binwalk en mode extraction sur le fichier obtenu et j'ai pu obtenir un filesystem.
binwalk -e PLF_PART_1_decompressed


Le système de fichiers extrait s'est avéré être minimaliste, ne contenant qu'une structure de base et peu de fichiers exécutables ou de configurations utiles pour une analyse approfondie.
La présence de deux images de noyau (zImage) et la nature minimaliste/rudimentaire du premier système de fichiers corrobore l'hypothèse d'un système d'amorçage initial (staging kernel) ou d'un système de recovery, dont la fonction est de préparer l'environnement pour le système d'exploitation principal et son filesystem associé, situé dans la deuxième partie du fichier.
Le contenu d'intérêt se situe donc très probablement dans la deuxième partie du fichier.
Extraction du deuxième filesystem
Comme la deuxième partie du fichier contient beaucoup de données compressées, j'ai développé un petit script Python dont le rôle est de récupérer les données compressées et de les décompresser. En observant les fichiers ainsi générés, je m'aperçois qu'ils ont tous un chemin en guise d’entête, suivi de 12 octets nuls (00).

J'en déduis que l'entête indique l'emplacement du fichier, et qu'elle est suivie du contenu de celui-ci. J'ai donc adapté mon script en conséquence afin de reconstruire l'arborescence complète.
import re
import subprocess
import os
os.system("rm files/*")
output = subprocess.check_output(["binwalk","../PLF_PART_2"]).decode()
match = re.findall(r"([0-9]+) 0x([0-9A-F]+) gzip compressed data, from Unix, last modified:", output)
offsets = [offset for (offset,offset_hex) in match]
for i in range(0,len(offsets)-1):
start_offset = int(offsets[i])
end_offset = int(offsets[i+1])
command = f"dd if=../PLF_PART_2 bs=1 skip={start_offset} count={end_offset-start_offset} of=files/file{i}.gz"
os.system(command)
os.system(f"gunzip files/file{i}.gz")
f = open(f"files/file{i}","rb")
file_data = f.read()
f.close()
filename = file_data.split(b"\x00")[0].decode()
file_real_data = file_data[len(filename)+13:]
print(filename)
folders = filename.split("/")[:-1]
for i in range(len(folders)+1):
print("fs/" + "/".join(folders[:i]))
if not os.path.exists("fs/" + "/".join(folders[:i])):
os.makedirs("fs/" + "/".join(folders[:i]))
f = open("fs/" + filename,"wb")
f.write(file_real_data)
f.close()
Après exécution du script, on arrive à reconstruire une arborescence complète, comme on peut le voir sur la capture ci-dessous.

Celle-ci contient plusieurs programmes et scripts Shell.
Analyse du firmware (motherboard)
On a désormais accès à un système de fichier contenant un grand nombre d'éléments.
Afin d'identifier les programmes / scripts shells d'intérêt, c'est-à-dire ceux en lien avec le pilotage des moteurs, je liste uniquement les fichiers qui contiennent la chaine de caractère motor.
grep -lF "motor" \*

J'obtiens un exécutable au format ELF, "program.elf".
Reverse engineering du programme principal
Le fichier program.elf est un binaire au format ELF destiné aux architectures ARM. Afin de l'analyser, j'ai utilisé l'outil « Ghidra », développé par la NSA.
Ghidra est un outil de rétro-ingénierie logiciel (SRE), gratuit et open-source, utilisé pour l'analyse de code binaire via le désassemblage et la décompilation.
L'avantage de Ghidra est qu'il supporte beaucoup d'architecture (dont l'ARM), qu'il génère du pseudocode C plus facile à interpréter que l'assembleur et qu'il est gratuit.
Binaire strippé
Un point important à noter est que le binaire est strippé, ce qui complexifie l'analyse.
Un binaire strippé est un fichier exécutable dont on a retiré les tables de symboles et les informations de débogage.
J'ai donc renommé dans Ghidra chaque fonction au fur et à mesure de mon analyse, en me basant sur leur rôle (du moins l'idée que j'avais de leur rôle).
Dans la suite de cette étude, j’appellerai les fonctions par le nom que je leur ai donné, afin de rendre la lecture plus facile.
Fonctionnement général
Comme le programme contient beaucoup de code, j'ai commencé par son point d’entrée. J'ai ensuite suivi les appels aux fonctions afin de remonter jusqu'à une fonction qui s'apparente à la fonction main. Celle-ci est passée en argument d'une autre fonction __libc_start_main, qui a pour rôle d'initialiser l'environnement de la bibliothèque C et de gérer les préparatifs nécessaires (comme les arguments et les constructeurs) avant d'appeler la fonction principale du programme, main().

Cela permet de déterminer sans trop de doute l'offset de la fonction main du programme.
|
Fonction main() |
Fonction run() |
|
Fonction Setup_hardware() |
|
La fonction main appelle une fonction setup_hardware qui initialise l'ensemble des composants du drone (GPS, Moteurs...) puis lance un fonction run qui itère à l'infini. La fonction setup_hardware initialise un protocole qui s'appelle BLC et semble être le protocole de gestion des moteurs.
En analysant les références à la chaine de caractère "BLC", on détermine un ensemble des fonctions en lien avec le protocole.
|
|
|
Initialisation des moteurs
Il y a une fonction qui a retenu mon attention, que j'ai baptisée BLC_checkup_motor. Elle prend en argument le numéro d'un moteur, le firmware du moteur, le checksum du firmware et deux autres variables.
/* Resultat des commandes, chaque element indique le resultat d'une commande */
*motor_command_check = 0;
motor_command_check[1] = 0;
motor_command_check[2] = 0;
motor_command_check[3] = 0;
/* Preparation des commandes */
command_E0_check_motor_state[0] = 0xe0;
uart_command_response = 0x40;
command_A1_set_motor_ok[0] = 0xa1;
command_40_get_motor_version_2[0] = 0x40;
/* Appelle de la commande 1: E0 */
print_blc(7,"BLC call for motor %d\n",motor_number);
intermediate_buffer[0] = 1;
BLC_send(command_E0_check_motor_state,intermediate_buffer,0);
intermediate_buffer[0] = 1;
read_is_ok = BLC_recv(&uart_command_response_E0,intermediate_buffer,0);
motor_command_check[3] = 0;
read_is_ok_ = read_is_ok & 0xffff;
if ((read_is_ok_ == 0) &&
(uart_command_response = (uint)uart_command_response_E0, uart_command_response != 0x50)) {
if (uart_command_response != 0) {
read_is_ok_ = 0xffff;
read_is_ok = 0xffffffff;
goto need_reflash;
}
}
Cette première partie du code envoie la commande 0xE0 au moteur et récupère la réponse. Si elle est égale à 0x50 ou différente de 0x00, le code saute vers un bout de code qui est responsable du flashage du firmware sur la motorboard.
else {
need_reflash:
if (firmware_checksum == 0) {
check_and_flash_motor_if_needed:
read_is_ok_ = uart_command_response;
if (firmware_ptr == 0) {
read_is_ok_ = read_is_ok & 0xffff;
}
uart_command_response = read_is_ok & 0xffff;
if (firmware_ptr == 0) goto something_is_not_ok;
check_and_flash_motor_if_needed_2:
if (uart_command_response != 0) goto flash_and_start_failed;
uart_command_response = 0;
if ((param_4 & 2) != 0) {
print_blc(7,"BLC start flash\n");
iVar5 = 0;
/* Flashing the firmware */
do {
command_flash_71 = 0x71;
firmware_ptr_+_40 = firmware_ptr + 0x40;
intermediate_buffer_1 = 1;
BLC_send(&command_flash_71,&intermediate_buffer_1,0);
intermediate_buffer_1 = 0x40;
BLC_send(firmware_ptr,&intermediate_buffer_1,0);
intermediate_buffer_1 = 1;
iVar3 = BLC_recv(&command_0x91_get_checksum,&intermediate_buffer_1,0);
iVar5 = iVar5 + 2;
firmware_ptr = firmware_ptr + 0x80;
if ((iVar3 == 0) || ((char)command_0x91_get_checksum != 'p')) {
flash_aborted:
read_is_ok = 0xffffffff;
print_blc(7,"BLC flash aborted\n");
goto flash_done;
}
command_flash_71 = 0x71;
intermediate_buffer_1 = 1;
BLC_send(&command_flash_71,&intermediate_buffer_1);
intermediate_buffer_1 = 0x40;
BLC_send(firmware_ptr_+_40,&intermediate_buffer_1,0);
intermediate_buffer_1 = 1;
iVar3 = BLC_recv(&command_0x91_get_checksum,&intermediate_buffer_1,0);
if ((iVar3 == 0) || ((command_0x91_get_checksum & 0b11111111) != 0x70))
goto flash_aborted;
} while (iVar5 != 120);
read_is_ok = 0;
print_blc(7,"BLC flash done\n");
Cette partie du code s'occupe d'envoyer le nouveau firmware sur le motorboard.
- Dans une itération, il envoie d'abord le code 0x71 pour annoncer qu'il va envoyer de la donnée, puis il envoie 0x40 octets du firmware. Il vérifie ensuite que la réponse est égale à 0x70 avant de répéter une nouvelle fois l'opération.
- Il itère 120/2=60 fois. En effet, le compteur s’incrémente de deux à chaque itération, ce qui est logique, car il envoie deux blocs de 0x40=64 octets en une itération. Au total, 7680 octets sont transférés.
flash_done:
memset(&multi_usage_buffer,0,0x78);
if (firmware_checksum == 0) {
read_is_ok_ = 0xffff;
read_is_ok = 0xffffffff;
uart_command_response = read_is_ok_;
}
else {
print_blc(7,"BLC verify\n");
multi_usage_buffer_2 = CONCAT31(multi_usage_buffer_2._1_3_,0x91);
intermediate_buffer_1 = 1;
BLC_send(&multi_usage_buffer_2,&intermediate_buffer_1,0);
intermediate_buffer_1 = 0x78;
BLC_recv(&multi_usage_buffer,&intermediate_buffer_1,0);
compteur = 0;
do {
if (((*(char *)(firmware_checksum + compteur) !=
*(char *)((int)&multi_usage_buffer + compteur)) ||
(*(char *)(firmware_checksum + compteur + 1) !=
*(char *)((int)&multi_usage_buffer + compteur + 1))) ||
((*(char *)(firmware_checksum + compteur + 2) !=
*(char *)((int)&multi_usage_buffer + compteur + 2) ||
(*(char *)(firmware_checksum + compteur + 3) !=
*(char *)((int)&multi_usage_buffer + compteur + 3))))) {
read_is_ok = 0xffffffff;
print_blc(7,"BLC verify FAILED - page %d \n");
read_is_ok_ = 0xffff;
uart_command_response = read_is_ok_;
goto something_is_not_ok;
}
compteur = compteur + 4;
} while (compteur != 0x78);
read_is_ok_ = read_is_ok & 0xffff;
print_blc(7,"BLC verify OK\n");
uart_command_response = read_is_ok_;
}
goto something_is_not_ok;
}
Il vérifie ensuite si la fonction a été appelée avec le checksum du firmware, et si c'est le cas elle envoie la commande 0x91 (qui demande au motorboard d'envoyer le checksum de son firmware) et il le compare avec le sien.
}
start_motor:
motor_command_check[3] = 1;
intermediate_buffer[0] = 1;
BLC_send(command_A1_start_motor,intermediate_buffer,0);
local_104 = FUN_000186a0;
intermediate_buffer[0] = 4;
multi_usage_buffer = 0;
BLC_recv(&uart_command_response_E0,intermediate_buffer,&multi_usage_buffer);
if (uart_command_response_E0 != 0xa0) {
print_blc(7,"BLC start FAILED on motor %d\n",motor_number);
read_is_ok = 0xffffffff;
goto flash_and_start_failed;
}
}
Il initie les moteurs en envoyant la commande 0xA1. Si le résultat n'est pas 0xA0, c'est que l'initialisation n'a pas fonctionné.
else {
if ((read_is_ok_ == 0) && ((param_4 & 1) != 0)) {
print_blc(7,"BLC check BLC memory corruption on motor %d\n",motor_number);
memset(&multi_usage_buffer,0,120);
print_blc(7,"BLC verify\n");
command_flash_71 = 0x91;
multi_usage_buffer_2 = 1;
BLC_send(&command_flash_71,&multi_usage_buffer_2,0);
multi_usage_buffer_2 = 0x78;
BLC_recv(&multi_usage_buffer,&multi_usage_buffer_2,0);
compteur = 0;
do {
if ((((*(char *)(firmware_checksum + compteur) !=
*(char *)((int)&multi_usage_buffer + compteur)) ||
(*(char *)(firmware_checksum + compteur + 1) !=
*(char *)((int)&multi_usage_buffer + compteur + 1))) ||
(*(char *)(firmware_checksum + compteur + 2) !=
*(char *)((int)&multi_usage_buffer + compteur + 2))) ||
(*(char *)(firmware_checksum + compteur + 3) !=
*(char *)((int)&multi_usage_buffer + compteur + 3))) {
print_blc(7,"BLC verify FAILED - page %d \n");
print_blc(7,"BLC memory corrupted on motor %d\n",motor_number);
param_4 = param_4 | 2;
print_blc(7,"BLC erase memory on motor %d\n",motor_number);
intermediate_buffer[0] = 1;
command_61 = 0x61;
BLC_send(&command_61,intermediate_buffer,0);
intermediate_buffer[0] = 1;
read_is_ok = BLC_recv(&uart_command_response_E0,intermediate_buffer,0);
uart_command_response = (uint)uart_command_response_E0;
if (uart_command_response == 0x60) goto check_and_flash_motor_if_needed;
read_is_ok_ = 0xffff;
read_is_ok = 0xffffffff;
goto error_in_erasing_firmware;
}
compteur = compteur + 4;
} while (compteur != 120);
print_blc(7,"BLC verify OK\n");
}
error_in_erasing_firmware:
uart_command_response = read_is_ok_;
if (firmware_ptr != 0) goto check_and_flash_motor_if_needed_2;
uart_command_response = read_is_ok & 0xffff;
something_is_not_ok:
if (read_is_ok_ == 0) goto start_motor;
}
if (uart_command_response != 0) goto flash_and_start_failed;
}
Le code ci-dessus s’exécute si la motorboard n'a pas demandé de flash un nouveau firmware. Il envoie la commande 0x91 pour récupérer le checksum du firmware actuel de la motorboard. Si le checksum diffère de celui de la carte mère, alors la commande 0x61 est envoyée, ce qui a pour effet d'effacer le firmware actuel du motorboard. Si la commande s'est bien déroulée, la valeur 0x60 doit être retournée par la motorboard et le code jump vers le flashage du nouveau firmware.
make_sleep(100);
motor_number_4LSB[0] = (byte)motor_number & 0b00001111;
intermediate_buffer[0] = 1;
BLC_send(motor_number_4LSB,intermediate_buffer,0);
intermediate_buffer[0] = 1;
BLC_send(command_40_get_motor_version_2,intermediate_buffer,0);
intermediate_buffer[0] = 2;
read_is_ok = BLC_recv(&uart_command_response_E0,intermediate_buffer,0);
if ((read_is_ok & 0xffff) == 0) {
*(byte *)((int)motor_command_check + 9) = uart_command_response_E0;
*(byte *)((int)motor_command_check + 10) = local_6f;
if ((uint)local_6f + (uint)uart_command_response_E0 * 256 < 263) {
intermediate_buffer[0] = 6;
}
else {
intermediate_buffer[0] = 9;
}
read_is_ok = BLC_recv(motor_command_check,intermediate_buffer,0);
if ((read_is_ok & 0xffff) == 0) {
motor_command_check[3] = 2;
if (*(short *)motor_command_check == -1) {
subversion_motor = 1;
uVar3 = 6;
uVar5 = 10;
motor_number_cpy = 3;
*(undefined *)motor_command_check = 3;
*(undefined *)((int)motor_command_check + 1) = 0;
uVar1 = 10;
*(undefined *)((int)motor_command_check + 2) = 1;
subversion_motor_hardware = 6;
*(undefined *)((int)motor_command_check + 3) = 1;
*(undefined *)(motor_command_check + 1) = 6;
*(undefined *)((int)motor_command_check + 5) = 10;
*(undefined *)((int)motor_command_check + 6) = 1;
*(undefined *)((int)motor_command_check + 7) = 6;
*(undefined *)(motor_command_check + 2) = 10;
version_motor = 0;
version_motor_hardware = subversion_motor;
uVar4 = subversion_motor;
}
else if (*(short *)motor_command_check == 0) {
uVar1 = *(undefined *)((int)motor_command_check + 5);
subversion_motor_hardware = *(undefined *)(motor_command_check + 1);
subversion_motor = *(undefined *)((int)motor_command_check + 2);
uVar3 = *(undefined *)((int)motor_command_check + 7);
uVar5 = *(undefined *)(motor_command_check + 2);
motor_number_cpy = 5;
*(undefined *)motor_command_check = 5;
*(undefined *)((int)motor_command_check + 1) = 0;
version_motor = 0;
version_motor_hardware = *(undefined *)((int)motor_command_check + 3);
uVar4 = *(undefined *)((int)motor_command_check + 6);
}
else {
subversion_motor_hardware = *(undefined *)(motor_command_check + 1);
uVar1 = *(undefined *)((int)motor_command_check + 5);
motor_number_cpy = *(undefined *)motor_command_check;
version_motor = *(undefined *)((int)motor_command_check + 1);
subversion_motor = *(undefined *)((int)motor_command_check + 2);
uVar3 = *(undefined *)((int)motor_command_check + 7);
uVar5 = *(undefined *)(motor_command_check + 2);
version_motor_hardware = *(undefined *)((int)motor_command_check + 3);
uVar4 = *(undefined *)((int)motor_command_check + 6);
}
print_blc(7,
"BLC motor %d soft version %d.%d, hard version %d.%d, supplier %d.%d, lot number %02 d/%02d, FVT1 %02d/%02d/%02d\n"
,motor_number,*(undefined *)((int)motor_command_check + 9),
*(undefined *)((int)motor_command_check + 10),motor_number_cpy,version_motor,
subversion_motor,version_motor_hardware,subversion_motor_hardware,uVar1,uVar4,uVar3,
uVar5);
return read_is_ok;
}
}
Cette dernière partie du code sélectionne le moteur (commande unicast) en envoyant comme commande le numéro du moteur, puis récupère sa version en envoyant la commande 0x40. Il parse ensuite la version pour l'afficher sur le terminal de la carte mère.
On en déduit donc le résultat suivant:
| Nom de la commande | Valeur | Description | Valeur de retour |
|---|---|---|---|
| Récupérer le statut du moteur | E0 | Récupère le statut du moteur | 00 : ok 50 : besoin de flash la mémoire |
| Écrire dans la mémoire du contrôleur du moteur | 71 XX…XX | Écris dans la mémoire le programme du moteur. Le programme doit faire 64 octets, représenté par les XX…XX dans la commande. La commande doit être envoyée 60 fois (60*64 octets) | 70 |
| Récupérer le checksum de la mémoire | 91 | Demande de récupérer le checksum du moteur. | 120 octets, correspondant au checksum de la mémoire |
| Initier le moteur | A1 | "Lance" le moteur | A0 |
| Assigner un moteur comme étant le moteur 1,2,3 ou 4 | 01 ou 02 ou 03 ou 04 | Assigner un moteur comme étant le moteur 1,2,3 ou 4 | 00 |
| Effacer le contenu de la mémoire | 61 | Effacer le contenu de la mémoire | 60 |
| Récupérer la version du moteur | 40 | Récupérer la version du moteur sous la forme de 11 octets. Exemple : 01 0b 03 00 01 01 0a 0a 1a 0a 0a = soft version 1.11, hard version 3.0, supplier 1.1, lot number 10/10, FVT1 26/10/10 | 11 octets |
Ces résultats rappellent ceux trouvé sur internet que je cite dans mon précédent article, avec néanmoins quelques différences.
On peut également résumer le processus de flashage du firmware par le schéma ci-dessous.

Transmission de la commande vitesse au moteur
Une autre fonction qui a piqué mon intérêt est la fonction responsable du pilotage de la vitesse des moteurs. Pour être honnête, je l'ai trouvé en tâtonnant. J'ai regardé toutes les fonctions qui faisaient des appels à la fonction qui envoie des données par le port UART. Je suis tombé sur celle ci-contre, et elle semble correspondre. Elle prend un argument un array de 4 int (32 bits), fait des calculs et génère une commande qu'elle envoie.
La première partie du code vérifie si la taille des 4 commandes pour le moteur est inférieure à 512=0b1000000000 (c'est-à-dire qu'il n'y a pas de bit égal à 1 au-dessus du 9ème bit). Si c'est le cas, il récupère ces 9bits, sinon il les met tous à 1. On en déduit que la commande est codée sur 4x9bits, et que si on code sur plus il met la vitesse max, c'est-à-dire 511=111111111.

La deuxième partie construit la commande: une liste de 5 int codé sur 8bits.
command[0] = (byte)(motor1_speed_LSB >> 4) | 0b00100000;
Il récupère les 5 premiers bits de la commande pour le moteur 1 ( shift de 4 vers la droite, comme la commande fait 9 bits, les 4 bits de LSB sont effacés et il ne reste que les 5 bits MSB, qui se retrouve au début 000 XXXXX). Il passe ensuite un masque (0x20=00100000). On obtient ainsi le premier octet de la commande : 001 XXXXX.
motor1_speed_et_return_value = (motor1_command & 0b00001111) << 4;
// ...
motor2_command_MSB = (motor2_command << 19) >> 24;
// ...
command[2] = (byte)motor3_command_MSB | (byte)motor2_speed;
Il applique ensuite un masque pour récupérer les 4 bits LSB de la commande du moteur 1, les décales 4 fois vers la gauche, on a donc XXXX 0000. Il récupère ensuite les 4 bits de MSB du moteur 2 (shift de 19 vers la gauche, puis shift de 24 vers la droite, c'est-à-dire au total on a bougé de 24-19=5 bits vers la droite, ce qui écrase les 5 bits de LSB. Comme la commande fait 9 bits, il ne reste que 9-5=4 bits de MSB). Il ajoute ensuite ça au résultat précédent et on obtient XXXX XXXX pour le deuxième octet de la commande.
motor2_speed = (motor2_command & 0b00011111) << 3;
// ...
motor3_command_MSB = (motor3_command << 18) >> 24;
// ...
command[2] = (byte)motor3_command_MSB | (byte)motor2_speed;
De la même manière, il récupère les 5 bits du LSB de la commande du moteur 2 (avec et ET logique), et les décale de 3 vers la gauche XXXXX 000. Il récupère ensuite les 9-(24-18)=9-6=3 bits de MSB de la commande du moteur 3, puis effectue un OU logique pour les additionner. On obtient donc XXXXX XXX. On peut continuer ainsi de suite jusqu'à avoir obtenu la commande complète, que l'on peut résumer avec un schéma :

Gestion du firmware des motorboards
Le code fait référence à un fichier BLC.hex situé dans /firmware/BLC.hex. Il lit son contenu, le décode en instruction ARM et le place en mémoire (fonction ). Plus précisément, il repère le caractère ":", puis récupère les 8 premiers caractères (2-4-2). Ces caractères ont l'air d'agir comme une sorte de header pour la ligne en question. Enfin, les caractères suivant le "header" sont les opcodes des instructions ARM.

D'après la description que je viens de donner, il s'agit du format HEX de Intel (ici), utilisé pour structurer de l'information destinée à des microcontrôleurs, des EEPROM ou d'autres composants programmables. D'après Wikipédia, le format peut être décrit comme un ensemble de lignes de texte où chaque ligne respecte la syntaxe suivante ::BBAAAATTHHHHHH.....HHHHCC avec
- BB est le nombre d'octets de données dans la ligne (en hexadécimal)
- AAAA est l'adresse absolue (ou relative) du début de la ligne
- TT est le champ spécifiant le type
- HH...HHHH est le champ des données
- CC est l'octet de checksum. C'est le complément à deux de la somme des valeurs binaires des octets de tous les autres champs. (Les calculs sont faits sur 8 bits, en ignorant les retenues.)
On peut vérifier si cette analyse est correcte en visionnant directement le contenu du fichier BLC.hex dans le système de fichier obtenu précédemment.

Celui-ci semble correspondre au format iHEX. On peut utiliser l'outil objcopy afin d'extraire le contenu du fichier et obtenir un fichier binaire brute.
objcopy --input-target=ihex --output-target=binary BLC.hex BLC.bin
On peut également utiliser l'outil avrdude pour obtenir le même résultat, étant donné que ce code est très certainement destiné de la motorboard, et que pour rappelle celle-ci est équipé d'un microcontrôleur Atmel ATMega8a (un AVR).
avr-objcopy -I ihex -O binary BLC.hex BLC.bin
Vérification de l'intregrité du firmware
Juste après la récupération du firmware, on a un bout de code qui peut sembler compliqué. Je pense qu'il s'agit du calcul du condensat dont je parle dans mon précédant article. Pour chaque bloc de 128 octets, un octet est retourné. J'ai essayé de comprendre comment fonctionne l’algorithme sans essayer de comprendre le code, en interprétant uniquement les résultats (vu que visiblement il n'y a que des additions).
0xFF * 128 : 0xC0 = 192
0x00 * 128 : 0x00 = 00
0x01 * 128 : 0x40 = 64
0x02 * 128: 0x80 = 128
0x80 * 128 : 0x00 = 128
0x40 * 128 : 0x00 = 0
0x20 * 128 : 0x00 = 0
0x10 * 128 : 0x00 = 0
A ce niveau là je comprends qu'il s'agit d'une addition de tout les octets du blocs, puis une division par 2 sur la somme et enfin on garde que le premier octet du résultat.
>>> hex((0xff*128)//2)[-2:]
'c0'
>>> hex((0x80*128)//2)[-2:]
'00'
>>> hex((0x01*128)//2)[-2:]
'40'
>>> hex((0x02*128)//2)[-2:]
'80'
>>> hex((0x40*128)//2)[-2:]
'00'
>>> hex((0x27*128)//2)[-2:]
'c0'
Après vérification en ayant saisit tout un tas de valeurs arbitraires, il semblerait que j'ai vu juste.
Analyse du firmware (motorboard)
Pour cette seconde phase d'analyse sur ce nouveau firmware, j'ai utilisé le logiciel IDA Pro (car malheuresement Ghidra ne supporte pas nativement l'AVR).
Le point positif est que la documentation du microcontrôleur ATMEGA8A est disponible gratuitement ici. Je vais pouvoir m'en servir pour mieux comprendre le code.
Fonctionnement de l'ATMEGA8A
Avant de commencer, quelques informations importantes (tiré de la documentation de l'ATMEGA8A). Elles aideront notre reverse, notamment à comprendre le rôle de chaque registre.
- UDR (UART Buffer) Buffer qui contient les données à envoyer et qui stock les données à réceptionner.


Processus d'initialisation
J'ai démarré mon analyse depuis le code du signal _RESET. Son objectif principal est d'amener le microcontrôleur à un état initial connu et stable afin que l'exécution du programme puisse commencer correctement.
Dans notre cas _RESET appelle un ensemble de fonctions de manière récursif jusqu'à appeler une "grande" fonction qui ne return jamais (elle boucle à l'infini). J'ai alors supposais qu'il s'agissait de la fonction principale du microcontrôleur.

Cette fonction semble initialiser l'ensemble des composants du microcontrôleur (l'ADC, les timers et la connexion USART).
Reception UART (USART_RX)
La fonction USART_RX contient le code qui nous intéresse le plus: celui qui est exécuté à la réception d'une trame UART.

Vérification de l'état de la commande
Premièrement, elle vérifie la valeur d'une variable en mémoire (que j'ai appelée command_state). Cette variable peut prendre trois valeurs : 0, 1 ou 2. Si elle est différente de ces trois valeurs, la fonction jump à la fin et return directement (sans aucune autre opération). Je suppose qu'il s'agit du statut de la réception d'une commande - 0 : nouvelle commande - 1 : commande en cours - 2 : comme reçu

Vérification du type de commande
Si command_state est à 0 (nouvelle commande), alors elle récupère les 3 bits de poids fort (MSB) de la commande UART reçue et la place dans une zone mémoire que j'ai appelée prefix_command. Pour cela, elle effectue les opérations binaires suivantes : - 1. SWAP(XXXXYYYY) = YYYYXXXX - 2. LSR(YYYYXXXX) = 0YYYYXXX - 3. ANDI(0YYYYXXX, 111) = XXX Elle compare ensuite la valeur de prefix_command (000,001,010,011,111)

On peut déduire que c'est les 3 premiers bits de la commande qui fixe une catégorie de commande.
- 0xE0 = 0b11100000 (Récuperer le statut)
- 0x40 = 0b01000000 (Récupérer version)
- 0x91 = 0b10010001 (Récupérer checksum fw)
- 0xA1 = 0b10100001 (Lancer moteur)
- 0x61 = 0b01100001 (Effacer le fw)
- 0x71 = 0b01110001 (Ecrire le fw)
- 0b001XXXXX... (Vitesse moteur)
Pilotage des moteurs
Les commandes dont le préfixe est 011 et 001 finissent rapidement par jumper au même endroit. Leur effet est simplement de placer une valeur dans une variable que j'ai appelée next_iteration (car elle est utilisée lors de la prochaine itération, c'est-à-dire lors de la réception d'un nouvel octet par UART).
- 011 : next_iteration=1
- 001 : next_iteration=4

La valeur 4 n'est pas anodine. On sait que 001 est la commande qui permet d'envoyer la vitesse, et que, pour rappel, la vitesse est codée sur 5 octets. Il reste donc 4 octets à réceptionner dès lors que le système reçoit le premier octet de la commande (celui qui contient le préfixe).
La variable command_state est ensuite passée à 1, pour indiquer très certainement que la motorboard attend d'autres octets appartenant à la même commande.
Lors de la réception de l’octet suivant, celui-ci est stocké dans un tampon que j’ai nommé unknown, puis la variable next_iteration est décrémentée de 1. Cela confirme l’hypothèse qu’il s’agit d’un compteur indiquant le nombre d’octets restant à recevoir.

Si, après décrémentation, cette valeur n’est pas nulle, le programme effectue alors un jump vers la phase de comparaison (décrite précédemment), en omettant cette fois la vérification des motifs 001 et 011 de départ.


Si le préfixe de la commande précédente est inférieur à 011 (c'est-à-dire 000, 001 ou 010), alors :
- 000 : Vérifie si le contenu d'une variable que j'ai appelé motor_id est nulle.
- Si elle n'est pas nulle, elle ne fait rien.
- Si elle est nulle :
- elle lui assigne la valeur du nouvel octet reçu par UART si celle-ci est strictement inférieure à 5.
- Sinon elle lui assigne la valeur de R1 qui vaut 0.

- 001 : Compare MOTOR_ID à 1,2,3,4 et jump sur le code correspondant. Selon le cas, extrais la vitesse associée au moteur en utilisant les variables :
- unk_2001DA (command_history): 1er octet de la commande
- unk_2001DB : 2ème octet de la commande
- unk_2001DC : 3ème octet de la commande
- unk_2001DD : 4ème octet de la commande
- unk_2001DE : 5ème octet de la commande

On peut rapidement le vérifier. Prenons l'exemple du moteur 4 et disons que les deux derniers octets de la commande valent : - unk_2001DD=XXXXXX12 - unk_2001DE=3456789X
ROM:0848 lds r18, unk_2001DD # R18 = XXXXXX12
ROM:084A ldi r19, 0
ROM:084B andi r18, 3 # R18 = XXXXXX12 & 11 = 12
ROM:084C andi r19, 0 # R19 = 00000000
ROM:084D lsr r19
ROM:084E mov r19, r18 # R19 = 00000012
ROM:084F clr r18 # R18 = 00000000
ROM:0850 ror r19 # R19 = 00000001; CR=2; R18 = 00000000
ROM:0851 ror r18 # R19 = 00000001; CR=0; R18 = 20000000
ROM:0852 lds r24, unk_2001DE # R24 = 3456789X
ROM:0854 lsr r24 # R24 = 03456789
ROM:0855 ldi r25, 0 # R25 = 00000000
ROM:0856 or r18, r24 # R18 = 23456789
ROM:0857 or r19, r25 # R19 = 00000001
ROM:0858 sts unk_2001D3, r19
ROM:085A sts unk_2001D2, r18
ROM:085C sts unk_2001D4, r20

On obtient bien R19:R18 = 0000000123456789 la vitesse du moteur 4. On a ensuite, peu importe l'identifiant du moteur : - unk_2001D3 : contient les 8 premiers bits de la vitesse - unk_2001D2 : contient les 8 derniers bits de la vitesse
Comme j'étais un peu pressé, j'ai arrêté mon reverse là, en considérant que j'avais toutes les informations nécessaires pour piloter les moteurs.
Conclusion
On a désormais une idée plutôt précise du fonctionnement du protocole BLC utilisé pour piloter les moteurs des motorboard. On peut synthétiser nos résultats avec les figures ci-dessous:


On a maintenant toutes les informations qu'il nous faut pour passer au pilotage des moteurs. La suite dans mon prochain article, disponible ici.
Où pi est la probabilité (la fréquence) d'apparition de l'octet de valeur i, et N est le nombre total de valeurs d'octets possibles (256 pour 8 bits).






