IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Rétro-ingénierie du dock média du clavier Everest Max par l'analyse des trames USB

Dans l’article dédié au clavier Everest Max de Mountain, le lecteur peut apprendre que certaines fonctionnalités, notamment celles du dock média, ne sont pas disponibles sous Linux. Cet article détaille la méthodologie à avoir pour réimplémenter les fonctionnalités manquantes.

1 commentaire Donner une note à l´article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Malheureusement, les constructeurs de claviers ne fournissent que très rarement le support de Linux (et quelquefois, celui de Mac OS non plus). Par conséquent, les utilisateurs de ces systèmes ont un accès restreint aux fonctionnalités du clavier et notamment, il n’est pas possible de le reconfigurer sans Windows.
Toutefois, l’utilisateur pourra accepter la contrainte en configurant le clavier sous Windows. En effet, la configuration étant généralement sauvegardée dans le clavier, celle-ci sera donc aussi présente lors d’une session sous Linux ou Mac OS. Cependant, dans le cas du clavier Everest Max de Mountain, les informations affichées par le dock média proviennent du PC. De ce fait, sous Linux et Mac OS, la présence du dock est bien moins utile (pas d’affichage de l’horloge, pas d’affichage correct du volume sonore...). Cela est bien dommage, mais il est possible, sans trop de difficulté, d’y remédier.
Ainsi, cet article présente une méthode afin de restaurer sous Linux (ou Mac OS) quelques fonctionnalités du dock média et ainsi retrouver l’intérêt du dock média.

II. Réimplémentation du service

Lorsqu’on observe le fonctionnement du dock média du clavier Everest Max de Mountain, on remarque que les informations affichées proviennent du PC. Autrement dit, le dock média n’a pas un fonctionnement indépendant. Aussi, en observant les processus installés avec le logiciel Base Camp du constructeur, il est certain que c’est le service qui communique avec le clavier.

Pour reproduire ce comportement sous Linux, il est possible de recréer un programme envoyant des informations à afficher.
Pour envoyer des informations au clavier (connecté par USB), la bibliothèque libusb (documentation) est une solution viable. Celle-ci permet de communiquer, de manière bas niveau, avec le clavier (ou tout autre périphérique USB). La première étape est de connaître l’identifiant du clavier, que l’on peut récupérer avec lsusb :

 
Sélectionnez
Bus 001 Device 006: ID 3282:0001 Mountain Mountain Everest Keyboard

Par exemple, le programme suivant permet d’établir une connexion avec le clavier :

 
Cacher/Afficher le codeSélectionnez

Il ne reste plus qu’à déterminer comment envoyer des informations au clavier. Pour cela, il est possible d’utiliser Wireshark (avec USBPcap) afin d’analyser les paquets échangés entre le service Base Camp et le clavier. Pour faciliter la tâche, il est nécessaire de trouver une idée pour contrôler les informations envoyées au clavier. Voici ce que le dock média affiche :

  • l’heure, malheureusement certainement envoyée qu’une seule fois ;
  • les actions par minute ;
  • les niveaux d’utilisation des ressources système ;
  • le volume sonore.

Parmi ces possibilités, le volume sonore est le plus facile à contrôler. En effet, le contrôle sur les ressources système n’est pas précis et les actions par minute difficiles à contrôler finement.

Ce genre d’informations contrôlables est nécessaire pour commencer l’investigation. En effet, les échanges entre le clavier et le système sont réguliers. Par conséquent, il est nécessaire de comprendre la raison de cet échange régulier, mais d’autant plus, le contenu de l’échange. De plus, il est possible que les échanges contiennent les niveaux d’utilisation des ressources et les autres informations nécessaires au dock média. Avec une information contrôlable, il est possible de capturer les paquets et de chercher la valeur envoyée.

Pour mieux savoir quand les données intéressantes sont échangées, la veille du dock média est désactivée et ce dernier est configuré pour afficher le volume sonore par défaut. Bref, tout est fait pour observer ce qui se passe, tout en ayant le plus de paramètres connus et le moins possible de facteurs extérieurs.

Un autre détail observable est que le changement de volume dans Windows n’est pas directement répliqué sur le dock média. Cela signifie que l’information est transférée avec les paquets réguliers et non au moment du changement de l’information.

Comme les paquets sont affichés en hexadécimal, nous allons modifier le volume dans la plage allant de 0 à 10. Ainsi, la valeur affichée dans Wireshark, sera la même que sur l’écran.

Une première capture avec un changement de volume produira jusqu’à une centaine de paquets. Les échanges sont bidirectionnels. Wireshark permet de réduire le nombre de paquets affichés grâce aux filtres. Ici, le filtre usb.src == host permet de ne garder que les paquets envoyés par le PC. Ainsi, il est possible de comparer les paquets facilement et de voir en effet, un paquet intéressant :

lorsque le volume est 6 :

Un paquet contenant la valeur recherchée 0x06

lorsque le volume est 8 :

Un autre paquet contenant la valeur 0x08

Il ne reste plus qu’à reproduire ce paquet dans le programme utilisant la bibliothèque libusb. Encore une fois, Wireshark indique de précieuses informations :

  • le paquet est de type URB_INTERRUPT out ;
  • le paquet contient 64 octets de données.

Ce qui donne le code suivant :

 
Sélectionnez
uint8_t data[] = { 0x11, 0x83, 0x00, 0x00, volume, 0x00, 0x00, 0x00,
                   0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                   0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                   0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                   0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                   0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                   0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                   0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00 };
int transferred = 0;
int res = libusb_interrupt_transfer(keyboard, endpoint, data.data(), data.size(), &transferred, 0);
if(res != 0)
{
    show_error("Failed to send data\n",res);
}
fprintf(stdout, "Transferred: %d\n", transferred);

Toutefois, en tant que tel, cela ne fonctionne pas. Une piste est de reproduire les différents échanges (principalement, les envois de données du PC vers le clavier) jusqu’à trouver la succession de paquets permettant de changer le volume.

Ce qui nous amène au code suivant :

 
Sélectionnez
void bulkTransfert(libusb_device_handle* keyboard, int endpoint, std::vector<uint8_t>& data)
{
    fprintf(stdout, "Send bulk (%ld)\n", data.size());
    int actual_length = 0;
    int res = libusb_bulk_transfer(keyboard, endpoint, data.data(), data.size(), &actual_length, 0);
    if(res != 0)
    {
        show_error("Failed to send data\n",res);
    }
    fprintf(stdout, "Transferred: %d\n", actual_length);
}

bool sendVolume(libusb_device_handle* keyboard, uint8_t volume)
{
    assert(keyboard);

    std::vector<uint8_t> prep_data = { 0x11, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00
                       };

    std::vector<uint8_t> volume_packet = { 0x11, 0x83, 0x00, 0x00, volume, 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00
                       };

    std::vector<uint8_t> buf(64, 0);

    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, prep_data);
    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, prep_data);
    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, prep_data);
    bulkTransfert(keyboard, 0x84, buf);

    bulkTransfert(keyboard, 0x05, volume_packet);

    return true;
}

Ce code n’est qu’une preuve du concept. Il fonctionne, mais il reste beaucoup d’inconnues, telles que :

  • pourquoi il est nécessaire d’envoyer trois fois les paquets [0x11, 0x14] avant d’envoyer le paquet contenant le volume ;
  • comment envoyer d’autres informations (l’heure, le nombre d’actions par minute) ;
  • quelles sont les informations que le clavier envoie au PC.

III. Horloge

Comme pour le volume, l’horloge du dock média reçoit l’heure depuis le PC. En effet, le clavier ne possède aucun équipement pour sauvegarder l’heure actuelle. Par conséquent, cela signifie que l’ordinateur doit envoyer l’heure et la date au clavier lors du branchement de ce dernier.

Maintenant que nous avons une meilleure compréhension (certes, imparfaite) des échanges entre l’ordinateur et le clavier, il devient plus simple de retrouver trace des paquets intéressants. Dans un tel cas, il est nécessaire d’effectuer deux captures afin d’avoir deux traces possédant deux temps différents. L’idée est d’arrêter la capture lorsque le dock média se met à jour avec l’heure correcte.

Pour la capture, il était 18:31. On doit donc trouver 12 et 1F et c’est exactement le cas :

Image non disponible

Cela aurait pu être plus compliqué si pour une raison quelconque, les données étaient optimisées pour utiliser le moins de bits possible ou même chiffrées.

Ce qui donne la fonction suivante :

 
Sélectionnez
bool sendTime(libusb_device_handle* keyboard, unsigned char month, unsigned char day, unsigned char hour, unsigned char minute)
{
    assert(keyboard);

    std::vector<uint8_t> prep_data = { 0x11, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00
                       };

    std::vector<uint8_t> time_packet = { 0x11, 0x84, 0x00, 0x01, 0x00, 0x00, month, day,
                         hour, minute, 0x01, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00
                       };

    std::vector<uint8_t> buf(64, 0);

    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, prep_data);
    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, prep_data);
    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, prep_data);
    bulkTransfert(keyboard, 0x84, buf);

    bulkTransfert(keyboard, 0x05, time_packet);

    return true;
}

Cela fonctionne. Néanmoins, le constructeur propose deux modes d’affichage de l’heure. Avec ce programme, l’affichage revient en mode horloge à cadran, et ce même si c’est l’horloge numérique qui a été configurée. Cela veut donc dire qu’il y a dans ce paquet l’information du type d’horloge à afficher.

Pour retrouver la donnée correspondante, il suffirait de tracer les paquets envoyés par le service Base Camp, lorsqu’on change le mode d’affichage. Ou bien d’analyser les paquets, une fois avec l’horloge à cadran et une fois avec l’horloge numérique.

IV. Mettre l’heure à jour lors du branchement du clavier

Il est maintenant possible de changer l’heure du clavier. Cependant, il serait préférable que le système le fasse automatiquement lorsqu’on branche le clavier (cela inclût au démarrage du PC, lorsque le clavier est pris en compte par le système).
Pour cela, il est possible, sous Linux, d’utiliser udev. Ce daemon s’occupe de la gestion des périphériques et offre un mécanisme permettant d’effectuer des actions suivant certains événements. Cela se configure au travers de règles. Au travers de celles-ci, il est possible d’indiquer une commande à exécuter lorsque udev détecte ce clavier. Le fichier, pouvant être nommé 99-MountainEverestMax.rules, à placer dans /etc/udev/rules.d, devra contenir la ligne suivante :

 
Sélectionnez
ACTION=="add", ATTRS{idVendor}=="3282" ATTRS{idProduct}=="0001" RUN+="/usr/local/bin/test.sh"

L’action « add » correspond à l’apparition d’un périphérique. Comme le périphérique peut être une clef USB, une webcam ou encore le clavier en question, il est nécessaire de vérifier aussi l’identifiant. Si toutes les conditions sont réunies, on ajoute la commande voulue à la liste des choses que udev doit exécuter.

Une fois la règle ajoutée, il faut demander à udev de se recharger afin de prendre en compte la nouvelle règle avec la commande suivante :

 
Sélectionnez
sudo udevadm control --reload

Pour que cela soit complet, tout en gardant la solution simple, le programme C envoyant les données au clavier acceptera une série d’arguments. Ces derniers sont : le numéro du jour, le numéro du mois, l’heure et les minutes. Par conséquent, nous pouvons demander à udev d’exécuter le script qui suit :

 
Sélectionnez
#!/bin/bash

/usr/local/bin/setKeyboardTime `date "+%m %d %H %M"`

Ainsi, le programme C permettant d’envoyer la date au clavier ne s’occupe que de cela. La récupération de la date est effectuée par un programme dédié et reconnu. Par ailleurs, il est très important de valider les arguments (conversion des chaînes de caractères avec sscanf, vérification de la valeur retournée par sscanf et vérification que les valeurs spécifiées sont valides).

Le programme setKeyboardTime est, évidemment, placé à côté du script.

V. L’heure ne change pas

Même si l’ordinateur envoie l’heure au clavier, celle-ci ne se met pas à jour automatiquement. La seule alternative possible est de naviguer dans les menus pour sortir de l’horloge et revenir dessus. Dans un tel cas, l’heure actuelle s’affichera, mais en restant fixe.

Évidemment, ce problème n’existe pas sous Windows. Il manque donc quelque chose dans le programme. Si l’on suit à nouveau les paquets échangés entre l’ordinateur et le clavier, on remarque un paquet revenant régulièrement :

Recherche d'autres paquets intéressants

Ce paquet est envoyé toutes les secondes. Serait-il d’une quelconque utilité pour que le clavier puisse suivre le temps. Pour vérifier cela, il suffit d’implémenter une boucle envoyant le paquet toutes les secondes et ce, pendant deux minutes. Toutefois, cela ne règle pas le problème.

Encore une fois, en capturant les paquets, on peut analyser la suite des événements. Notamment, avant le paquet définissant l’heure, il y a deux autres paquets particuliers :

Ce qui donne l’enchaînement suivant :

 
Sélectionnez
bool sendTime(libusb_device_handle* keyboard, unsigned char month, unsigned char day, unsigned char hour, unsigned char minute)
{
    assert(keyboard);

    std::vector<uint8_t> prep_data = { 0x11, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00
                       };

    std::vector<uint8_t> empty_time = { 0x11, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00
                       };

    std::vector<uint8_t> unknown_time = { 0x11, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00
                       };

    std::vector<uint8_t> time_packet = { 0x11, 0x84, 0x00, 0x01, 0x00, 0x00, month, day,
                         hour, minute, 0x01, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00,
                         0x00, 0x00, 0x00, 0x00, 0x00  , 0x00, 0x00, 0x00
                       };

    std::vector<uint8_t> buf(64, 0);

    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, prep_data);
    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, prep_data);
    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, prep_data);
    bulkTransfert(keyboard, 0x84, buf);

    bulkTransfert(keyboard, 0x05, unknown_time);

    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, empty_time);

    bulkTransfert(keyboard, 0x84, buf);
    bulkTransfert(keyboard, 0x05, time_packet);

    return true;
}

Et avec cet enchaînement, le clavier affichera l’heure et mettra à jour l’horloge au fil du temps.

Cependant, même si le résultat est fonctionnel, la fonction de chaque paquet n’est toujours pas définie. Pour éclaircir cela, il faudra analyser plus de paquets et suivre les échanges liés aux autres fonctionnalités proposées par Base Camp.

VI. Conclusion

Au cours de cet article, il a été abordé la façon dont on pouvait analyser un logiciel communiquant avec le clavier afin de reproduire ce comportement. Ainsi, deux fonctionnalités ont été réintroduites avec succès sous Linux alors que cela n’est pas supporté officiellement.
Cela a été possible grâce à l’utilisation de Wireshark permettant la capture des paquets échangés entre l’ordinateur et le clavier. Ensuite, à l’aide de la bibliothèque libusb, il est possible de créer un programme reproduisant ces échanges. Le volume sonore du PC ainsi que l’heure sont maintenant affichés comme prévu, et ce même si le clavier est branché à un ordinateur sous Linux.

Évidemment, il reste un travail demandant de la patience afin de reproduire le reste des fonctionnalités proposées par le logiciel du constructeur.

Finalement, même si le procédé semble aisé à la lecture de l’article, chaque étape est une victoire en soi. En effet, réussir à envoyer les bons paquets demande de la patience, car même en reproduisant les paquets affichés dans Wireshark, cela fonctionne rarement au premier essai.

VII. Remerciements

Logo Mountain

Cet article n'aurait pas pu voir le jour sans le support de Mountain. Vous pouvez retrouver le clavier Everest Max dans la boutique officielle de Mountain.
Developpez.com et moi-même tenons à remercier Mountain.

Je souhaite aussi remercier f-leb pour la relecture technique et escartefigue pour la relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

En complément sur Developpez.com

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2022 Alexandre Laurent. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.