NB: Cet article fait suite à Reverse engineering des moteurs d’un drone Parrot (CPEFly).
L’objectif de cette partie est d’essayer d’en apprendre plus sur le contrôle des moteurs. Pour rappel, de ce qu’on l’on sait, les moteurs sont contrôlé 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é.
Analyse du firmware : reverse du fichier .PLF
On commence ce processus de reverse en analysant le fichier de mise à jour du firmware du drone. Pour rappel, dans un système informatique, un firmware est un logiciel intégré dans un matériel informatique pour qu’il puisse fonctionner.
Je télécharge le fichier du mise à jour du firmware sur le site officiel de Parrot. Il s’agit d’un fichier au format PLF, encore un format propriétaire Parrot. En effectuant une commande strings (qui nous retourne les chaines de caractères présentent dans un fichier), on tombe sur des choses intéressantes, comme deux accès ftp sur les ports 21 et 5551 du drone.
21 stream tcp nowait root ftpd ftpd -w /data/video
5551 stream tcp nowait root ftpd ftpd -w /update
Il y a de grande chance que la partie du firmware qui nous intéresse ne soit pas chiffré. Je m’empresse de faire un binwalk afin d’y voir plus clair.
On remarque que le firmware contient des données compressé et deux zImage. Pour rappel: « zImage is compressed version of the Linux kernel image that is self-extracting ». Je décide d’extraire chaque zImage et toutes les données compressées qui les suivent dans deux fichiers distincts. J’extrait également les deux premières archives compressées dans deux fichiers distincts, que je décompresse et que je concatène, puis je relance un binwalk dessus.
dd if=ardrone2_update.plf bs=1 skip=772 count=2179912 of=Disk1
dd if=ardrone2_update.plf bs=1 skip=2180684 of=Disk2
dd if=../Disk1 bs=1 skip=1196588 of=file2.gz
dd if=../Disk1 bs=1 skip=13069 count=1183519 of=file1.gz
gunzip file2.gz
gunzip file1.gz
cp file1 file
cat file2 >> file
Bingo, on obtient un file system !
Malheureusement il s’agit d’un système plutôt simpliste qui ne nous apprend pas grand chose. Je pense qu’il s’agit d’un premier système qui a pour but l’installation du deuxième systeme, d’où le fait qu’on ait deux zImage. Ce qu’on cherche se situe donc probablement dans l’autre partie du fichier.
Comme il y a beaucoup de donnée compressé, j’ai fait un petit script python (pas très jolie) qui récupère les données et les décompressent. En observant les fichiers décompressés générés, je m’aperçoit qu’ils ont tous un path en guise d’entête (plus 12 octets).
J’en déduis qu’il s’agit du path de chaque fichier. Sachant cela j’adapte mon script pour reconstruire le filesystem.
import re
import subprocess
import os
os.system("rm files/*")
output = subprocess.check_output(["binwalk","../Disk2"]).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=../Disk2 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()
Oura ! On arrive à obtenir toutes l’arborecence, comme on peut le voir sur la capture ci-dessous.
Comme je m’intéresse seulement au fonctionnement des moteurs, je fais un grep -lF « motor » * pour déterminer tous les fichiers dans lequel le termes « motor » apparaît. Je fais pareil avec la chaine « /dev/ttyPA0 », et j’obtiens un fichier: « program.elf ».
Analyse du programme principale de la carte mère (ARM)
Le fichier program.elf est un binaire au format ELF pour une architecture ARM. J’utilise le logiciel développé par la NSA « Ghidra » pour l’étudier. Comme il est strippé (il n’y a pas de symbole, donc pas de noms de fonctions / de variables globales), je renomme chaque fonction selon les actions qu’elles effectuent. Comme il y a beaucoup de code, je commence par le point d’entrée afin de retrouver la fonction main du programme. 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.
main()
run()
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 à ces strings, on détermine l’ensemble des fonctions en lien avec BLC.
initialisation des moteurs
Il y a une fonction qui a retenu mon attention mon attention, que j’ai baptisé 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érent 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é 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 siens.
}
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é, 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 etre retourné 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 numero 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 |
Ecrire 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é 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 |
Qui rappelle le résultat trouvé sur internet dans mon précédant article, avec néanmoins quelques différences.
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 faisait 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), fais 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 sont inférieures à 512=0b1000000000 (c’est à dire qu’il n’y a pas de bit égale à 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édant 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 obtenue la commande complète, que l’on peut résumer avec un (jolie) 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 contenue, 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.)
Comme j’ai accès au filesystem, je peux inspecter le contenue du fichier BLC.hex pour voir si mon analyse est correcte.
Et ça semble correspondre. J’utilise la commande « objcopy –input-target=ihex –output-target=binary BLC.hex BLC.bin » pour obtenir un fichier binaire. Comme je pense que ce code est à destination du moteur, et que le moteur est équipé d’un microcontrôleur Atmel ATMega8a (un AVR), j’utilise avrdude pour obtenir un binaire. « avr-objcopy -I ihex -O binary BLC.hex BLC.bin ». Pour commencer ce nouveau reverse, je sors mon vieux IDA pro.
Reverse du firmware du moteur : AVR
Premièrement je remarque directement les deux fonctions USART (TXC pour la transmission et RXC pour la réception). Je pars de la fonction _RESET pour commencer mon reverse. Elle appelle une fonction qui elle-même appelle une autre fonction et ainsi de suite jusqu’à appeler une « grande » fonction qui semble gérer tout un tas de chose.
Le bon point est que la documentation du microcontrôleur ATMEGA8A est disponible gratuitement ici. Je vais pouvoir m’en servir pour mieux comprendre le code.
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.
Nom du registre | Description | Description de chaque bit |
UCSRA | USART Control and Status Register A | Bit 7 – RXC: USART Receive Complete This flag bit is set when there are unread data in the receive buffer and cleared when the receive buffer is empty (i.e. does not contain any unread data). If the Receiver is disabled, the receive buffer will be flushed and consequently the RXC bit will become zero. The RXC Flag can be used to generate a Receive Complete interrupt (see description of the RXCIE bit). Bit 6 – TXC: USART Transmit Complete This flag bit is set when the entire frame in the Transmit Shift Register has been shifted out and there are no new data currently present in the transmit buffer (UDR). The TXC Flag bit is automatically cleared when a transmit complete interrupt is executed, or it can be cleared by writing a one to its bit location. The TXC Flag can generate a Transmit Complete interrupt (see description of the TXCIE bit). Bit 5 – UDRE: USART Data Register Empty The UDRE Flag indicates if the transmit buffer (UDR) is ready to receive new data. If UDRE is one, the buffer is empty, and therefore ready to be written. The UDRE Flag can generate a Data Register Empty interrupt (see description of the UDRIE bit). UDRE is set after a reset to indicate that the Transmitter is ready. Bit 4 – FE: Frame Error This bit is set if the next character in the receive buffer had a Frame Error when received (i.e., when the first stop bit of the next character in the receive buffer is zero). This bit is valid until the receive buffer (UDR) is read. The FE bit is zero when the stop bit of received data is one. Always set this bit to zero when writing to UCSRA. Bit 3 – DOR: Data OverRun This bit is set if a Data OverRun condition is detected. A Data OverRun occurs when the receive buffer is full (two characters), it is a new character waiting in the Receive Shift Register, and a new start bit is detected. This bit is valid until the receive buffer (UDR) is read. Always set this bit to zero when writing to UCSRA. Bit 2 – PE: Parity Error This bit is set if the next character in the receive buffer had a Parity Error when received and the parity checking was enabled at that point (UPM1 = 1). This bit is valid until the receive buffer (UDR) is read. Always set this bit to zero when writing to UCSRA. Bit 1 – U2X: Double the USART transmission speed This bit only has effect for the asynchronous operation. Write this bit to zero when using synchronous operation. Writing this bit to one will reduce the divisor of the baud rate divider from 16 to 8 effectively doubling the transfer rate for asynchronous communication. Bit 0 – MPCM: Multi-processor Communication Mode This bit enables the Multi-processor Communication mode. When the MPCM bit is written to one, all the incoming frames received by the USART Receiver that do not contain address information will be ignored. The Transmitter is unaffected by the MPCM setting. |
UCSRB | USART Control and Status Register B | Bit 7 – RXCIE: RX Complete Interrupt Enable Writing this bit to one enables interrupt on the RXC Flag. A USART Receive Complete interrupt will be generated only if the RXCIE bit is written to one, the Global Interrupt Flag in SREG is written to one and the RXC bit in UCSRA is set. Bit 6 – TXCIE: TX Complete Interrupt Enable Writing this bit to one enables interrupt on the TXC Flag. A USART Transmit Complete interrupt will be generated only if the TXCIE bit is written to one, the Global Interrupt Flag in SREG is written to one and the TXC bit in UCSRA is set. Bit 5 – UDRIE: USART Data Register Empty Interrupt Enable Writing this bit to one enables interrupt on the UDRE Flag. A Data Register Empty interrupt will be generated only if the UDRIE bit is written to one, the Global Interrupt Flag in SREG is written to one and the UDRE bit in UCSRA is set. Bit 4 – RXEN: Receiver Enable Writing this bit to one enables the USART Receiver. The Receiver will override normal port operation for the RxD pin when enabled. Disabling the Receiver will flush the receive buffer invalidating the FE, DOR and PE Flags. Bit 3 – TXEN: Transmitter Enable Writing this bit to one enables the USART Transmitter. The Transmitter will override normal port operation for the TxD pin when enabled. The disabling of the Transmitter (writing TXEN to zero) will not become effective until ongoing and pending transmissions are completed (i.e., when the Transmit Shift Register and Transmit Buffer Register do not contain data to be transmitted). When disabled, the Transmitter will no longer override the TxD port. Bit 2 – UCSZ2: Character Size The UCSZ2 bits combined with the UCSZ1:0 bit in UCSRC sets the number of data bits (Character Size) in a frame the Receiver and Transmitter use. Bit 1 – RXB8: Receive Data Bit 8 RXB8 is the ninth data bit of the received character when operating with serial frames with nine data bits. Must be read before reading the low bits from UDR. Bit 0 – TXB8: Transmit Data Bit 8 TXB8 is the ninth data bit in the character to be transmitted when operating with serial frames with nine data bits. Must be written before writing the low bits to UDR. |
UCSRC | USART Control and Status Register C | Bit 7 – URSEL: Register Select This bit selects between accessing the UCSRC or the UBRRH Register. It is read as one when reading UCSRC. The URSEL must be one when writing the UCSRC. Bit 6 – UMSEL: USART Mode Select This bit selects between Asynchronous and Synchronous mode of operation. 0 Asynchronous Operation 1 Synchronous Operation Bit 5:4 – UPM1:0: Parity Mode These bits enable and set type of Parity Generation and Check. If enabled, the Transmitter will automatically generate and send the parity of the transmitted data bits within each frame. The Receiver will generate a parity value for the incoming data and compare it to the UPM0 setting. If a mismatch is detected, the PE Flag in UCSRA will be set. 0 0 Disabled 0 1 Reserved 1 0 Enabled, Even Parity 1 1 Enabled, Odd Parity Bit 3 – USBS: Stop Bit Select This bit selects the number of stop bits to be inserted by the transmitter. The Receiver ignores this setting. 0 1-bit 1 2-bit Bit 2:1 – UCSZ1:0: Character Size The UCSZ1:0 bits combined with the UCSZ2 bit in UCSRB sets the number of data bits (Character Size) in a frame the Receiver and Transmitter use. 0 0 0 : 5-bit 0 0 1 : 6-bit 0 1 0 : 7-bit 0 1 1 : 8-bit 1 0 0 : Reserved 1 0 1 : Reserved 1 1 0 : Reserved 1 1 1 : 9-bit Bit 0 – UCPOL: Clock Polarity This bit is used for Synchronous mode only. Write this bit to zero when Asynchronous mode is used. The UCPOL bit sets the relationship between data output change and data input sample, and the synchronous clock (XCK). |
UBRRL and UBRRH | USART Baud Rate Registers | Bit 15 – URSEL: Register Select This bit selects between accessing the UBRRH or the UCSRC Register. It is read as zero when reading UBRRH. The URSEL must be zero when writing the UBRRH. Bit 14:12 – Reserved Bits These bits are reserved for future use. Bit 11:0 – UBRR11:0: USART Baud Rate Register This is a 12-bit register which contains the USART baud rate. The UBRRH contains the four most significant bits, and the UBRRL contains the eight least significant bits of the USART baud rate. Ongoing transmissions by the Transmitter and Receiver will be corrupted if the baud rate is changed. Writing UBRRL will trigger an immediate update of the baud rate prescaler. |
UDR | UART Buffer | Buffer qui contient les données à envoyer et qui stock les données à réceptionner. |
Cette fonction semble initialiser l’ensemble des composants du microcontrôleur (l’ADC, les timers et la connection USART). Comme elle ne return jamais (elle boucle à l’infinie), je suppose qu’il s’agit de la fonction « principale » du microcontrôleur.
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.
Premièrement, elle vérifie la valeur d’une variable en mémoire. Cette variable peut prendre trois valeurs : 0, 1 ou 2. Si elle est différente de 1 ou 2 la fonction return directement (sans aucune autre opération). Je pense qu’il s’agit du statut de la réception d’une commande (0: nouvelle commande, 1: commande en cours, 2: comme reçu).
Ensuite, elle récupère les 3 MSB de la commande et compare leur valeur (000,001,010,011,111). On en déduit que c’est les 3 premiers bits de la commande qui fixe le type de commande.
SWAP(XXXXYYYY) = YYYYXXXX, puis LSR(YYYYXXXX) = 0YYYYXXX, puis ANDI(0YYYYXXX, 111) = XXX.
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. On peut synthétiser nos résultats avec les figures ci-dessous:
Note en vrac
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 me casser la tète à comprendre le code, mais plutôt en interprétant 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.
On a maintenant toutes les informations qu’il nous faut pour passer au pilotage des moteurs. La suite dans mon prochain article, disponible ici.