ex0ns

about
A Computer Science Student's blog, about programming, algorithms, hack and a lot more.

Analyse d'un protocole : BeeWi Helipad

16 Sep 2012

Notre cible est l'application “Beewi Heli” qui permet de controler un hélicopètre par bluetooth depuis son téléphone. Nous allons adapter l'application afin de pouvoir controler ce petit hélicoptère, en C++ (ce sera l'occasion d'étudier la gestion du bluetooth en C++).

==Nous aborderons les thèmes suivants : == * Décompilation et analyse d'une application Android * Gestion du bluetooth sur linux * Progammation et module bluetooth en C++ et Python

Analyse de l'application

Pour commencer, on se rend compte que si on lance l'application, notre ordinateur (avec le bluetooth allumé et détéctable) n'apparait pas dans la liste des periphériques, il saute au yeux que l'application effectue un tri sur les périphériques bluetooth avant de les afficher, afin vérifier cela, il va nous falloir : - jd_gui, un décompilateur Java - dex2jar, pour transformer le .apk en .java (code source java plus facilement lisible)

Le but de cet article n'est pas de vous montrer comment récupérer l'apk, il faut le récupérer depuis votre téléphone dans le dossier /data/app/ (explications SO) Il faut maintenant transformer ce .apk en .java pour pouvoir voir ce qu'il contient, pour cela, rien de plus simple:

sh d2j-dex2jar.sh beewi.apk 

vous sortira un magnifique fichier java que nous allons pouvoir analyser avec jd_gui :

./jd_gui beewi-dex2jar.java

Sur la gauche, s'affiche la liste des fichiers présents dans l'application nous allons nous interesser au fichier “SelectDeviceActivity” (n'oubliez pas que, pour le moment, la seule chose qui nous interresse est de comprendre le filtrage qu'effectue l'application).

Attention, ceci est un code décompilé et il peut y avoir des parties complétements étranges dans le code, le but n'étant pas de le modifier, mais de comprendre son fonctionnement, nous pouvons quand même tirer les grandes lignes de chaque fonction et en déceler le fonctionnement.

La fonction qui attire mon attention (et sûrement la votre) est

private static boolean isBluetoothCar(BluetoothDevice paramBluetoothDevice)

Une voiture ? Je pensais que l'on cherchait un hélicopètre ? Oui, mais une recherche sur le site de Beewi et on se rend compte qu'avant de developper un hélicoptère, la société vendait des voitures, en utilisant probablement un code similaire.

Cette fonction, pas très complexe à comprendre, ne se résume en fait qu'a une seul ligne, la dernière :

if (localBluetoothClass.getDeviceClass() == 1028);

La fonction vérifie que la classe du périphérique est bien 1028.

Un petit tour sur la doc android : http://developer.android.com/reference/android/bluetooth/BluetoothClass.Device.html nous apprend que 1028 (0x00000404) correspond à un appareil du type AUDIOVIDEOWEARABLEHEADSET_, ce n'est qu'un détail, mais la valeur héxadécimale est, elle très importante. Afin de vérifier notre hypothèse, modifions la classe du bluetooth de notre PC afin de voir s'il est découvert par l'application.

sudo hciconfig hci0 class 0x00000404 # hci0 est notre adaptateur bluetooth

Relancez l'application... Surprise ! Notre PC apparait bien dans la liste des appareils avec lequel on peut effectuer une connexion.

Petite astuce: si vous souhaitez modifier la classe de votre ordinateur, même après un redémarrage, il vous faudra éditer le fichier /var/lib/bluetooth/XX:XX:XX:XX:XX/config.

Nous aurions pu faire autrement (pour éviter d'avoir à décompiler l'application), nous aurions pu scanner l'hélicoptère afin de connaitre sa classe et de simplement la copier, cependant, cette technique ne nous donne pas la même classe, en effet: Pour scanner l'hélicoptère :

hcitool inq

Qui retourne une sortie plus ou moins similaire à

quiring ...
XX:XX:XX:XX:XX:XX   clock offset: 0x5cee    class: 0x240404

Ainsi, la classe de notre hélicoptère est égale à 0x240404.

Serveur bluetooth

Il faut maintenant que l'on code un serveur dont le rôle sera d'écouter les requêtes envoyées par l'application afin de comprendre le protocol et pouvoir forger nos propres requêtes. Le developpement d'une application bluetooth en C ou C++ étant lourd, et relativement complexe à faire, nous utiliserons donc python, avec la bibliothèque bluez (pour python...), pour effectuer nos tests, l'application finale, elle, sera codée en C++.

Vous l'aurez donc compris, l'hélicoptère fait office de serveur, et l'application, client se connecte à ce serveur, mais comment le client reconnait-t'il le serveur ? Sur quel port doit-il se connecter ? Afin de pouvoir répondre à ces questions, il va falloir se plonger un peu dans la documentation de PyBluez, la librairie bluetooth en Python Premièrement, il va falloir faire un scan pour detecter les appareils alentours (nous pourrions faire un scan avec hcitool, mais je souhaite développer un peu la programmation en python). Pour lister les appareils aux alentours, il faut utiliser la fonction discover_devices, sur la documentation, on apprend que le type de variable retourné par la fonction est une liste de tuple sous la forme (address, name).

La seconde fonction qui va nous être utile ici, est find_service, qui prend en paramètre une adresse, et/ou un uuid et/ou un nom, et qui retourne une liste de dictionnaires, avec différentes informations (énumérées dans la documentation), ainsi, la première fonction retourne l'adresse et le nom, nous sera utile pour appeller la seconde fonction, qui se chargera de mettre en évidence les différents services disponibles en bluetooth sur l'appareil. Dans les dictionnaires, seules les informations name, protocol et port nous seront utiles. Ainsi, le scanneur ressemble à quelque chose tel que :

# -*- coding: utf8 -*-
import bluetooth

devices = bluetooth.discover_devices(flush_cache=True, lookup_names=True);
for addr, name in devices:
    for service in bluetooth.find_service(address=addr):
        print 'Nom : %s '  % (service['name'])
        print ' Protocol : %s ' % (service['protocol'])
        print ' Port : %s ' % service['port']
    print ''

Ce petit bout de code, devrait sortir en retour quelque chose tel que :

XX:XX:XX:XX:XX:XX -> BeeWi BBZ301
 Nom : None
 Protocol : L2CAP
 Port : 1
 Nom : SerialPort
 Protocol : RFCOMM
 Port : 6

On voit qu'il y a deux services sur l'hélicoptère, un sur le port 1 et l'autre sur le port 6, c'est ce dernier qui va nous interesser (protocol RFCOMM). N'oubliez pas que notre but pour le moment est d'émuler un serveur afin de récupérer les commandes envoyés par le client, il faut donc créer un serveur qui écoutera les connexions entrantes sur le port 6, et fera une sauvegarde des données recues.

Encore une fois, un petit tour sur la documentation pour se renseigner sur la classe BluetoothSocket ainsi que sur les fonctions bind, listen, advertiseservice_ et le couple accept/recv (si vous avez déjà manipulé des sockets, vous devriez connaitre ces fonctions). Nous je m'attarderai pas sur toutes les fonctions volontairement, mais si vous avez des questions, n'hésitez pas à m'en faire part dans les commentaires.

La première fonction relativement complexe à utiliser est advertise_service, qui prend en paramètre une socket écoutant sur un port (bind et listen), le nom du service (qui devra être celui de l'hélicoptère), service_classes, qui nous initialiserons comme indiqué dans la documentation à SERIALPORTCLASS et le profile, que nous laisserons également comme indiqué dans la documentation, c'est à dire à SERIALPORTPROFILE.

Le reste est relativement simple, on créer une socket bluetooth du type RFCOMM (comme celui de l'hélicoptère), il faut ensuite binder cette socket sur un port (le 6 dans notre cas) grâce à la fonction bind() et puis, il faut mettre la socket en écoute, à l'aide de listen(), ensuite, on créer un service comme expliqué précédement, et on accepte la première connexion entrante (en créant une nouvelle socket). Pour finir, on fait une boucle infinie, dans laquelle on recoit des données du clients grâce à la fonction recv().

Le code final ressemblerait donc à quelque chose comme ca :

# -*- coding: utf8 -*-
import bluetooth

server=bluetooth.BluetoothSocket( bluetooth.RFCOMM )
 # Car le serveur de l'hélicoptère utilise ce protocol)
server.bind(("",6))
 # Bind sur le port 6
server.listen(1)
  # Attente d'une connexion (et une seule)

bluetooth.advertise_service(server, "SerialPort",
                                service_classes=[bluetooth.SERIAL_PORT_CLASS],
                                profiles=[bluetooth.SERIAL_PORT_PROFILE])
 # Création d'un service similaire à celui de l'hélicoptère

client, address = server.accept()
 # On accepte la première connexion 
print "Connection depuis : ",address

while client:
        data = client.recv(1024)
        print "Données recues : [%s]" % data
        stop = raw_input()


client_sock.close()
server_sock.close()

En le lancant et en connectant l'application, on se rend compte que les requetes ont la forme: Données recues : [0x1500000000FF] Et que cette valeur change en fonction des différentes variables (vitesse, hauteur, inclinaison)

Je sais que certains d'entre vous sont déçus car je n'ai pas tenu ma promesse (bluetooth en C++), alors je me suis penché sur le problème qu'est la bibliothèque Bluez en C++, la documentation de celle-ci n'existant tout simplement pas, vous imaginez que ca ne facilite pas les choses... Enfin voilà, après quelques jours de recherches (principalement sur les sites : people.csail.mit.edu et btessentials.com. Après avoir bataillé des heures, voici une application fonctionelle permettant de faire exactement la même chose que le code python juste au dessus.

/*
 * Ecrit par : Ex0ns
 * Site : ex0ns.me
 * Sources : http://people.csail.mit.edu/albert/bluez-intro/x604.html et http://people.csail.mit.edu/rudolph/Teaching/Articles/BTBook.pdf
 * Bibliothèque utilisées : bluez
 * Compilation : g++ advertise.cpp -o advertise -lbluetooth
 */

#include <iostream> 
#include <unistd.h> // int close(int fd)
#include <sys/types.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/sdp.h>  // Fonction sdp_* (pour créer un service bluetooth)
#include <bluetooth/sdp_lib.h> // sdp_session, sdp_close
#include <bluetooth/rfcomm.h> // struct sockadd_rc

#define BDADDR_ANY_INITIALIZER   0 // Définition pour régler un prolème de temporary addresses lié à l'utilisation de BDADDR_ANY
#define BDADDR_LOCAL_INITIALIZER 0 // De même mais pour BDADDR_LOCAL

using namespace std;

/*
 * Cette fonction, plutot complexe, permet d'ajouter un service à la liste des services disponible,
 * C'est l'équivalent de la fonction bluetooth.advertise_service() en python
 * La documentation sur la bibliothèque Bluez étant inexistante, cette fonction peut contenir des erreurs, mais est fonctionnelle.
 */
sdp_session_t *advertise_service()
{
    uint8_t rfcomm_port = 6; // Numéro de Port
    const char *service_name = "SerialPort"; // Nom du service
    uuid_t root_uuid, l2cap_uuid, rfcomm_uuid,  svc_class_uuid;
    sdp_list_t *l2cap_list = 0,
               *rfcomm_list = 0,
               *root_list = 0,
               *proto_list = 0, // Contient tous les protocoles utilisés par le service (RFCOMM et L2CAP)
               *access_proto_list = 0,
               *svc_class_list = 0,
               *profile_list = 0;
    sdp_data_t *channel = 0;
    sdp_profile_desc_t profile;
    sdp_record_t record = { 0 };
    sdp_session_t *session = 0;

    // Informations sur la classe, équivalent au paramètre  service_classes=[bluetooth.SERIAL_PORT_CLASS] dans le code python
    sdp_uuid16_create(&svc_class_uuid, SERIAL_PORT_SVCLASS_ID);
    svc_class_list = sdp_list_append(0, &svc_class_uuid);
    sdp_set_service_classes(&record, svc_class_list);

    // Informations sur le profile, équivalent au paramètre profiles=[bluetooth.SERIAL_PORT_PROFILE]) dans le code python
    sdp_uuid16_create(&profile.uuid, SERIAL_PORT_PROFILE_ID);
    profile.version = 0x0100;
    profile_list = sdp_list_append(0, &profile);
    sdp_set_profile_descs(&record, profile_list);


    // Rendre le service publique et detectable
    sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); 
    root_list = sdp_list_append(0, &root_uuid);
    sdp_set_browse_groups( &record, root_list );

    // Informations sur le protocol L2CAP (obligatoire pour la connexion, je n'ai toujours pas trouvé pourquoi)
    sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID);
    l2cap_list = sdp_list_append( 0, &l2cap_uuid );
    proto_list = sdp_list_append( 0, l2cap_list ); // Ajout du L2CAP à la liste des protocoles du service

    // Création de la socket RFCOMM
    sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID);
    channel = sdp_data_alloc(SDP_UINT8, &rfcomm_port);
    rfcomm_list = sdp_list_append( 0, &rfcomm_uuid );
    sdp_list_append( rfcomm_list, channel ); // Précise quel port (canal) utiliser

    sdp_list_append( proto_list, rfcomm_list ); // Ajout du protocol RFCOMM à la liste de ceux du service
    access_proto_list = sdp_list_append( 0, proto_list );
    sdp_set_access_protos( &record, access_proto_list );

    // Phase finale de création du service, on donne le nom, et accesoirement une description et un "fourniseur"
    sdp_set_info_attr(&record, service_name, NULL, NULL);


    // Il faut maintenant contacter le serveur SDP local pour créer et enregister un nouveau service
    bdaddr_t any  = BDADDR_ANY_INITIALIZER; // Voir explications (commentaires suivants)
    bdaddr_t local = BDADDR_LOCAL_INITIALIZER; // De même
    session = sdp_connect(&any, &local, 0 ); // On contact le serveur sdp
    /*
     * Alors là, petit particularité, en théorie, la fonction sdp_connect s'appelle de la façon suivante :
     * session = sdp_connect(BDADDR_ANY, BD_ADDR_LOCAL, 0 );, mais en faisant ca, on obtient une erreur à la compilation,
     * Pour corriger on déclare deux constantes au début, BDADDR_ANY_INITIALIZER et BDADDR_LOCAL_INITIALIZER, que nous utiliserons
     */
    sdp_record_register(session, &record, 0); // On ajoute notre service à la liste des services

    // Il faut maintenant faire un peu de nettoyage dans nos structures
    sdp_data_free( channel );
    sdp_list_free( l2cap_list, 0 );
    sdp_list_free( rfcomm_list, 0 );
    sdp_list_free( root_list, 0 );
    sdp_list_free( access_proto_list, 0 );

    return session;
}

int main(){
    /*
     * Création d'un service bluetooth et écoute des données envoyées par le client
     * Pour reverse un protol BeeWi Helipad
     * Voir : http://ex0ns.hostei.com/article-6-Analyse-d-un-protocole-BeeWi-Helipad pour plus d'informations
     */
    int sock, client; // Nos deux sockets (client et serveur)
    socklen_t addrlen; // Taille de la structure qui contiendra les informations clients
    unsigned char buff[14];
    struct sockaddr_rc addr; // Structure des informations clients

    sock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); // Création d'une socket bluetooth

    addr.rc_family = AF_BLUETOOTH; // On précise la famille (tout comme pour INET ou INET6)
    bdaddr_t any = BDADDR_ANY_INITIALIZER; // Même problème, vois explications dans la fonction ou au niveau de #define
    bacpy(&addr.rc_bdaddr, &any);
    addr.rc_channel = 6; // Port utilisé

    bind(sock, (struct sockaddr *)&addr, sizeof(addr)); // Rien de très étonnant
    listen(sock, 1); // On écoute un client

    sdp_session_t *session = advertise_service(); // On crée notre service, sur le même port
    addrlen = sizeof(addr); 
    client = accept (sock, (struct sockaddr *)&addr, &addrlen); // Création de la socket client
    while(client){ // tant que le client est connecté
        recv(client, buff, 14, 0);
        cout << buff;
    }
    close (client); // Fermeture des sockets
    close (sock);
    sdp_close(session); // Fermeture du service 
    return 0;
}

Notez que vous pouvez également trouver ce code, mieux présenté, sur pastebin.

Analyse des données

Nous pouvons déjà remarquer les deux premiers bytes et les deux derniers semblent être fixes, nous les appellerons maintenant prefixe pour 0x15 et suffixe pour 0xFF. Lançons le serveur, et jouons un peu sur l'application pour voir quelles sont les informations modifiées lorsque l'on accelère. On se rend compte qu'en fonction de l'accélération, les données passent de 0x1500000000FF à 0x15FF000000FF donc, que la valeur varie entre 0x00000000FF et 0xFF000000FF (le but va être de créer un variable pour chaque donnée et de recomposer la valeur finale avant de l'envoyer). La vitesse du trimmer (stabilisation), elle, peut, d'après l'application, varier entre 0x14000000f8FF et 0x17000000f8FF, on peut également noter si la vitesse du trimmer est nulle, alors le préfixe vaut 0x15, quand la vitesse est positive (sens de rotation positif), le préfixe passe à 0x17, et si le sens de rotation est contraire, alors le préfixe devient 0x14. On remarque que les deux avant derniers chiffres varient en fonction de la vitesse, à une echelle de 0x08, ainsi, la gestion de ces deux paramètres est facile à implementer, mais il reste la plus complexe, celle de la direction, et c'est pas du gateau (il faut d'ailleurs que je finisse de comprendre le fonctionnement avant d'écrire la fin de cet article).

Edit du 11/10/12

Voici une petite interface de contrôle, alors j'entend par petit, basique, en effet, écran noir, 800x600, adresse mac dans le code source, pas très sympa, mais fonctionnelle. Elle permet de controler le rotor et le trimmer de l'hélicoptère assez facilement et donne un petit apercu de la SFML (malheureusement, la vitesse n'est toujours pas manipulable). Sans plus attendre, voici le code (disponible sur pastebin).

/*
    Ultimate BeeWi HeliPad controler
    Used Lib : sfml-1.6, bluez
    Author : ex0ns (ex0ns.hostei.com)
    Langage : C++
    Compilation : g++ controler.cpp -o controler -lbluetooth -lsfml-window -lsfml-system
*/
#include <iostream>
#include <sstream>

#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>

#include <SFML/System.hpp> // SFML 
#include <SFML/Window.hpp>
#include <SFML/Network.hpp>

typedef long long int lli;

const char *intToChar(lli number){
    /* This Function is use to create an hex string from a number), so it can be pass as parameter to the socket */
    std::stringstream ss(std::stringstream::in | std::stringstream::out);
    ss << "0x" << std::hex << number;
    return ss.str().c_str();
}

int main()
{
    /* Variables For the Bluetooth Connection */
    struct sockaddr_rc addr = {0};
    char server[18] = "00:11:67:E3:DE:63"; // MAC adress of our Helicoptere
    int s, status; // Two socket handler

    /* Variables For the Flying Monitor */
    lli prefixe = 0x150000000000; // See the article for more details
    lli suffixe = 0x0000000000FF;
    lli firstRotorSpeed = 0x0000000000;
    lli trimmerValue = 0x0000;

    lli firstRotorSpeedCoeff = 0x0500000000; // Rotor speed increase
    lli trimmerCoeff = 0x0800; // Trimmer Modification

    lli command; // Final Command sent to the Heli
    int trimmerPosition = 0; // 0 = Null, 1 = Positif (right), -1 (negative)

    /* Handle Bluetooth Connection */
    s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); // See the article for more details
    addr.rc_family = AF_BLUETOOTH;
    addr.rc_channel = (uint8_t)6;
    str2ba(server, &addr.rc_bdaddr);

    status = connect(s, (struct sockaddr *)&addr, sizeof(addr));

    if(status == 0){
        sf::Window App(sf::VideoMode(800,600,30), "Helipad Controler"); // Just show a black screen I didn't take the time to create a beautiful nice GUI 
        sf::Clock clock;
        bool running = true;
        while(App.IsOpened()){

            sf::Event Event;
            while(App.GetEvent(Event)){
                if(Event.Type == sf::Event::Closed) // Hook the "cross" button to quit
                    App.Close();

                if(Event.Type == sf::Event::KeyPressed && (Event.Key.Code == sf::Key::PageUp)){ // Increase First Rotor Velocity
                    if(firstRotorSpeed <= 0xFF00000000-firstRotorSpeedCoeff)
                        firstRotorSpeed+=firstRotorSpeedCoeff;
                } 
                if(Event.Type == sf::Event::KeyPressed && (Event.Key.Code == sf::Key::PageDown)){ // Descrease First Rotor Velocity
                    if(firstRotorSpeed >= firstRotorSpeedCoeff)
                        firstRotorSpeed-=firstRotorSpeedCoeff;
                }

                if(Event.Type == sf::Event::KeyPressed && (Event.Key.Code == sf::Key::Subtract)){ // Reduce trimmer velocity 
                    /*
                     * The Subtract key is the '-' on the numPad
                     */
                    if(trimmerValue > trimmerCoeff && trimmerPosition == 1 ){ 
                        trimmerValue-=trimmerCoeff;
                        prefixe = 0x170000000000;                       
                    }
                    else if(trimmerValue == trimmerCoeff && trimmerPosition == 1){ 
                        trimmerValue = 0x0000;
                        prefixe = 0x150000000000;
                        trimmerPosition = 0;
                    }
                    else if(trimmerValue <= 0xF800-trimmerCoeff && trimmerPosition != 1){ 
                        trimmerValue+=trimmerCoeff;
                        prefixe = 0x140000000000;
                        trimmerPosition = -1;
                    }
                }

                if(Event.Type == sf::Event::KeyPressed && (Event.Key.Code == sf::Key::Add)){ // Increase trimmer velocity
                    /*
                     * The Add key is the '+' on numPad
                     */
                    if(trimmerValue > trimmerCoeff && trimmerPosition == -1 ){  
                        trimmerValue-=trimmerCoeff;
                        prefixe = 0x140000000000;                   
                    }
                    else if(trimmerValue == trimmerCoeff && trimmerPosition == -1){ 
                        trimmerValue = 0x0000;
                        prefixe = 0x150000000000;
                        trimmerPosition = 0;
                    }
                    else if(trimmerValue <= 0xF800-trimmerCoeff && trimmerPosition != -1){ 
                        trimmerValue+=trimmerCoeff;
                        prefixe = 0x170000000000;
                        trimmerPosition = 1;
                    }
                }
            }

            App.Display();
            if(clock.GetElapsedTime() > 0.5){ // Timer To Avoid Request Spamming
                command = prefixe + firstRotorSpeed + trimmerValue + suffixe;
                std::cout << intToChar(command) << std::endl;
                status = write(s, intToChar(command), 14); // We know that we have to send 14 bytes by analysing the requests
                clock.Reset(); // To reset timer so GetElapsedTime can work again
            }
        }
    }else{
        std::cout << "Connection Failed" << std::endl;
    }

    close(s);
    return 0;
}

Voilà, rien de bien sorcier, je projette, si je trouve le temps, de vous en proposer une plus sympa, avec deux ou trois boutons, une recherche des périphériques à porté, gestion des touches (préférences) mais le code sera tout de suite plus conséquent !