Rétro-ingénierie : comprendre et traduire un protocole réseau propriétaire

Introduction

Qu’est-ce que la rétro-ingénierie, et pourquoi y avoir recours

La rétro-ingénierie se définit comme l’étude d’un produit ou d’un système existant dans le but de déterminer son fonctionnement et la manière dont il a été conçu. Dans le cadre du développement logiciel, tout développeur peut se retrouver dans une situation nécessitant de la rétro-ingénierie, souvent afin de pallier un manque de documentation ou de code source pour une brique logicielle utilisée.

Chez enioka Haute Couture, ce besoin est survenu dans le cadre du projet enioka Scan, une bibliothèque Android open-source proposant une interface commune à divers scanners matériels de code barre. L’objectif est de simplifier le travail des développeurs en leur permettant de supporter de nombreux appareils avec la même application. Cependant, rien n’est magique, et du code spécifique à chaque scanner doit exister, il se trouve donc dans enioka Scan.

De nombreux scanners disposent d’une documentation (parfois incomplète ou dépassée) de leur API, mais bien souvent leur protocole n’est pas documenté et requiert un SDK ou une application propriétaires. Afin de supporter ces scanners de manière entièrement open source, il nous est donc nécessaire d’effectuer la rétro-ingénierie de leur protocole pour comprendre comment communiquer avec, et ainsi développer un SDK Open Source utilisable dans notre bibliothèque.

Dans ce billet, nous allons présenter un exemple de démarche de rétro-ingénierie, appliqué ici sur un scanner Bluetooth Low Energy Zebra RS5100, mais l’objectif est avant tout d’introduire des outils et bonnes pratiques applicables à d’autres scénarios.

Cible de notre exemple

L’objectif de cette démonstration va être de déterminer comment communiquer de manière simple avec le scanner RS5100 du fabriquant Zebra afin d’effectuer l’opération la plus importante de notre bibliothèque : lire un code-barre.

Le RS5100 est un scanner Bluetooth, et la version qui nous intéresse est son mode Low Energy. Il n’est pas nécessaire de comprendre le fonctionnement en profondeur de la pile Bluetooth Low Energy pour obtenir ces informations. La seule couche protocolaire qui nous sera utile est ATT (Attribute Protocol) :


Format d’un paquet ATT

ATT est assez simple et se décompose en 3 parties :
– Un OpCode, qui décrit l’opération effectuée par le payload (lecture, écriture, commande…)
– Un Handle, qui décrit quelle fonctionnalité est ciblée par le payload (comparable à un endpoint d’une API REST)
– Un payload d’une longueur maximale de 20 octets. Si le payload dépasse, il sera coupé à 20 octets et continué dans le paquet suivant.

Le scanner possède une documentation pour sa version Bluetooth Classic. Il communique via un protocole en série propriétaire, “SSI” (Simple Serial Protocol). La documentation n’est cependant pas à jour, et les limitations du protocole ATT signifient qu’il n’est pas possible pour Zebra de réutiliser le même protocole à l’identique (on subodore néanmoins que la structure sera proche, cette documentation restera donc utile).

Étape 0 : Obtention des données à analyser

Pour pouvoir comprendre le protocole utilisé par le scanner, il faut pouvoir analyser des paquets de données l’utilisant.

Comme nous développons une bibliothèque Android, nous allons utiliser l’application smartphone officielle du scanner afin de générer ces données, et Wireshark pour les observer. Android ne permet pas une analyse réseau en temps réel via Wireshark, nous serons donc contraints d’analyser des logs après avoir effectué nos actions.

Pour faciliter l’analyse et réduire au maximum le “bruit” afin de mieux identifier les éléments qui nous intéressent, nous allons générer un trafic contenant des actions pré-déterminées: appairage du scanner, lecture d’un code-barre de valeur connue, déconnexion du scanner. Il pourra être nécessaire d’effectuer plusieurs captures afin de faire varier certains éléments (par exemple le type de code-barre scanné): une plus grande quantité d’actions qui ne diffèrent que très peu permet de plus facilement identifier l’élément de la donnée qui correspond au facteur modifié. A l’inverse, si les données observées varient beaucoup d’une capture à l’autre cela signifient qu’elles ne sont probablement pas liées à l’élément étudié.

Étape 1 : Identification des canaux de communication

Toujours dans une optique de réduction du “bruit”, il est important de correctement identifier les origines et destinations des données étudiées. Wireshark permet de filtrer la majorité du trafic pour n’afficher que celui passant par ATT, mais cela ne suffira pas à complètement isoler nos données (des messages de configuration sont échangés régulièrement entre le smartphone et le scanner).


Capture sans filtrage

Dans notre cas, nous cherchons à identifier l’OpCode et Handle ATT utilisés par le scanner pour communiquer avec le smartphone, et vice-versa. Nous devons donc trouver dans la capture des communications qui semblent correspondre à ce type d’échange.


Capture après filtrage pour le protocole ATT

Pour cela, nous pouvons simplement essayer de retrouver les paquets contenant des code-barre dont la valeur est connue.


Paquet contenant le début d’un code-barre

Une fois ce paquet trouvé, nous pouvons observer les autres paquets qui l’entourent, et identifier un motif qui se reproduit fréquemment dans la capture : un paquet ATT provenant du smartphone, d’OpCode “Write Command” (0x52) et de Handle 0x0016, suivi de paquets provenant du scanner, d’OpCode “Handle Value Notification” (0x1B) et de Handle 0x0018. Nous pouvons donc nous concentrer sur les paquets qui suivent ce même motif et ignorer les autres.

Étape 2 : Interprétation de la fonction des paquets

Nous avons désormais une pile de paquets qui correspondent aux données que nous souhaitons étudier. Nous voulons maintenant les regrouper selon leur fonction, ce qui nous permettra d’étudier les données par groupe plutôt que par paquet individuel, et donc d’observer plus facilement les variations minimes entre paquets du même groupe.

Prenons cette liste de paquets SSI (partie Payload reconstituée d’un paquet ATT uniquement) :


Capture après filtrage pour ne garder que les données étudiées

Nous pouvons facilement identifier un premier groupe correspondant aux paquets de lecture de code-barre.


Paquets correspondant à une lecture de code-barre

De même, nous pouvons remarquer la présence de paquets presque identiques tous les deux messages. De nombreux protocoles possèdent une fonction ACK / NACK permettant d’attester ou non de la bonne réception d’un message. Nous pouvons donc déduire un second groupe correspondant a ce type de paquet, avec un peu moins de certitude que le premier groupe.


Paquets correspondant à un acquittement de réception

Enfin, les paquets restants sont trop peu nombreux et différents pour deviner leur fonction pour le moment. Il faudra y revenir ultérieurement, si l’étude des paquets identifiés nous permet de suffisament comprendre le protocole pour les identifier.

Il est important de rappeler qu’à cette étape, tout n’est qu’hypothèses qu’il faudra valider ou non. Nous ne travaillons actuellement qu’avec des paquets ATT capturés sur un appareil Android, et bien que certains éléments semblent évidents (comme les paquets contenant un code-barre), le reste n’est que supposition, et rien ne permet de dire avec certitude que le groupe ‘ACK’ correspond bien à cette fonctionnalité pour le moment.

Étape 3 : Traduction des paquets

Nous possédons désormais deux groupes de paquets dont nous pensons connaître la signification. Il est temps de vérifier cette hypothèse en traduisant leur contenu. Pour cela, nous pouvons nous aider de la documentation existante pour SSI, le protocole de Zebra. Bien que la documentation soit datée et concerne la version Bluetooth Classic du protocole, il est probable que la version Low Energy de SSI soit assez similaire, et que le protocole n’ait pas évolué trop fortement depuis la parution de sa documentation.

Comment procéder à cette traduction ? Avec l’aide de motifs reconnaissables dans la structure des paquets.

En rétro-ingénierie de protocoles, 3 types de motifs récurrents peuvent nous permettre de plus facilement traduire la donnée :
– les motifs communs a tous protocoles de communication (principalement : les séquences d’octets décrivant une taille de payload), ils sont en général assez intuitifs et facile à identifier, à l’exception des checksum dont la formule n’est pas toujours évidente à déduire;
– les motifs propres au protocole étudié (principalement : des OpCode qui permettent aux appareils de traiter davantage la donnée), ceux-ci nécessitent en général de la documentation pour être traduits voire identifiés;
– les motifs propres au contexte d’un paquet (par exemple dans le cas d’un paquet contenant un code-barre : sa taille ou sa symbologie), qui nécessitent de connaître la fonction du paquet, mais sont relativement simples à traduire une fois détectés.

Ces trois motifs devraient nous permettre de traduire la majorité des octets présents dans les paquets étudiés.

Groupe ‘ACK’


Les deux variantes d’un paquet supposé ACK

Commençons par le type de message le plus simple : le ACK. Chaque paquet ne contient que 8 octets dont la moitié ne varie pas, ce qui limite les possibilités. Commençons par vérifier si ces paquets correspondent bien à un ACK en comparant leur structure à la documentation SSI.


Documentation SSI sur les paquets ACK

Nous pouvons constater que la majorité des octets correspondent à ceux décrits par la documentation :
– le troisième byte possède bien la valeur 4, taille de la partie SSI du paquet en excluant le checksum;
– le quatrième byte correspond bien à l’OpCode ACK;
– le cinquième byte, principale variation entre les différents paquets observés, correspond bien à 0x00 ou 0x04 selon la source du paquet;
– le 6e octet devrait selon la documentation correspondre à un “status”, cependant sa valeur ne correspond pas au fonctionnement décrit par la documentation. Nous devrons donc considérer que la documentation n’est simplement pas à jour;
– enfin, les deux derniers octets correspondent bien au checksum attendu en effectuant le calcul sur les 4 précédents octets.

Restent les deux premiers octets, qui ne figurent pas dans la documentation SSI, dont nous ne connaissons pas la signification pour le moment.

Groupe ‘Code-barre’


Paquet contenant un code-barre

Le second groupe identifié est le groupe de paquets contenant un code-barre. La documentation SSI décrit ce type de paquet comme “DECODE_DATA”.


Documentation SSI sur les paquets DECODE_DATA

En reprenant la base découverte en traduisant les paquets ACK, nous pouvons à nouveau retrouver les octets “taille”, “opcode”, “source”, “status” et “payload” correspondants aux valeurs décrites par la documentation. L’octet “barcode type” est également présent avec la bonne valeur (0x0B correspond à la famille de code-barre EAN-13, qui est bien le type de code-barre scanné pour ce test). Nous retrouvons également le code-barre en question avant le checksum, correspondant au champ “decode data”.

Une fois de plus, les deux premiers octets ne sont pas décrits par la documentation. Les trois octets précédant le code-barre sont également inconnus.

Octets non identifiés

Afin de traduire les octets inconnus, nous allons procéder à une nouvelle capture durant laquelle nous allons scanner plusieurs code-barres de symbologie et longueur différente, pour générer des paquets similaires mais dont les octets en lien avec ces variables seront différents.


5 paquets de type code-barre

Nous pouvons constater que la majorité du paquet reste effectivement inchangée, à l’exception des octets “length”, “barcode type” et “checksum” comme attendu. Les premier et dixième octets ont également changé, ce qui semble indiquer qu’ils sont liés au type ou à la taille du code barre.

La taille d’un payload est, comme mentionné en début d’étape, une donnée très utilisée dans tout protocole de communication qui transporte un payload dont la taille peut varier, comme ici un code-barre. Si nous convertissons le dixième octet en valeur décimale, nous obtenons systématiquement une valeur égale à la taille du code-barre scanné. De plus, si nous effectuons de nouveaux tests avec des scans de longueur bien plus élevée (dépassant 255 caractères, la valeur maximale d’un octet non-signé), le neuvième octet change également. Nous pouvons donc conclure que les octets 9 et 10 correspondent à la taille du code-barre scanné.

En appliquant un principe similaire, les deux premiers octets varient fortement selon la taille du paquet SSI, mais ne correspondent pas à la taille de ce dernier tels quels. Cependant, en les lisant de droite à gauche (ordre petit boutisme), nous obtenons bel et bien la taille totale du paquet SSI, ces deux octets et le checksum inclus. Il s’agit probablement d’un marqueur permettant d’aider l’appareil à reconstituer des paquets SSI divisés en plusieurs paquets ATT.

Reste seulement le huitième octet du paquet DECODE_DATA. Cet octet n’a pas d’équivalence dans la documentation, et aucune manipulation n’a permis de le faire varier. Il n’est donc pas possible de deviner sa signification et de le traduire, c’est une des limites de la rétro-ingénierie.

Résultats

Grâce à la documentation, aux motifs récurrents et au choix des données générées pour l’étude, nous avons pu comprendre traduire, à un octet près, l’intégralité des paquets SSI contenant un code-barre ou un ACK. Grâce à ces découvertes, nous pouvons appliquer ces mêmes principes aux autres paquets précédemment non-identifiés pour obtenir ce résultat :

En continuant d’analyser différents types de trafic entre le scanner et le smartphone, nous pouvons traduire d’autres types de paquets SSI et progressivement découvrir le reste du protocole. Il ne nous restera alors qu’à implémenter dans enioka Scan un traducteur pour convertir nos fonctionnalités en messages SSI correspondants.

Conclusion

Nous avons vu ici un exemple de démarche de rétro-ingénierie appliquée à un protocole propriétaire de scanner Bluetooth Low Energy, divisée en quelques étapes :

  • Capture des flux à analyser, en se limitant au maximum à des actions simples et identifiables
  • Identification des canaux de communication, afin de ne conserver que les paquets susceptibles de nous intéresser
  • Interprétation des paquets, afin de les regrouper par type pour faciliter leur traduction
  • Traduction des paquets, octet par octet
  • Répétition de ces étapes si nécessaire, en procédant à de nouvelles captures d’actions plus précises pour différencier les derniers octets

Cette démarche nous a permis de comprendre et traduire tous les messages échangés entre un smartphone et le scanner durant une utilisation classique. Ces découvertes rendent possible l’implémentation d’un SDK open-source dans enioka Scan, et la prise en charge de ce scanner (et tout autre scanner utilisant le même protocole) pour nos utilisateurs.

Ce scénario n’est qu’un exemple, et les notions introduites ici restent applicables à d’autres cas logiciels : une bonne préparation du jeu de données, une bonne connaissance du contexte étudié et la recherche de motifs récurrents permettent de traduire en partie un protocole inconnu, ou du moins permettent de donner un sens aux données observées. Une documentation, même datée ou incomplète, facilite grandement la traduction, mais n’est pas toujours nécessaire pour obtenir une connaissance exploitable de la cible.


Publié

dans

par

Étiquettes :

Blog at WordPress.com.