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.
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 :
- les 32 premiers octets rendaient un en-tête commençant par
FW_AES; - 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.