pédalier USB et reverse ingineering

Published: 17-11-2015

Updated: 02-03-2018

By: Maxime de Roucy

tags: python usb

J’ai récemment acheté un pédalier USB FS3_P de PC sensor. Ce pédalier peut émuler à peut près tous les scancodes et peut déclencher la sésie de plusieurs scancode à la suite. Je ne sais pas quelle est le nombre de scancode maximum que l’on peut générer. Le problème est que le logiciel de configuration tourne uniquement sous Windows et surtout qu’il ne permet pas de spécifier n’importe quel scancode. Je vais expliquer ici comment j’ai « reverse ingineeré » le protocole de communication avec le device et écrit un petit script python pour pouvoir le configurer depuis mon laptop sous Archlinux mais surtout avec n’importe quel scancode.

Ici lorsque je parle de « scancode », je parle de scancode USB.

J’ai d’abord installé VirtualBox et activé l’extension (modprob vboxdrv) me permettant de binder le pédalier sur la machine virtuel. Pour mes tests j’ai utilisé une VM de développement/test fourni gratuitement par Microsoft. Ces VM sont initialement destinée à tester les différentes versions d’IE.

Wireshark permet de capturer les données transitant sur de l’USB. Pour ce faire il faut charger le module usbmon. L’interface USB aparaissé alors dans le menu de Wireshark (lancé en root… je sais, c’est pas bien) en temps que « usbmon1 ».

Le truc à bien observer dans la communication est « Leftover Capture Data », je vous conseille créer un colone correspondant à cette élément dans votre interface Wireshark. Il s’agit du payload du « paquet » USB.

J’ai installé le soft de configuration sur la VM Windows, fait plusieurs tests. Puis j’ai fait des configuration très simple pendant que je capturé les traces USB avec Wireshark.

J’ai par exemple obtenu la trace suivante pour un changement de configuration (avec un layout qwerty) :

No. Source Destination Leftover Capture Data Info
1 host 7.0 GET DESCRIPTOR Request DEVICE
2 7.0 host GET DESCRIPTOR Response DEVICE
3 host 1.0 GET DESCRIPTOR Request DEVICE
4 1.0 host GET DESCRIPTOR Response DEVICE
5 host 7.0 0183080000000000 URB_CONTROL out
6 7.0 host URB_CONTROL out
7 7.2 host 466f6f7453776974 URB_INTERRUPT in
8 host 7.2 URB_INTERRUPT in
9 7.2 host 63683346312e3274 URB_INTERRUPT in
10 host 7.2 URB_INTERRUPT in
11 host 7.0 0182080100000000 URB_CONTROL out
12 7.0 host URB_CONTROL out
13 7.2 host 0881000400000000 URB_INTERRUPT in
14 host 7.2 URB_INTERRUPT in
15 host 7.0 0182080200000000 URB_CONTROL out
16 7.0 host URB_CONTROL out
17 7.2 host 0881000500000000 URB_INTERRUPT in
18 host 7.2 URB_INTERRUPT in
19 host 7.0 0182080300000000 URB_CONTROL out
20 7.0 host URB_CONTROL out
21 7.2 host 0881000600000000 URB_INTERRUPT in
22 host 7.2 URB_INTERRUPT in
23 host 7.0 0180080100000000 URB_CONTROL out
24 7.0 host URB_CONTROL out
25 host 7.0 0181060100000000 URB_CONTROL out
26 7.0 host URB_CONTROL out
27 host 7.0 0604141a08150000 URB_CONTROL out
28 7.0 host URB_CONTROL out
29 host 7.0 0181080200000000 URB_CONTROL out
30 7.0 host URB_CONTROL out
31 host 7.0 0801000000000000 URB_CONTROL out
32 7.0 host URB_CONTROL out
33 host 7.0 0181080300000000 URB_CONTROL out
34 7.0 host URB_CONTROL out
35 host 7.0 0801000000000000 URB_CONTROL out
36 7.0 host URB_CONTROL out

Après analyse on imagine le déroulement suivant :

J’ai lu certains articles intéressant sur le protocole USB :

J’ai utilisé la bibliothèque pyusb et les exemples de sa documentation.

Après de nombreux essais/erreurs j’ai pu produire un petit script python permettant de configurer le pédalier avec les chaines de caractères (qwerty) :

Celui-ci nécessite les droits d’écriture sur le device usb. Personellement je le lance en temps que « root » via sudo mais on peut faire plus propre (j’ai juste eu la flemme de chercher).

#!/usb/bin/python3

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import usb.core
import usb.util
import sys
import time

# find our device
dev = usb.core.find(idVendor=0x0c45, idProduct=0x7403)

# was it found?
if dev is None:
    raise ValueError('Device not found')

for cfg in dev:
    for intf in cfg:
        if dev.is_kernel_driver_active(intf.bInterfaceNumber):
            try:
                dev.detach_kernel_driver(intf.bInterfaceNumber)
            except usb.core.USBError as e:
                sys.exit("Could not detach kernel driver from interface({0}): {1}".format(intf.bInterfaceNumber, str(e)))

dev.set_configuration()

msg = [0x01, 0x83, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)

dev.read(0x82, 16, 100)

time.sleep(1)
msg = [0x01, 0x82, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
dev.read(0x82, 32, 1000)
time.sleep(1)

msg = [0x01, 0x82, 0x08, 0x02, 0x00, 0x00, 0x00, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
dev.read(0x82, 32, 100)
time.sleep(1)

msg = [0x01, 0x82, 0x08, 0x03, 0x00, 0x00, 0x00, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
dev.read(0x82, 32, 100)
time.sleep(1)

msg = [0x01, 0x80, 0x08, 0x01, 0x00, 0x00, 0x00, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
time.sleep(1)

# sometime you have to adapte (lower) "number_of_scancode"
# else the footswitch generate unwanted shift scancode at
# the end of the of the generation sequence

##
# slot 1
##

slot = 1
# 5 → 4
number_of_scancode = 4
msg = [0x01, 0x81, number_of_scancode + 2, slot, 0x00, 0x00, 0x00, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
time.sleep(1)

# 12qq[enter]
#                                     1     2     q     q   [enter]
msg = [number_of_scancode + 2, 0x04, 0x59, 0x5a, 0x14, 0x14, 0x28, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
time.sleep(1)

##
# slot 2
##

slot = 2
number_of_scancode = 10
msg = [0x01, 0x81, number_of_scancode + 2, slot, 0x00, 0x00, 0x00, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
time.sleep(1)

# su - test[enter]
#                                     s     u           -           t
msg = [number_of_scancode + 2, 0x04, 0x16, 0x18, 0x2c, 0x2d, 0x2c, 0x17]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
time.sleep(1)
#       e     s     t   [enter]
msg = [0x08, 0x16, 0x17, 0x28, 0x00, 0x00, 0x00, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
time.sleep(1)

##
# slot 3
##

slot=3
# 14 → 12
number_of_scancode = 12
msg = [0x01, 0x81, number_of_scancode + 2, slot, 0x00, 0x00, 0x00, 0x00]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
time.sleep(1)

# qQqq1230xaPas5[enter]
#                                     q     Q     q     q     1     2
msg = [number_of_scancode + 2, 0x04, 0x14, 0x94, 0x14, 0x14, 0x59, 0x5a]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
time.sleep(1)
#       3     q     Q     q     Q     q     4   [enter]
msg = [0x5b, 0x14, 0x94, 0x14, 0x94, 0x14, 0x5c, 0x28]
dev.ctrl_transfer(0x21, 9, 0x200, 1, msg)
time.sleep(1)


dev.reset()
usb.util.dispose_resources(dev)

for cfg in dev:
    for intf in cfg:
        if dev.is_kernel_driver_active(intf.bInterfaceNumber):
            try:
                dev.attach_kernel_driver(intf.bInterfaceNumber)
            except usb.core.USBError as e:
                sys.exit("Could not attach kernel driver from interface({0}): {1}".format(intf.bInterfaceNumber, str(e)))

Si vous voulez le réutiliser pour configérer le pédalier avec vos propre chaines de caractères je vous encourage à lire la page « Yubikey, bépo et esperluette » qui indique comment récupérer les scancodes des touches. Comme pour la Yubikey, il faut ajouter « 0x80 » au scancode pour que le pédalier produise un appui/relachement de la touche shift avant et après la touche correspondante.

Je n’ai pas encore trouvé pourquoi et de quel manière il faut diminuer la valeur du nombre de scancode par rapport à la réalité. Si vous utilisé ce script, vérifiez que le pédalier produit bien les scancodes désiré et modifiez le nombre de scancodes si celui-ci produit trop de shift. En effet, si j’utilise le nombre de scancodes réel, le pédalier produit en fin de séquence des appuis (sans relachement) de la touche shift.