Brother exigeait Windows pour un firmware chiffré avec trente-deux octets de vide

Le firmware du Brother ADS-4500W dormait dans une ressource PE chiffrée avec une clé nulle ; son protocole TCP/5566 tenait dans des trames de seize octets.

Partager
Un scanner prisonnier d’une fenêtre libère un rouleau de firmware chiffré, pendant qu’un manchot ouvre une serrure remplie de trente-deux octets vides.

Le mot firmware vient d’une époque où le logiciel proche du matériel avait au moins la politesse de ne pas prétendre être une application. Il vivait quelque part entre le silicium et la magie noire, changeait rarement et, lorsqu’il changeait, on sortait un utilitaire constructeur dont l’ergonomie rappelait que la maintenance n’avait pas participé aux réunions produit.

Les périphériques réseau modernes ont conservé le vocabulaire et supprimé la retenue. Un scanner documentaire est désormais un ordinateur avec un écran, deux interfaces réseau, un serveur web, SNMP, LDAP, OAuth2 et assez de logiciel pour avoir des vulnérabilités pendant des années. Le mien, un Brother ADS-4500W, exécutait pourtant toujours son firmware 2.28 sans jamais suggérer qu’un 3.09 existait. Qui met à jour le firmware de son scanner, sérieusement ? Précisément la personne qui vient de découvrir que son scanner a un firmware.

L’interface web savait afficher la version. Elle ne savait pas la changer. Brother proposait bien un « Firmware Update Tool » qui, selon sa page de téléchargement, envoie le firmware à la machine. Sous Linux, la même page proposait des pilotes de numérisation et s’arrêtait là. Le matériel parle eSCL, SFTP, HTTPS et SNMP, mais sa maintenance exigeait Windows ou macOS. C’est une conception multiplateforme très aboutie : elle supporte toutes les plateformes qui intéressent le constructeur.

Une mise à jour minuscule et sa VM Windows

La voie officiellement carrossable passait par Windows. J’ai démarré une VM Windows 11 et téléchargé l’exécutable Brother. Sa console était verrouillée et n’exposait aucun affichage graphique : il restait à piloter l’interface Win32 à distance.

Les contrôles n’exposaient aucun motif UI Automation, l’UAC gardait la porte et les dialogues vivaient sur un bureau verrouillé. Cette petite promenade a donc demandé une tâche planifiée interactive élevée, PrintWindow, puis les messages BM_CLICK, WM_SETTEXT et IPM_SETADDRESS. Une fenêtre modale bloquait SendMessage, alors les clics ont dû être postés. La console répondait en CP850 et cassait le premier lecteur Python au premier accent. Tout cela pour appuyer sur une flèche vers le bas.

Le scanner est finalement passé de 2.28 à 3.09. La photographie avant/après a montré l’arrivée d’OAuth2 et deux points d’entrée LDAP et Active Directory désormais en erreur 500. Le changelog Brother disait, pour cette version comme pour les précédentes : « Répare les problèmes mineurs. » C’est une formulation admirablement robuste : elle reste vraie même lorsqu’on ne sait pas ce que « répare » veut dire.

Sans assistance par IA, je n’aurais probablement jamais consacré cette soirée à un scanner qui fonctionnait. Avec elle, la mise à jour devenait assez peu coûteuse pour être tentée, mais passer par Windows sans comprendre le geste devenait soudain beaucoup moins satisfaisant. Le paradoxe a donc produit la question utile : que fait réellement cet exécutable de 45 555 512 octets ?

Le blob n’attendait pas derrière une URL

Mon premier modèle était simple : l’outil télécharge un fichier puis le dépose sur un point d’entrée HTTP caché. C’est la forme habituelle des mises à jour d’appareils administrés par navigateur, donc une excellente hypothèse à invalider rapidement.

Le point d’entrée /ft/adm_firmwareupdate du scanner ne faisait que rapporter la version. Les routes voisines, les scripts de l’interface et les requêtes du navigateur ne contenaient aucun geste de flashage. Pendant ce temps, le binaire Windows importait WS2_32 et parlait à un port qui n’avait rien d’un serveur web : TCP/5566.

L’exécutable lui-même n’était pas une archive auto-extractible. file, la signature Authenticode, la table d’imports et l’empreinte donnaient plutôt ce portrait :

format       PE32, interface graphique Windows
taille       45 555 512 octets
signataire   Brother Industries, Ltd.
sha256       b658029a846b977645d03fd38bd659ef49ab847a152445732af106ba19535892
imports      WS2_32, IPHLPAPI

Le format PE conserve, à côté du code, un arbre de ressources destiné aux icônes, dialogues, chaînes et données arbitraires ; sa disposition est décrite dans la spécification PE/COFF de Microsoft. Dans cet arbre, le type BINARY, identifiant 137, langue 1028, pesait 41 943 088 octets. Le firmware n’était pas téléchargé au dernier moment. Il voyageait dans l’exécutable, ce qui est pratique lorsqu’on tient à distribuer quarante et un mégaoctets de données avec quatre mégaoctets de boutons autour.

Extraire la ressource ne suffisait pas. Son début ne ressemblait ni à une image de firmware, ni à une archive connue, et les chaînes attendues restaient absentes. Le blob était chiffré.

La cryptographie comme papier cadeau

Le binaire liait OpenSSL statiquement. Il contenait aussi une routine PBKDF2-HMAC-SHA256, un sel GGBONBON et 10 000 tours. Voilà une piste très séduisante : un KDF nommé, un sel pittoresque, exactement le genre de détail qui donne l’impression que la soirée avance.

Cette routine ne protégeait pas le firmware. Elle servait une autre famille de commandes. Le chemin qui chargeait BINARY/137 appelait une seconde routine, beaucoup moins ambitieuse : AES-256-CBC, avec une clé de 32 octets nuls et un vecteur d’initialisation de 16 octets nuls.

AES est un standard parfaitement sérieux. Une clé entièrement nulle est aussi une clé parfaitement valide ; elle a seulement l’inconvénient d’être connue de toute personne sachant compter jusqu’à zéro. Ici, le chiffrement ne constituait pas une barrière de sécurité. Il emballait le firmware assez fermement pour que strings n’abîme pas le suspense.

Le conteneur se déchiffrait en deux passes indépendantes avec les mêmes zéros :

  1. les 32 premiers octets rendaient un en-tête commençant par FW_AES ;
  2. le reste rendait une image commençant par A4LT.

Le second magic annonçait un ADS4_LT_Package, avec son octet de version et sa table de partitions. Les chaînes en clair parlaient enfin comme un firmware : messages de flashage, comparaison de versions, libellés du panneau dans vingt-huit langues. L’empreinte SHA-256 de l’image obtenue était :

ffdd79656367c5329b3bfe35992069eaf7dd89b98f01c53bf65b35786b91c56f

Les deux magics et ces chaînes formaient des témoins hors ligne : le déchiffrement ne produisait pas seulement un grand fichier statistiquement plausible. Il produisait le paquet que l’outil préparait avant le transfert.

Un extracteur minimal n’a besoin que de lire l’en-tête PE, convertir les RVA en décalages de fichier, descendre l’arbre BINARY/137, puis appliquer deux fois l’AES-CBC sans bourrage à retirer. Les coordonnées importantes sont celles-ci :

ressource     BINARY/137, langue 1028
en-tête       32 octets chiffrés → FW_AES
corps         octets suivants    → A4LT
algorithme    AES-256-CBC
clé / IV      32 / 16 octets nuls

Pas besoin de publier deux cents lignes de Python. Avec ces cinq prises et la spécification PE, le prochain propriétaire d’un ADS-4500W n’est plus condamné à faire confiance à une fenêtre grise.

Seize octets pour prendre le scanner en otage

Restait le transport. Chaque commande envoyée sur TCP/5566 tient dans une trame little-endian de seize octets :

30 02 00 00 | op | 00 | zone | 00 | idx(2) | 00 00 | longueur(4)

Le premier mot, 0x230, marque la commande. La réponse est un état de 32 octets. Lorsqu’une longueur est annoncée, les données suivent la trame dans le sens imposé par l’opération. Une demande d’état réelle ressemble à ceci :

30 02 00 00 12 00 02 00 00 00 00 00 20 00 00 00

Envoyée au scanner, elle a bien rendu 32 octets. C’était le premier moment où la dissection d’un PE devenait une conversation avec l’objet physique plutôt qu’une fanfiction de désassembleur.

La séquence normale reconstruite dans la fonction de mise à jour est courte :

Étape Opération Zone et index Charge utile
état ou réveil 0x12 0x02, 0 aucune
verrou 0x2a 0x98, 7 jeton UUID de 80 octets
lecture de contrôle 0x28 0x10, 0xf0 UUID en NVRAM
annonce 0x2b 0x08, 0x101 taille de l’image
flux 0x2a 0x08, 0 image A4LT en clair
sondage 0x12 0x02, 0 pourcentage
déverrou 0x2a 0x98, 8 même jeton UUID

Le verrouillage a été éprouvé sur l’appareil sans lancer de flash : un UUID neuf de 80 octets a été posé à l’index 7, relu à l’identique, puis présenté à l’index 8 pour rendre la main. Après l’échange, l’état HTTP du scanner était revenu à Ready. Cela validait le port, l’encodage des trames et la mécanique de session sans transformer un périphérique utile en presse-papier expérimental.

Une complication attendait dans le désassemblage. Quatre écritures semblaient se glisser entre le verrou et le transfert. Les ignorer aurait produit un client Linux incomplet ; les reproduire aveuglément aurait été plus fidèle au désassembleur qu’au programme.

Le bloc était gardé par un drapeau INI, UpdateFwAllFlash. Sans fichier INI, sa valeur restait nulle et un saut contournait les quatre écritures d’un seul tenant. Deux autres clés, EnableDowngrade et Development, confirmaient la nature du voisinage : un mode de reflashage total destiné au développement, pas le chemin normal. Le bon clone n’était donc pas celui qui exécutait tout ce qui existait, mais celui qui reproduisait ce que l’outil exécutait réellement.

Au passage, les chaînes du binaire ont offert un souvenir gratuit : une commande de l’appareil sait lancer killall -9 check_snmp_alive && killall snmpd. Cette capacité n’était pas nécessaire à la mise à jour et n’a pas été explorée. Toute épopée sérieuse doit savoir laisser au moins une porte inquiétante fermée.

Deux commandes, et une troisième que je ne lancerai pas encore

La chaîne Linux tient désormais en deux outils. Le premier télécharge ou lit l’exécutable signé, vérifie les magics et extrait l’image en clair. Le second lit cette image, construit les trames et déroule par défaut la séquence à vide. Une option explicite est nécessaire pour ouvrir TCP/5566, et une seconde confirmation littérale précède tout transfert.

Les pointeurs à garder sous le coude sont peu nombreux :

téléchargement  dlf106893 → ADS-3300W_ADS-4500W_Win_FW_3.09.exe
artefact         PE32 signé, 45 555 512 octets
firmware         BINARY/137 → AES-256-CBC(0, 0) → A4LT
transport        TCP/5566, commandes de 16 octets, états de 32 octets
ordre            état → verrou → contrôle → annonce → flux → sondage → déverrou

Le protocole normal est reproduit. L’extraction est validée hors ligne. La prise et la restitution du verrou sont confirmées sur le scanner. En revanche, le client Linux n’a jamais envoyé les 41 mégaoctets jusqu’au redémarrage : le scanner est déjà à la version publiée, l’outil refuse le retour arrière et je n’ai pas de second ADS-4500W prêt à mourir pour la littérature.

Le monde après ce chantier est donc légèrement meilleur, selon une définition très locale du progrès. Un futur firmware pourra être extrait sans Windows et son transport n’est plus une boîte noire. Le scanner fonctionne toujours. La seule étape non testée est celle qui peut le briquer, ce qui paraît à la fois une faiblesse méthodologique et une excellente politique domestique.