Logo Blog perso d'Ozwald

Dissection de programmes sous linux

Par Oz le - Geek
Hack LD_PRELOAD Linux Reverse Engineering algo gdb objdump

J'ai longtemps pensé que pour s'initier au reverse le chemin "standard" consistait à apprendre à débuguer ses propres programmes autrement qu'à coup de printf. Avec le recul je me dit qu'il y a beaucoup d'autres chemins, et j'ai bien envie de partager avec vous celui qui consiste à observer les programmes des autres :) J'avais déjà parlé de python-ptrace qui permet de faire des trucs très sympa, mais aujourd'hui on va découvrir ensemble beaucoup d'autres astuces d'ingénierie inverse dans le but d'analyser le fonctionnement d'une application flash. Accrochez votre ceinture et re-mobilisez toutes les connaissances de base que vous avez sur le fonctionnement d'un PC parce qu'il va y avoir de la variété dans les techniques (mal) employées dans cet article-fleuve !

Engin de chantier - Based on a Creative Common photo published by Elvert Barnes on Flickr

Un avertissement s'impose : je suis pentesteur de métier, pas réverseur, donc j'opère dans ce domaine en amateur et j'utilise certainement beaucoup de détours peu efficaces pour arriver au même résultat que ce qu'un pro obtiendrait de façon plus élégante et rapide. Donc si vous retenez quoique ce soit de cet article vous êtes maintenant prévenus : c'est de l'amateurisme et il y a certainement pleins de choses qu'il ne faut pas faire comme je vais l'exposer.

Plantons maintenant le décor. Slow Frog (un français !) expliquait en 2011 comment il avait écrit un bot en python pour jouer à sa place à un petit jeu flash consistant à tenir une pizzeria. Le jeu était simple et l'article très amusant. J'avais plus ou moins oublié cet article jusqu'à ce que je tombe sur une conférence de deux gars ayant codés des bots pour des jeux plus conséquents1 et je me suis alors dit que j'allais tenter le coup sur un jeu flash plus compliqué.

Petite digression à cette étape du récit : écrire un bot est probablement contre les CGU du jeu donc je ne vais pas le citer publiquement dans cet article (même si je doute que qui que ce soit ai envie de me chercher des noises pour avoir "triché" à un petit jeu flash). Sachez juste qu'il s'agit d'un jeu assez largement plus complexe que celui de la pizzeria, et qu'il fait intervenir un nombre non-négligeable d'information textuelle et/ou numériques.

Bref, je me suis donc dit que j'allais tenter d'écrire un bot pour ce petit jeu flash juste pour vérifier si j'en étais bien capable. La première optique emprunté était la plus intuitive : l'approche graphique. Le concept est simple : je prends un screenshot, puis je l'analyse. Pour ce faire j'ai mobilisé 3 briques logicielles de bases :

Ce qui m'a posé le plus de problème s'est avéré sans conteste la lecture des textes. En effet, la police utilisée était petite ce qui rendait la lecture difficile et les erreurs n'étaient pas rare (l'OCR se plantait environ une fois sur dix...ce qui est énorme dans le contexte du jeu qui m'intéresse). J'ai donc codé des vérification de cohérences entre chaque intervention de l'OCR, ce qui a pas mal aidé mais ne donnait toujours pas un résultat parfait. Après plusieurs mois à améliorer le système de temps en temps j'ai obtenu un taux de succès de 100% d'identifications correctes sur les icones diverses du jeu, mais toujours pas un taux de reconnaissance acceptable sur les informations textuelles. J'ai donc laissé tombé le projet, lassé de voir mon bot prendre des décisions farfelues toutes les 5mn sur la base d'information mal lues.

Plusieurs mois après je me suis remis à la tache, en décidant d'intercepter les communications entre le jeux flash et le serveur du jeu plutôt qu'en essayant de "lire" l'écran. En effet, l'information transmise par le serveur est déjà dans un format compréhensible par un logiciel, autant en profiter. Autre avantage de cette technique : les outils impliqués sont ceux que j'ai l'habitude de manier lorsque je fais des pentests web. Aussitôt pensé aussitôt fait : j'intercale un proxy d'interception entre mon navigateur web et internet. Déception : le jeu refuse de démarrer. Je passe le proxy en transparent : même problème. Je fabrique ma propre autorité de certification racine, je l'intègre aux autorité de confiance de mon navigateur, puis de mon OS, et je l'utilise pour générer dynamiquement les certificats SSL bidons à présenter au navigateur4. Hélas, j'ai toujours le même problème : le jeu flash refuse catégoriquement de contacter le serveur de jeu via mon proxy. Tristesse, déception, et incompréhension.

Qu'à celà ne tienne, je n'ai qu'à descendre plus bas dans l'OS pour intercepter ces communications réseau ! L'astuce à laquelle je pense alors est celle de LD_PRELOAD. Sous linux la variable d'environnement LD_PRELOAD permet de forcer le chargement d'une librairie partagée avant toutes les autres, ce qui autorise la surcharge de fonctions issues des librairies légitimes. Par exemple si je code une fonction printf puis que je la compile dans une librairie partagée et que je force le chargement de cette librairie en priorité via LD_PRELOAD, l'ensemble des processus que je lancerai ensuite feront appel à mon implémentation de printf plutôt qu'à celle de la libc :) L'idée derrière cette astuce c'est de surcharger les fonctions de communication réseau pour intercepter les informations envoyées et reçues dans le tunnel SSL5.

Pour mettre en place cette astuce il va falloir que j'identifie quelles sont les fonctions susceptibles d'êtres utilisées par le jeu pour ses communications. Normalement la commande ltrace permet de tracer (i.e. afficher) l'ensemble des appels à des fonctions de librairies partagées qu'un programme effectue. L'utilisation aurait donc été immédiate dans mon cas : je lance ltrace sur le processus du plugin-container de firefox faisant tourner le jeu, je cherche dans les fonctions qui sont affichées celles qui ont un lien avec des communications SSL, puis je les re-code afin de les surcharger avec LD_PRELOAD pour intercepter les informations en transit. Oui mais voilà : chez moi ltrace fait systématiquement planter firefox :( . J'ai essayé de restreindre son champs d'investigation en ne demandant que les fonctions de certaines librairies (pensant que, peut-être, c'était la latence ajoutée qui faisait planter le soft) mais rien n'y fait : impossible d'utiliser ltrace :(

De déception j'ai alors tenté de tracer uniquement les appels systèmes (avec le plus standard strace), ce qui a marché à merveille. Malheureusement je me suis retrouvé noyé dans une tonne d'appel système avec peu d'espoir d'y trouver quoi que ce soit en rapport avec SSL de toute façon.

Après cette mini digression vers les appels système je suis retourné aux librairies partagées. Si je ne pouvais pas tracer les appels effectivement réalisés, je pouvais au moins tenter de les deviner. Pour ce faire j'ai dumpé la table de liaison dynamique du plugin flashplayer grace à la super commande objdump6 :

$ objdump -T /opt/Adobe/flash-player/flash-plugin/libflashplayer.so

/opt/Adobe/flash-player/flash-plugin/libflashplayer.so:     file format elf32-i386

DYNAMIC SYMBOL TABLE:
00000000      DF *UND*  00000000  GLIBC_2.1   iconv
00000000      DF *UND*  00000000              gtk_main_iteration
00000000      DO *UND*  00000000              gtk_major_version
...
00000000      DF *UND*  00000000  NSS_3.4     CERT_DecodeCertFromPackage
00000000      DF *UND*  00000000              gdk_window_set_back_pixmap
00000000      DF *UND*  00000000              gdk_screen_get_display
00000000      DF *UND*  00000000              XFreeGC
00000000      DF *UND*  00000000              gtk_entry_get_text

L'option "-T" d'objdump affiche la liste des fonctions importées par le soft ciblé, ainsi que, parfois, la librairie dont elle est tirée. En lisant ces importations je n'ai réussi à identifier qu'un seul appel interessant pour l'interception de communications chiffrées : PR_Read7. Tant mieux pour moi, ça ne fait qu'un "suspect" à auditionner ! Pour la petite histoire sachez que cette fonction fait partie des utilities fournis par le framework Mozilla et qu'elle permet de lire dans une socket abstraite (qu'elle soit dotée d'une couche SSL ou non ne change pas l'appel à la fonction). Voici donc la surcharge que j'ai écrite :

#define _GNU_SOURCE //obligatoire pour utiliser "dlsym" qui n'est à priori pas POSIX. Plus d'info ici : http://linux.die.net/man/3/dlopen
#include <prio.h> //Pour avoir les headers mozilla
#include <stdint.h> 
PRInt32 PR_Read(PRFileDesc *fd, void *buf, PRInt32 amount) {
    // D'abord je retrouve la "vrai" fonction PR_Read
    static PRInt32 (*real_PR_Read)(PRFileDesc*, void*, PRInt32) = NULL;
    real_PR_Read = dlsym(RTLD_NEXT, "PR_Read");
    // Maintenant j'appelle la "vrai" fonction PR_Read
    PRInt32 res = real_PR_Read(fd, buf, amount);

    // Enfin je stocke les données reçus si jamais il y en a.
    if (res>0){
        FILE* f = fopen("/tmp/debug.log","a");
        fprintf(f, "PR_Read : %d\n",res);

        int i; char* mybuf = (char*) buf;
        fprintf(f, "<<<\n");
        for (i=0; i<res; i++) {
            if ((mybuf[i]>8) && (mybuf[i]<127)) {
                fprintf(f, "%c", mybuf[i]);
            } else {
                fprintf(f, "%x", mybuf[i]);
            }
        }
        fprintf(f, "\n>>>");
        fclose(f);
    }
    return res; // Et, bien sur, je retourne le résultat renvoyé par la "vrai" fonction PR_Read
}

La ligne de compilation ressemble à ça : gcc -O2 -I/usr/include/nspr -I/usr/include/nss -shared -ldl -fPIC -o surcharge.so surcharge.c

Les deux -I sont pour avoir les includes de prio.h (en fait je n'ai besoin que de l'un des deux, mais je ne sais plus lequel donc, de paresse, j'ai laissé les deux). Le -shared c'est parce que je veux compiler en librairie partagée, le -fPIC c'est pour obtenir du code qui fonctionne indépendament de sa position en mémoire (je me demande si le -shared n'inclus pas cette option d'ailleurs...) et le -ldl c'est pour être linké avec la librairie de link permettant de retrouver les fonctions originales que je surcharge via dlsym.8

Une fois compilé en *.so l'utilisation de ma fonction d'interception se fait comme ça :

$ export LD_PRELOAD="/home/ozwald/surcharge/surcharge.so"
$ firefox http://urldujeu.lan #Là on utilise normalement son programme
$ unset LD_PRELOAD # Une fois qu'on a fini on supprime la variable LD_PRELOAD pour que tout redevienne normal

Bonne surprise : quand j'utilise firefox mon fichier /tmp/debug.log contient bien des informations ! Mauvaise surprise : les informations contenues sont (très ? (trop ?)) nombreuses et à priori sans aucun rapport avec les actions du jeu (grace à un tail -f j'ai pu constater que, souvent, des actions se déroulaient au niveau du jeu mais ne déclenchaient aucune écriture dans le fichier de log). Déception...je n'ai visiblement pas intercepté la bonne fonction.

Appliquant le proverbe orc "quand ça ne marche pas en poussant, pousse plus fort" j'ai décidé de surcharger plus de fonctions. J'ai donc fait un plus simple objdump -x sur le plugin flash pour obtenir la liste des librairies linkées au lieu des fonctions. Dans le tas j'ai trouvé plusieurs librairies relatives à la communication et/ou au SSL :

Dynamic Section:
...
  NEEDED               libssl3.so
  NEEDED               libnss3.so
  NEEDED               libnssutil3.so
  NEEDED               libnspr4.so
...

Partant de cette liste j'ai identifié plusieurs fonctions appartenant à ces librairies et qui pourraient tremper dans de la communication. J'ai donc surchargé : PR_Read, PR_Recv, SSL_read, BIO_read, et BIO_gets !

Comme on pouvait s'y attendre ça a été un échec pitoyable. Aucune de ces fonctions additionnelles n'est utilisée par le jeu :(

La méthode orc ne fonctionnant visiblement pas j'ai opté pour...la méthode orc ! "Quand ça ne marche pas en poussant, pousse plus fort" (je suis parfois tétu), donc j'ai surchargé strncpy, strcpy, recv, fread, read, strtok, XDrawString, et Xutf8DrawString.

Cette méthode "très subtile" m'a octroyé une petite victoire : j'obtient des informations qui semblent correspondre aux évènements du jeu. Visiblement certaines de ces fonctions sont utilisées pour peupler la petite fenêtre de log qui résume les évènements de jeu en bas d'écran; donc j'obtient pas mal d'informations! Malheureusement je me suis vite rendu compte que celà ne suffirait pas pour écrire un bot robuste. En effet certaines informations étaient manquantes, il y avait énormément de "bruit", et la cohérence temporelle n'était pas assurée (i.e. certains évènements m'étaient remontés dans un ordre différent de ceux dans lequel ils se produisaient dans le jeu...je blame le multithread sur le coup).

Là j'ai l'impression d'être face à un mur... Devant mon incapacité à expliquer l'échec de la surcharge de PR_Read & co ainsi que l'échec silencieux de la méthode d'interception via proxy avec un certificat SSL pourtant "valide" je me suis dit que, peut-être, c'était le plugin flash qui faisait de la magie noire. Je me suis donc renseigné et j'ai trouvé Gnash et Lightspark qui sont deux logiciels complémentaires réalisant une implémentation libre d'un interpreteur flash. Rien de tel qu'un logiciel libre pour comprendre le fonctionnement de quelque chose donc j'ai installé gnash / lightspark et j'ai tenté de lancer le jeu. Malheureusement lightspark a planté :-( C'était prévisible de la part d'un logiciel jeune et dont les spécifications proviennent en grande partie de difficiles efforts d'ingénierie inverse du plugin flash officiel. J'ai forcé un petit peu dans cette voie en récupérant les versions de dévelopement9 de gnash et lightspark et j'ai re-tenté le coup avec les même résultats (i.e. plantage de lightspark).

Je me retrouve donc encore une fois coincé...une nouvelle approche s'impose ! Je vais dumper la mémoire du processus grace à /proc/PID/maps et /proc/PID/mem dans l'espoir de trouver des choses intéressantes. Dans l'esprit j'étais parti pour faire quelque chose à la memory eye en fait.

Pour ceux qui l'ignore voici un petit résumé de l'esprit des fichiers que je vais utiliser : Sous linux le pseudo-système de fichier /proc/ contient pleins d'information sur le fonctionnement en cours du système. Par exemple /proc/cpuinfo contient des informations sur votre processeur. Chez moi il contient ça :

$ cat /proc/cpuinfo
processor   : 0
vendor_id   : AuthenticAMD
cpu family  : 16
model       : 6
model name  : AMD Athlon(tm) II X2 250 Processor
...

En l'espèce nous allons nous intéresser aux fichiers /proc/PID/maps et /proc/PID/mem. Le plus simple à comprendre est /proc/PID/mem puisqu'il contient simplement la mémoire totale du système telle qu'elle est vue depuis le processus d'id PID. La mémoire adressable étant gigantesque nous allons cibler nos recherches grace à /proc/PID/maps qui contient la liste des segments de mémoire adressable qui sont effectivement alloués et accessibles par le processus en question. Pour faire un test vous pouvez lancer une commande less sur un fichier quelconque (ou juste un top, comme vous préférez), récupérer son PID grace à ps aux | grep NomDeVotreCommande puis faire un cat sur le /proc/PID/map correspondant. Voilà ce que ça donne chez moi (j'ai lancé un top dans un autre shell):

$ ps aux | grep [t]op
ozwald    9064  0.0  0.0   2700  1136 pts/0    S+   12:33   0:17 top
$ cat /proc/9064/maps
08048000-08055000 r-xp 00000000 03:02 730853     /usr/bin/top
08055000-08056000 r--p 0000c000 03:02 730853     /usr/bin/top
08057000-0807b000 rw-p 00000000 00:00 0          [heap]
...
b76f1000-b76f2000 rw-p 00185000 03:02 938394     /lib/libc-2.15.so
...
b7739000-b773a000 rw-p 00044000 03:02 417799     /lib/libncurses.so.5.9
...
bfc5c000-bfc7e000 rw-p 00000000 00:00 0          [stack]
...

Comme vous pouvez le voir nous obtenons des informations pour chaque plage allouée. En particulier nous avons :

  • l'adresse de début (0xbfc5c000 pour la stack par exemple)
  • l'adresse de fin (0xbfc7e000 pour poursuivre notre exemple de la stack)
  • les permissions ("rw-p" pour la stack)

Nous pouvons donc à présent cibler toutes les plages qui sont à la fois "R"eadable et "W"ritable par le processus, et y chercher des élèments parlant. Pour lire le contenu de ces zones mémoire il suffit d'ouvrir /proc/PID/mem en lecture seule, puis de faire un seek jusqu'à l'offset de début de plage, et à y lire autant d'octets que la taille de la plage. Le petit script python10 ci-dessous permet de dumper les plages mémoire du processus dont on passe le PID en paramètre :

#! /usr/bin/env python
import re
import sys
if len(sys.argv) != 2 :
    print "Merci de donner un PID en argument"
    print "Usage : %s PID"%sys.argv[0]
    sys.exit(1)

mypid=sys.argv[1]
mypid=str(mypid)
sys.stderr.write("PID = " + str(mypid) )
maps_file = open("/proc/"+mypid+"/maps", 'r')
mem_file = open("/proc/"+mypid+"/mem", 'r', 0)
for line in maps_file.readlines():  # for each mapped region
    m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r][-w])', line)
    if m.group(3) == 'rw':  # if this is a writeable region
        sys.stderr.write("\nOK : \t" + line+"\n")
        start = int(m.group(1), 16)
        if start > 281474976710655 :
            continue
        end = int(m.group(2), 16)
        sys.stderr.write( "start = " + str(start) + "\n")
        mem_file.seek(start)  # seek to region start
        chunk = mem_file.read(end - start)  # read region contents
        open("%d.dump"%start,'w').write(chunk) # dump contents to a file
    else :
        sys.stderr.write("\nPASS : \t" + line+"\n")
maps_file.close()
mem_file.close()

Bon, avec un tel outil les perspectives d'analyse sont démultipliées ! Je lance donc le jeu, récupère le PID de l'interpreteur flash officiel via un ps aux | grep [f]lash, et dump le contenu de sa mémoire en invoquant le script ci-dessus. Je me retrouve avec quelques dizaines de fichiers de dump, chacun correspondant à une plage mémoire. Après quelques grep bien sentis11 j'identifie des structures JSON qui semblent contenir des éléments de jeu et qui ressemblent à ça :

GameEvent:ClientArrival { "ClientID":"123", "ClientName":"Roger", ..}
GameStatus:OrdersWaiting { "Orders":[ {"ClientID":"123", "Product":"Pizza", ...}, {}, ..., {}] }

L'espoir renait parce que ces structures sont très intéressantes : d'une part elles contiennent toutes les informations dont j'ai besoin pour écrire un bot, et d'autre part le fait qu'elles soient en JSON me laisse penser qu'il s'agit bien là de l'information échangée en réseau dans le tunnel SSL et sur laquelle j'essaie de mettre la main depuis le début. J'ai donc creusé dans cette voie pour, finalement, obtenir un script python qui fonctionne en deux temps :

  1. Il identifie la zone mémoire où sont les structures JSON
  2. Une fois la zone identifiée avec certitude il dump cette zone en boucle en guettant des changements (ce qui signifierait l'arrivée d'un nouveau paquet d'information en provenance du serveur et donc l'arrivée d'un nouvel élément de jeu tel qu'un client).

La qualité d'information que j'ai obtenu avec ce script est exemplaire puisque, contrairement à la lecture de l'écran par OCR, je n'ai aucune erreur sur le contenu des texte. Malheureusement, lorsque plusieurs évènements se suivent très rapidement dans le temps (comme deux clients qui rentrent quasiment en même temps dans la pizzeria) mon script ne perçoit que l'un des deux évènements et rate l'autre qui se fait écraser en mémoire entre deux dumps. Il fallait s'y attendre puisque je fait du "poll" sur la mémoire au lieu d'avoir obtenu un système en "push" comme me le permettrait la surcharge d'une fonction par LD_PRELOAD :(

N'ayant pas envie d'abandonner j'ai poursuivi dans une "voie du milieu12" tirant partie de plusieurs des travaux effectués jusqu'à présent. Lorsque je dumpais les structures JSON directement depuis la mémoire j'ai remarqué que l'adresse où étaient les structures intéressantes ne bougeait quasiment jamais lors d'une même partie. Ces structures JSON n'arrivant pas là par magie (à moins que le plugin flash officiel pratique réellement la magie noire...) je me suis dit qu'identifier la fonction responsable de l'écriture de cette structure serait très intéressant puisque je pourrait peut-être la surcharger avec LD_PRELOAD.

La démarche que j'ai adopté a donc été la suivante :

  1. Lancer une partie
  2. Identifier le PID de l'interpreteur flash par ps aux | grep [f]lash
  3. Dumper la mémoire du processus et identifier l'adresse d'une structure JSON d'intérêt
  4. Attacher gdb au processus de l'interpreteur flash
  5. Poser un breakpoint sur les écritures à l'adresse de la structure JSON
  6. Afficher, automatiquement lors du déclenchement de ce breakpoint, le contenu de la mémoire (pour vérifier si j'ai bien une structure JSON comme je le pensait) ainsi que la backtrace des 4 derniers appels.

L'objectif de ce protocole étant d'identifier quelle partie de code est responsable de l'écriture de ces structures. En termes de commande voici ce que ça a donné :

$ ps aux | grep [f]lash
ozwald    9064  0.0  0.0   2700  1136 pts/0    S+   12:33   0:17 flash-plugin
$ python memory_dump_and_seek.py 9064
Dumping...
Seeking json...
The json object is at address 0xaa72f000
$ gdb
(gdb) attach 9064
(gdb) watch *0xaa72f000
Hardware watchpoint 1: *0xaa72f000
(gdb) commands 1
>silent
>x/1s 0xaa72f000
>bt 4
>cont
>end
(gdb) cont
Continuing.
0xaa72f000: "GameEvent:ClientArrival { "ClientID":"123", "ClientName":"Roger", ..}...
#0  0xb584c026 in ?? () from /lib/libc.so.6
#1  0x00000277 in ?? ()
#2  0xb2b2c1d9 in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
#3  0xb2b33f9b in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
0xaa72f000: "GameEvent:ClientArrival { "ClientID":"124", "ClientName":"Paul", ..}...
#0  0xb584c026 in ?? () from /lib/libc.so.6
#1  0x00000277 in ?? ()
#2  0xb2b2c1d9 in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
#3  0xb2b33f9b in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
0xaa72f000: "GameEvent:ClientArrival { "ClientID":"125", "ClientName":"Jean", ..}...
#0  0xb584c026 in ?? () from /lib/libc.so.6
#1  0x00000277 in ?? ()
#2  0xb2b2c1d9 in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
#3  0xb2b33f9b in ?? () from /opt/Adobe/flash-player/flash-plugin/libflashplayer.so
...

Voilà qui sent bon. A chaque interruption :

  • la zone mémoire contient bien ce qui ressemble à une structure JSON valide
  • la backtrace est systématiquement la même
  • cerise sur le gateau : la dernière instruction appelée (i.e. celle qui est responsable de l'écriture mémoire) appartient à /lib/libc.so.6 et sera donc surchargeable via LD_PRELOAD (alors que si ça avait été une fonction interne à l'interpreteur flash j'aurai été plus ennuyé).

Par contre ce qui me surprend à ce moment là c'est que j'avais déjà fait des tentatives infructueuses en surchargeant une bonne pelleté de fonctions de la libc (strcpy et strncpy en particulier, souvenez-vous du début de cet article...il y a 3 pages :D ). Quelle fonction de la libc peut donc être responsable de ces écritures ? A cette étape je suis certain qu'un gourou de gdb pourrait répondre en une commande. Malheureusement je ne suis pas un gourou de gdb :( J'ai bien tenté de demander gentimment, mais sans succès :

(gdb) info symbol 0xb584c026
No symbol matches 0xb584c026.

Bon..."Quand ça ne marche pas en poussant, pousse plus fort" donc je vais adopter, encore une fois, une technique très subtile :

  1. Tout d'abord on active l'enregistrement des sorties de gdb dans un fichier texte, en prévision d'un gros tas de donnée à traiter : set logging on
  2. On demande à gdb d'afficher TOUTES les fonctions que le processus connait13 : show functions.

Une fois ceci fait on quitte gdb et on se retrouve avec un fichier gdb.txt qui contient les logs de la session retranscrivant ce que nous avons eu sur la sortie standard et ressemblant à ça :

All defined functions:

Non-debugging symbols:
0x08049290  _init
0x080492b8  strerror_r@plt
0x080492c8  abort@plt
0x080492d8  sysconf@plt
...
=== NDLR : environ 40 000 lignes plus tard ===
...
0xb470e8f0  _nss_dns_getnetbyaddr_r
0xb470ec40  _nss_dns_getcanonname_r
0xffffe400  __kernel_sigreturn
0xffffe40c  __kernel_rt_sigreturn
0xffffe414  __kernel_vsyscall

Avec ces informations, retrouver la fonction que je cherche est simple comme bonjour puisqu'un grep et un sort suffisent. Pour rappel je cherche la fonction de la libc qui contient l'adresse 0xb584c026 puisque c'est l'instruction à cette adresse qui est responsable de l'écriture de la structure JSON que je recherche à l'adresse 0xaa72f000 :

$ grep -E 0xb584[bc] gdb.txt | sort -u
...
0xb584bea0  __strncasecmp_l
0xb584bea0  strncasecmp_l
0xb584bf20  memccpy
0xb584bf80  memcpy
0xb584c680  __strsep_g
0xb584c680  strsep
...

La coïncidence est trop belle : c'est memcpy le "coupable" (puisque 0xb584bf80 < 0xb584c026 < 0xb584c680) ! Il ne me reste donc plus qu'à surcharger memcpy pour vérifier la théorie. Attention cependant : surcharger strcpy et strncpy ne constituait pas vraiment un risque (ces fonctions sont censées traiter des chaines de caractères), mais surcharger memcpy est bien plus audacieux. En effet, memcpy est d'un usage beaucoup plus versatile et tout aussi courant (si ce n'est beaucoup plus). Quelques précautions s'imposent donc lors de l'écriture de la surcharge afin de s'assurer que l'on ne va intercepter que les mouvements de mémoire qui nous intéresse. D'une part ça nous facilitera le traitement des données interceptées et, d'autre part, ça va permettre de limiter le ralentissement des processus que nous surveillons même s'ils font très souvent appel à memcpy. J'ai donc adopté les précautions suivantes :

  • Je n'ai utilisé aucune tournure qui pourrait faire appel à une primitive memcpy lors de l'optimisation du compilateur (je ne sais pas s'il y a un risque d'appel récursif, mais je préfère ne pas tenter).
  • J'ai utilisé un fichier de dump différent par processus et par thread afin d'éviter les problèmes d'accès concurrents (j'aurai pu jouer avec des mutex, mais ça aurait potentiellement ralenti le schmilblick et puis c'est plus long à coder)
  • J'ai fait attention à ne pas trop introduire de bug dans mon code. Ca a l'air évident mais ça ne coute rien de le rappeler. Par exemple : avant d'accéder aux éléments de csrc pour vérifier si la zone mémoire copiée commence bien par la chaine de caractère que je veux ("Game") je m'assure que csrc contient au moins autant de caractères que ce que je vais lire...

Bref, voilà le code :

#include <sys/syscall.h> //for gettid
#include <sys/types.h> // this and below for getpid
#include <unistd.h>
void *memcpy(void *dest, const void *src, size_t n){
    static void* (*real_memcpy)(void* , const void*, size_t) = NULL;
    real_memcpy = dlsym(RTLD_NEXT, "memcpy");

    const char* csrc = (const char*) src;

    if ((n>4) && (csrc[0]==71/*G*/) && (csrc[1]==97/*a*/) && (csrc[2]==109/*m*/) && (csrc[3]==101/*e*/)){
        int write_this_much = 0;
        // I only want printable ascii :
        while ((write_this_much<n) && (csrc[write_this_much]>31) && (csrc[write_this_much]<127) ) { write_this_much++; }

        // Un fichier par processus et par thread :
        char filename[128];
        snprintf(filename,128,"/tmp/debug_%d_%d.log\0", (int)getpid(), (int)syscall(SYS_gettid) );

        FILE* f = fopen(filename,"a");
        fprintf(f, "memcpy : <<< ");
        fwrite(csrc, 1, write_this_much, f);
        fprintf(f, ">>>\n");
        fclose(f);
    }
    return real_memcpy(dest, src, n);
}

Malgré toutes ces précautions lorsque j'utilise ma surcharge en déclarant la librairie dans LD_PRELOAD firefox crashe lamentablement au démarrage :-( ...Et c'est là que j'ai un gros coup de chance : chromium-browser, lui, fonctionne comme si de rien n'était14 :) ! Je ne sais même pas pourquoi j'ai essayé sur chromium cette fois-là alors que je n'avais fait aucun test dessus auparavant, mais je m'en félicite ;)

Bon, avec un navigateur qui marche je vais sur le jeu flash, et je me fait une petite partie. Une fois la partie terminée j'ouvre /tmp/debug_8613_8613.log et je constate avec joie qu'il contient l'ensemble des structures JSON que je voulais ! Aucune corruption, à priori aucun manque, et l'ordre semble correct.

En conclusion donc : j'en aurai bien ch*é pour arriver à ce résultat et je ne comprends pas encore tout (pourquoi firefox plante-t-il ? Est-ce-que le plugin flash embarque sa propre librairie SSL ?? Pourquoi ltrace fait-il planter les softs qu'il surveille ? etc.), mais j'ai également appris beaucoup (dumper proprement la mémoire d'un process, définir les commandes à lancer automatiquement quand gdb déclenche un breakpoint, définir un breakpoint sur une écriture en mémoire, ...) et surtout : J'ai réussi à faire ce que je voulais15 :-D !!!

  1. Eux, avaient une motivation pécuniaire, contrairement à Slow Frog.
  2. En guise de rafinnement je prenais le screenshot dans un répertoire appartenant à un volume monté en tmpfs histoire d'être certain de ne pas fatiguer le disque par les écritures de screenshot répétées
  3. J'ai également testé tesseract, un autre logiciel de reconnaissance de caractère (OCR), mais j'ai trouvé qu'il donnait globalement des résultats moins bons.
  4. Merci OpenSSL et BURP :)
  5. Avec cette même fonctionnalité de LD_PRELOAD certains ont réalisé des keylogger, des rootkits, etc.
  6. Vous pouvez également utiliser nm -D à la place de objdump -T
  7. Je ne connaissais pas cette fonction avant de tomber dessus lors de ces recherche, je l'ai donc identifiée par tatonnement.
  8. Pfiou ça en fait des options de gcc...J'espère que je n'ai pas dit trop de bétises d'ailleurs :-D
  9. J'aime bien l'expression "bleeding edge" plutôt que "version de développement", mais bon...limitons les anglicismes quand ils ne sont pas nécessaires ^^
  10. Script largement pompé depuis le net; googlez pour retrouver la source originale, je ne l'ai malheureusement pas noté.
  11. Par exemple j'ai lancé des grep sur le nom des clients de la pizzeria que je pouvais voir à l'écran lors du dump de la mémoire.
  12. Comme le diraient certains sages.
  13. Attention, dumper l'ensemble des fonctions retourne beaucoup de résultats et ça peut donc prendre du temps. Dans mon cas pratique celà signifiait que le freeze du jeu était assez long pour me faire perdre la partie. Etant donné que l'adresse d'enregistrement des JSON changeait à chaque démarrage de partie, dumper les fonctions devait être la dernière étape de mon processus d'analyse :)
  14. En fait chromium-browser rame assez notablement, mais il fonctionne sans montrer la moindre envie de planter.
  15. Bon, il me reste à remplacer les fichiers par des tubes nommés (mkfifo) puis à écrire la partie logicielle qui va lire le flux d'information en temps réel dans ces tubes, puis à écrire la partie "IA" qui va décider comment jouer, puis à écrire la partie qui va envoyer les actions au jeu...Mais le plus difficile est fait, si si je vous jure ;) D'ailleurs, le plus difficile étant fait, je ne sais pas si ça m'intéresse encore de terminer ce projet lol

Pole Emploi : de l'argent public mal employé

Par Oz le - Perso

Environ un an après la première démonstration d'incompétence que m'avait faite pole emploi ils remettent ça dans la joie et la bonne humeur :-D J'ai en effet appris le 18 avril que : "Votre inscription étant arrivée à échéance, vous cessez d'être inscrit sur la liste des demandeurs d'emploi à compter du 18 avril 2013, conformément aux dispositions du code du travail."

Voilà voilà voilà... Etant donné que je bénéficie du plafond de durée d'aide au retour à l'emploi (ayant travaillé plus de 36 mois consécutifs à temps plein avant de me retrouver sans emploi) je suis très heureux de savoir que je me retrouve désinscrit au bout de seulement 12 mois et sans aucune explication...

Le constat est donc clair : je peux témoigner qu'au moins depuis un an Pole Emploi est un ramassis d'incompétents et constitue une dépense d'argent public à mauvais escient1. En tant que citoyen je demande donc qu'ils remboursent l'argent ainsi volé pour faire tourner un service public fictif; mais je sais bien qu'il n'y a aucune chance pour que celà se produise un jour, snif :-(

Bref...en attendant j'ai demandé ma ré-inscription (qui d'ailleurs est une vrai plaie à faire ! J'ai rarement vu un service web aussi pourri :-/); on verra bien ce qu'il advient.

PS : pour me faire pardonner les derniers billets "non-techniques" (dont celui-ci fait évidemment parti) je prépare un micro article sur LD_PRELOAD et objdump afin d'intercepter des appels choisis et réaliser ainsi de l'analyse de binaire sous linux. C'est passionant comme méthode de jeux, même si je ne maitrise pas encore tout ce qui se passe2...

  1. J'aurai aimé pouvoir dire "dépense d'argent public inutile", mais étant donné qu'ils sont nuisibles le termes "inutile" est malheureusement trop gentil :-(
  2. en particulier j'essaie en ce moment d'intercepter, en clair, les communications réalisées par une animation flash dans un tunnel SSL. Malheureusement pour moi SSL_read, SSL_recv, BIO_read, PR_Read, et PR_Recv ne suffisent pas; recv ne me donne que le flux chiffré; et strncpy remonte trop de bruit par rapport au signal qui, en plus, semble incomplet.

Aller plus loin avec gEDA

Par Oz le - Électronique
Hack arduino atmega gEDA robot électronique

J'avais déjà rapidement parlé de gEDA dans un précédent billet1, mais pour ceux qui ne l'ont pas lu voici un rappel : gEDA est une suite de logiciels Open Source permettant de concevoir et simuler des circuits électroniques. Dans ce billet je vais tenter de résumer comment on peut utiliser gEDA pour amener la conception d'un circuit électronique à partir de rien jusqu'à l'obtention d'un véritable circuit imprimé physique. Le but de cet article est, d'une part, de me servir de pense-bête pour plus tard et, d'autre part, d'apporter quelques astuces à ceux qui, comme moi, vont découvrir de nombreuses embuches en tentant de concevoir leur premier circuit avec gEDA. Contrairement au billet précédent il n'y aura donc pas d'informatique du tout dans ce billet-ci et il risque d'être assez indigeste pour ceux qui n'ont pas envie de concevoir de circuits imprimés...Promis je me rattraperai plus tard avec d'autres billets ;) !

Arduino motor shield by Ozwald - Creative Common CCBYSA by ozwald.fr

gschem pour créer le schéma fonctionnel

Schéma gschem - Creative Common CCBY by ozwald.fr

Comme rappelé en introduction, gEDA est une suite de logiciels cohérents les uns avec les autres, chacun ayant son utilité propre. L'outil de la suite gEDA par lequel nous allons commencer est gschem qui permet de dessiner le schéma logique du circuit que l'on est en train de concevoir2. Il n'est pas follement intuitif d'utilisation mais après 30mn à s'habituer au maniement de l'outil on parvient à faire à peu près ce que l'on veut.

Pour dessiner votre schéma il suffit de sélectionner les composants que vous voulez dans la bibliothèque ( menu add > components ), de les placer sur l'espace de dessin, d'éditer leurs propriétés, puis de les relier entre eux avec des "nets node". Rien de bien sorcier en somme, mais voici quelques points qu'il faut penser à vérifier si vous ne voulez pas être ennuyé plus tard :

  • Penser à donner un nom unique à chaque composant (via sa propriété refdes)
  • Pour les composants "redondants" (c'est à dire les composants qui répètent plusieurs fois la même fonction logique, par exemple les 7404 qui contiennent 6 fonctions logiques "NON") vous ne ferez apparaitre qu'une fonction logique sur le schéma (forcément, c'est un schéma logico-fonctionnel) et pas une empreinte réaliste du composant. Pour retrouver vos petits lorsque vous concevrez le plan physique de votre circuit il faut penser à cette étape à mettre le même nom (propriété refdes) à toutes les fonctions logiques que vous voulez prendre sur le même composant, et à les différencier uniquement en fixant une valeur incrémentale3 (unique) à leur propriété slot.
  • Penser à donner une valeur à l'attribut footprint de chaque composant. Cette valeur servira plus-tard à définir l'empreinte physique du composant sur le circuit imprimé. Quelques footprints utilisables peuvent être trouvés dans /usr/share/pcb/pcblib-newlib/geda sur Gentoo, mais personnellement j'ai fini par agréger ma propre librairie de composants afin d'obtenir des descriptions cohérentes utilisables sur l'ensemble des logiciels de la suite gEDA, les composants fournis d'office n'étant généralement plus utilisables dès qu'on veux passer d'un logiciel de la suite à un autre4.
  • Faite apparaitre vos connecteurs sur le schéma logique :) ! On n'y pense pas forcément lorsqu'on a le nez dans le schéma fonctionnel mais il est essentiel de se rappeler que votre "signal IN" devra bien venir de quelque part sur votre circuit imprimé physique. Vous devez donc absolument ajouter à votre schéma gschem les composants de connectique.

pcb pour créer le plan physique

Schéma pcb - Creative Common CCBY ozwald.fr

Une fois le schéma fonctionnel satisfaisant (.sch) vous pouvez lancer pcb, également inclus dans la suite gEDA. Si vous avez bien fait votre fichier ".sch" un petit clic dans le menu File > Import Schematics fera apparaitre l'ensemble des composants physique de votre schéma logique (dans un gros paté au centre de la fenêtre de pcb). Si ça ne marche pas pensez à bien vérifier les points exposés dans la partie gschem ci-dessus, et si ça ne marche toujours pas changez les noms de vos composants pour des noms plus courts et homogènes en casse. Ca a l'air tout con mais j'ai constaté que certains composants qui n'apparaissaient pas dans mon import sur PCB, ont subitement décidés d'apparaitre quand j'ai changé leur nom de quelque chose du type LED_batterie vers quelque chose du type LEDB...allez comprendre !

Une fois l'ensemble de vos composants et connexions correctement chargées dans PCB vous allez pouvoir les disposer sur l'espace qu'occupera votre futur circuit ainsi que faire le routage5 à proprement parler. Le routage est une étape qui peut prendre BEAUCOUP de temps, mais elle est malheureusement nécessaire pour obtenir une réalisation physique qui fonctionne. Ci-dessous je vous mets quelques astuces en vrac pour le routage :

  • Penser à dé-selectionner, dans la colonne de gauche de l'interface de pcb, les "layers" que vous ne voulez pas utiliser avant de faire appel à la fonction "Auto-route" de pcb. En effet, si vous prévoyez de réaliser votre circuit imprimé sur une seule face (par exemple si vous voulez le graver vous-même par la suite) il ne faut pas que pcb utilise deux couches pour router (or en mode "Auto-route" il utilise autant de couche que possible). Pour ma part je ne garde que 2 "layers" actifs (en plus des layer "silk" et "rat lines" qui ne correspondent qu'à des indices visuels et ne sont donc pas utilisés par la fonction "Auto root") parce que j'utilise plutôt du double couche que du simple. En effet le double couche reste encore raisonnable (on peut graver soit-même des circuits en double couche) et il permet de router des cartes avec beaucoup de connexions sans avoir à ajouter des tas de ponts disgracieux jouant à saute mouton au dessus des pistes comme celà arrive rapidement quand on se restreint au simple couche.
  • Penser à faire un gros plan de masse, voire un gros plan au potentiel VCC (pour ce faire vous pouvez, par exemple, utiliser les outils polygone ou rectangle puis les relier à des net via un pin grace à l'outil "therm(al)").
  • Jouez avec la largeur et les marges de vos pistes. Plus ces paramètres sont petits plus le routage est facile, mais plus la réalisation physique sera compliquée et moins vous pourrez faire passer de courant.

Création physique !

Une fois votre PCB correctement créé sur le logiciel éponyme il est temps de passer à l'étape physique ! Pour ce faire il y a deux chemins possibles : soit vous fabriquez votre circuit vous-même, soit vous le faite fabriquer par une entreprise spécialisée.

Si vous décidez de fabriquer votre PCB pensez à faire un routage avec des pistes larges et bien espacées. En effet, la réalisation manuelle de PCB est une opération délicate donc moins il y aura de petits détails sur votre plan de PCB moins vous risquerez de faire d'erreur et ce n'est pas du luxe ! Plusieurs méthodes existent pour la réalisation manuelle mais sachez qu'elles impliquent toutes l'utilisation de produits chimiques et peuvent vous prendre plusieurs heures. Ne paniquez pas pour autant, de nombreux amateurs réalisent régulièrement des cartes6 donc je vous assure que c'est réalisable. N'ayant moi-même pas choisi cette voie je ne vais cependant pas rentrer dans les détails, mais cherchez sur le net et vous trouverez pleins de gens prêts à vous expliquer comment ils font chacune des deux étapes principales du processus :

  • Mise en place d'une couche protectrice sur une plaque cuivrée vierge reprennant la forme de votre circuit à imprimer : Soit vous utilisez des PCB recouvertes d'une résine photosensible et vous détruisez sélectivement cette résine en exposant la plaque à la lumière derrière un transparent sur lequel vous avez imprimé votre circuit; soit vous imprimez une image mirroire de votre circuit avec une imprimante laser sur du papier de mauvaise qualité (en espérant ne pas tuer votre imprimante) puis vous tentez de faire un "décalcomanie" de votre impression sur une plaque de cuivre nue avec un bête fer à repasser.

  • Trempage de la plaque, avec son masque protecteur, dans une solution qui va dissoudre le cuivre non-protégé et donc "graver" les contours de votre circuit : soit vous utilisez le produit chimique "standard" que vous devrez acheter pour une dizaine d'euros en magasin spécialisé (ce produit chimique est réutilisable), soit vous tentez de concocter vous-même un autre produit chimique à base d'eau oxygénée à vos risques et périls.

  • Suppression du masque protecteur une fois la gravure terminée pour découvrir vos pistes de cuivres : selon les méthodes suivies aux étapes précédentes ça peut être aussi simple que de laisser la plaque au soleil, ou un poil plus compliqué type "tamponnage de disolvant à l'aide d'un coton tige".

Power Shield - Creative Common CCBYSA by ozwald.fr

Si, en revanche, vous décidez de faire fabriquer votre PCB par une entreprise spécialisée soyez averti : ça coute cher ! La conception de circuits imprimés amateur n'est pas encore follement répandue et les tarifs restent avant tout orientés vers des tirages en série, le mode de facturation/production n'est donc généralement pas adapté aux besoins d'amateurs souhaitant n'avoir qu'un ou deux prototypes. Vous trouverez ainsi souvent un montant forfaitaire d'une trentaine d'euros pour "préparation des outils", puis comptez une autre trentaine d'euros pour une impression sur des PCB carré double face de moins de 10cm de cotés, et ajoutez enfin à celà une dizaine d'euros de frais de livraison. Pour ma part j'ai eu énormément de chance et j'ai pu faire appel à un petit fournisseur asiatique qui m'a réalisé 10 exemplaires de mon circuit (5,50cm par 5cm en double face) pour seulement 25€, donc je n'ai pas hésité :) ! Bref, ce qu'il faut retenir si vous voulez faire fabriquer vos circuits :

  • La tarification est souvent élevée et parfois obscure, mais il existe tout de même quelques fournisseurs qui mérittent le coup d'oeil.
  • Les entreprises acceptent généralement le format "gerber" pour vos fichiers, c'est un format vers lequel pcb sait exporter donc tout va bien7; mais parfois ils n'acceptent que le format du logiciel Eagle qui est un logiciel non-libre...à moins de tout recommencer sous ce logiciel vous ne pourrez donc pas faire appel à ces entreprises.

Une fois votre PCB nu en main8 vous n'avez plus qu'à vérifier qu'il n'y a pas de court-circuit apparent sur les contacts accessibles, puis à souder les composants en place (cf la photo du paragraphe précédent) et à faire vos tests unitaires en croisant les doigts !

Conclusion sur le combo gschem/pcb

Le message essentiel à retenir sur la conception c'est qu'il est vraiment important d'avoir des librairies de composants cohérents à utiliser sous gschem et pcb. Il n'y a rien de plus rageant que de faire son circuit logique sous gschem pour se rendre compte ensuite que le "footprint" que l'on a mis à toutes ses résistances est inconnu de pcb (et donc inutilisable) ou encore que le "footprint" de vos diodes d'électronicien correspond en fait à un composant de puissance d'un mètre de diamètre pour pcb -_- ... Pour ma part j'ai honteusement pioché/tranché dans les composants fourni avec gEDA sous ma gentoo ainsi que dans les nombreux éléments téléchargeables sur internet grâce aux explications de ce site de référence. Ayant tranché comme un goret dans des parties de code (et de commentaires, dont certains contenaient le descriptif de la version de license utilisée) pour rassembler ma petite librairie de composants je ne vais pas la rendre téléchargeable avec ce billet; mais si vous la voulez envoyez moi un mail et je pourrai vous passer une copie "en privée".

Enfin, quelques dernières remarques en vrac qui pourront vous aider si vous rencontrez des problèmes de composants incompatibles entre gschem et pcb9 :

  • Favorisez les composants dont les PIN sont numérotées (plutôt que "lettrées") dans gschem. Par exemple préférez le modèle "capacitor" dont les pins sont labélisées "1" et "2" plutôt que "P" et "N". En effet les symboles dans pcb utilisent plutôt des numérotations que des lettres donc si vous voulez que pcb parvienne à retrouver les connexions rentrées dans gschem il faut que les références des pins des symboles pcb soient les même que celles des symboles gschem.

  • Quelques associations "composant <==> footprint" qui marchaient pas trop mal dans pcb et qui étaient fournis dans ma Gentoo :

capacitor-2.sym <==> RCY100P 
lm7805-1.sym <==> TO126W
connector2-1.sym <==> JUMPER2
resistor-1.sym <==> ACY800
led-3.sym <==> RCY100
  • Les connecteurs sont parmi les composants les plus pénibles à traiter. En effet leurs formes sont souvent assez spécifique et il est essentiel qu'ils aient la bonne représentation dans pcb sous peine de vous retrouver avec un circuit imprimé inutilisable parce que vous n'avez pas la place de souder vos connecteurs. Pour ma part j'ai été obligé de créer mes modèles de connecteurs de toute pièce afin d'en avoir qui marchent10. Parmi les problèmes que j'ai cru identifier : les "connectors" fourni avec mon gschem ont des pin de type pas(siv) qui, visiblement, ne plaisent pas à pcb; du coup j'ai refait les connecteurs avec des pins de type io. Ah et mes connecteurs étaient également particulièrement sensibles aux blagues de nommage dont je vous parlais plus haut : quasiment tout ceux dont le nom dépassait 5 caractères n'étaient pas importés dans pcb...

En guise de conclusion : concevoir ses circuits imprimés ex nihilo jusqu'à obtenir un circuit physique qui fonctionne c'est une grande satisfaction (surtout pour moi qui ne pigeait rien à l'électronique il y a 4 ans); et même si le "motor shield" compatible arduino que j'ai réalisé et qui illustre intégralement cet article est bourré de maladresse de conception11 j'en suis très content et j'aurai peut-être l'occasion d'en faire bon usage :D !


Dromi le 2013/09/10 13:24

Après avoir utilisé Kicad au boulot, je te confirme qu'il est vraiment plus efficace que gEDA! (Bon ok il y a toujours une part de subjectivité...)
J'en ai profité pour lire la réponse à mon commentaire d'il y a 2 ans (...) à propos de la simulation :
Un commentaire revient régulièrement chez Kicad : l'important n'est pas d'implémenter une fonction, mais de pouvoir exporter au bon format (si possible standard mais bon ^^ ) pour pouvoir travailler avec d'autres outils qui font bien le travail et qu'il est inutile de chercher à concurrencer...
Ensuite je te l'accorde, ajouter un bouton qui lance automatiquement la simu sous ngspice, ça pourrait peut-être ne pas coûter si cher que ça...

  1. Billet qui a déjà presque 2 ans...le temps passe vite :)
  2. Dessiner/Relire des schémas logique à peine plus haut niveau que ceux dont nous parlons ici semble justement amuser follement beaucoup de monde en France ces temps-ci ;-)
  3. En commençant à compter à 1, donc 1,2,3,4,5 et 6 pour un 7404...Bah ouais c'est de l'électronique, pas de l'informatique donc on commence à "1".
  4. Douce ironie: les logiciels qui comptent plusieurs dizaines de miliers de ligne de code sont compatibles entre eux, mais les composants comptant à peine 20 lignes de texte, eux, ne sont pas prévu pour fournir des informatios à tout les logiciels...
  5. C'est à dire tracer les connexions physiques entre les différentes pates de vos composants.
  6. J'en connais plusieurs, et pourtant ils ne sont pas masochistes.
  7. Vous pouvez ensuite utiliser le logiciel gerbv pour visualiser les fichiers gerber générés par pcb avant de les envoyer à votre fabricant
  8. PCB nu tel que celui qui illustre ce billet, tout juste sorti de sa boite de livraison.
  9. Remarques qui vont surtout me servir de pense-bête personnel pour le jour où je voudrait rajouter des composants à ma bibliothèque et que j'aurai tout oublié :D
  10. Et encore...Je m'étais trompé dans l'espacement entre les deux pates de mon connecteurs, si bien que j'ai du les tordres à la pince pour les espacer afin qu'ils puissent s'enfiler correctement dans les trous prévus à cet effet dans mes circuits imprimés reçus d'Asie. Trous qui, celà dit en passant, étaient largement surdimensionnés pour mes connecteurs, j'aurai du modifier mes empreintes pcb pour utiliser des trous de la même taille que ceux destinés à accueillir les pates de mes autres composants.
  11. Pas de plan de masse, pas de condensateurs filtrant les parasites, pas de résistance de pull-up, etc... Mais les fichiers sont quand même disponibles si vous me les demandez par mail ;-)

Projet de Weekend : Tourelle de surveillance

Par Oz le - Robotique
HC-SR04 UART arduino atmega python robot servo électronique

Après plusieurs billets assez pauvre en contenu je me suis dit que résumer un micro-projet mettant en oeuvre plusieurs techniques différentes serait une bonne idée. Voici donc le résumé de la création d'un petit jouet qui m'a couté environ 7€ 1 ! Je vous laisse découvrir un à un les différents domaines que j'ai associés et essayer de deviner au fil de l'eau ce que j'obtient à la fin et qui constitue le but de ce projet (ainsi que le titre du billet...j'aurai d'ailleurs du y mettre des balises SPOIL ^^ ). Bonne lecture !

Tour de surveillance - Creative Common CC-BY-SA by Ozwald (me)

Un peu de vision

J'ai déjà expliqué sur ce blog comment on peut facilement programmer un micro-controlleur2, et nous avons également déjà vu comment utiliser ces micro-controlleurs pour faire tourner des moteurs, ce qui permet (par exemple) de faire bouger un robot. Nous savons donc à présent comment fabriquer un robot qui agit sur le monde (en s'y déplaçant) mais nous n'avons pas encore vu de méthode pour que le robot prenne connaissance du monde (or d'aucun diront qu'il est intéressant de comprendre son environnement avant d'agir). La première connaissance nouvelle que nous allons aborder dans ce billet c'est justement comment mesurer la distance qui sépare notre robot d'un obstacle situé devant lui.

Beaucoup de méthodes existent pour mesurer des distances sans contact. On peut par exemple citer les mesures de déphasages entre un rayon laser émis et son reflet renvoyé par l'obstacle, les mesures de focalisation d'un signal lumineux (généralement infra-rouge) émis puis réfléchi par l'obstacle, ou encore la mesure de temps entre l'émission d'un signal acoustique (généralement ultra-son) et la réception de son écho produit par l'obstacle.

Dans ce mini-projet j'ai opté pour la mesure par ultra-son, pour deux raisons :

  • c'est la méthode la moins chère (on trouve des modules tout fait à moins de 1.50€ sur eBay, à moins de 6$ sur amazon, à moins de 10€ sur robotshop, ou à 15€ chez gotronic pour la classe au dessus).
  • Ces modèles peu onéreux permettent pourtant déjà de mesurer des distances allant, classiquement, de quelques centimètres à environ 4m, ce qui est parfait pour un usage en intérieur.

Plus précisément j'ai opté pour un HC-SR04, qui semble être l'un des modèles les plus répandus (mais pas l'un des plus fiables).

HC-SR04 - Creative Common by "scanlime" on Flickr

Le fonctionnement de ce module est simplissime : on branche la PIN "GND" du module à la masse de notre circuit, la PIN "VCC" à une alimentation 5V, et les PIN "trig(ger)" et "echo" chacune à une I/O de notre microcontrolleur. Pour mesurer la distance qui sépare ce module d'un obstacle situé devant lui on envoie simplement une impulsion positive d'environ 10us sur la PIN "trigger" du module, puis on mesure la longueur de l'impulsion positive que nous envoie le module en réponse sur sa PIN "echo". La longueur de l'impulsion de réponse (sur la PIN "echo") est proportionnelle au temps mis par le son pour faire l'aller-retour entre le module et le premier obstacle rencontré (la vitesse du son étant à peu près constante on en déduit alors la distance qui sépare le module du premier obstacle situé devant lui). Pour être précis on considère que 1cm se traduit en environ 58uS d'impulsion "echo"; si je reçois une impulsion retour qui dure 580uS j'en déduis donc qu'il y a un obstacle à environ 10cm devant mon module. Trivial3 !

Un peu d'action (controllée)

La denière fois que nous avons parlé d'agir sur le monde depuis un microcontrolleur nous avions fait tourner des moteurs, dans un sens ou dans l'autre, à une vitesse arbitraire. C'est déjà très pratique, mais pas vraiment adapté à des mouvements finements controllés; en effet nous ignorons tout de la vitesse réelle de rotation du moteur (en fonction de la résistance qu'il rencontrera lors de sa rotation il tournera plus ou moins vite pour une même commande donnée). Pour palier ce manque de précision nous avons, là encore, de nombreuses options qui s'offrent à nous. Nous pouvons par exemple asservir la commande du moteur à la lecture de capteurs qui nous renseigneront sur la rotation réellement effectuée, ou nous pouvons opter pour des moteurs "pas à pas" plus compliqués à commander mais qui offrent une bien meilleure précision de commande, ou encore nous pouvons utiliser des servomoteurs qui permettent d'obtenir directement un déplacement asservi.

Pour ce petit projet nous allons opter pour un servomoteur. J'aurai pu avantageusement partir sur un moteur pas à pas mais j'avais besoin de jouer avec des servomoteurs pour un autre projet donc j'ai décidé de faire d'un pierre deux coups et de mettre également un servomoteur dans ce micro-projet. En plus, comme les capteurs ultrason hc-sr04, les servomoteurs ont le bon gout d'être simple à utiliser et peu cher (on en trouve à moins de 2€ sur eBay, à 3$ sur amazon, à un peu plus de 4€ chez gotronic, ou encore à une quinzaine d'euros chez selectronic pour la gamme au dessus).

Servo premier prix - Creative Common by "Nick Ames" on Flickr

Généralement les servomoteurs se connectent via 3 fils. Les couleurs varient selon les constructeurs mais vous pourrez trouver facilement les équivalences en cherchant sur internet. Les servomoteurs que j'ai (les même que celui de la photo ci-dessus) suivent les couleurs traditionnelles du constructeur Graupner :

  • un fil marron que je dois brancher à la masse
  • un fil rouge que je dois brancher à mon "VCC" (alimentation 5V);
  • un fil orange sur lequel je dois envoyer la commande. Comme je vous l'ai déjà dit la commande d'un servomoteur est simple : on doit envoyer sur le fil de commande, à intervalle régulier (environ toutes les 20ms au plus), une impulsion positive dont la largeur est notre commande4. L'impulsion doit avoir une largeur comprise, environ, entre 0,5ms et 2,5ms. Lorsque la commande est correctement envoyée l'axe du servomoteur s'aligne sur un angle compris entre 0° et 180°, proportionnellement à la commande envoyée5. Donc si j'envoie, toutes les 20ms, des impulsions de 0,5ms sur la commande de mon servomoteur celui-ci va s'aligner sur -90° et ne plus en bouger. Si j'allonge mes impulsions jusqu'à 1,5ms l'axe du servomoteur va tourner environ jusqu'à 0° et ne plus bouger. Enfin si je rallonge mon impulsion jusqu'à 2,5ms l'axe va sagement s'aligner sur +90°. L'avantage d'avoir un asservissement directement dans le servomoteur c'est que son axe va s'aligner à un angle correspondant à ma commande quelque soit la résistance à son mouvement, je n'ai pas à me préoccuper d'asservir le bouzin, c'est prévu dans le forfait de base.

Un peu de dialogue

Les amateurs d'arduino sont habitués à communiquer avec leur joujou préféré depuis leur PC en le branchant simplement en USB, seulement voilà : les atmega8 n'ont pas de port USB et donc pas de méthodes native pour discutter avec un PC. Pour palier ce "problème" nous avons, encore une fois, plusieurs solutions :

Les microcontrolleurs incluant directement le support de l'USB ont l'inconvénient d'être plus cher que nos composants "low cost" (atmega8, atmega328, attinyX5, etc.), mais surtout ils sont plutôt disponible au format TQFP qu'au format DIP6, donc j'exclus cette solution pour ce mini projet.

Implémenter la pile USB sur un atmega8 c'est sympa, mais comme je ne maitrise pas encore toutes les subtilités du débugage sur microcontrolleurs et que, de toute façon, la gestion USB pomperait quasiment toutes les resources du microcontrolleur, j'exclus aussi cette solution.

Il nous reste donc le passage par un "interprète". Ca tombe bien, sur internet on peut trouver des petits montages tout fait autour du chipset cp2102 pour moins de 2€ sur eBay ou moins de 9$ sur DX.

Convertisseur UART-USB à base de CP2102

Ces petits montages discutent en USB avec l'ordinateur, et en UART avec notre microcontrolleur. Avantage certain : les chipsets cp2102 sont pris en charge directement par le noyau linux depuis la version 2.6.12, il suffit donc de le brancher pour se retrouver avec un /dev/ttyUSBX fonctionnel qui fait le pont jusqu'à notre microcontrolleur :) Coté microcontrolleur l'UART est un protocole de communication série "universel" intégré en hardware dans les atmega8 et supérieurs7 ce qui permet de ne consommer quasiment aucune ressource de calcul !

Une fois le montage branché il nous suffit donc de coder la communication. Coté microcontrolleur c'est supporté en hardware, donc après la routine d'initialisation le code d'envoi d'un octet se résume à vérifier que le flag "REady" est positionné, puis à inscrire l'octet à envoyer dans le registre dédié "UDR" (on fait difficilement plus simple...) :

/* Wait for empty transmit buffer */
while ( !( UCSRA & (1<<UDRE)) ) {};
/* Put data into buffer, sends the data */ 
UDR = data;

Coté PC c'est de la communication série standard, vous pouvez donc utiliser des softs "tout fait" comme minicom, ou dénicher une librairie de communication série pour dialoguer à partir de votre language de script préféré :

import serial
ser = serial.Serial('/dev/ttyUSB0', 4800, stopbits = serial.STOPBITS_TWO, parity = serial.PARITY_ODD)
while True:
    print ord(ser.read(1))

Bien entendu la communication peut être bi-directionnelle sans aucune modification matérielle et sans vraiment plus d'effort de code (il faut remplacer ser.read par ser.write en python et lire un registre au lieu d'en écrire un du coté de l'atmega...dur n'est ce pas ?).

Un peu de superficiel

Je suppose maintenant que vous voyez où on va avec ces différentes briquettes et le titre de ce billet : nous montons une "tourelle de surveillance" (visible sur la photo tout en haut de ce billet). Cette "tourelle" va être constituée d'un capteur de distance à ultra-son, monté sur un servomoteur, controllé par un atmega8, communiquant avec un PC via UART/USB. L'idée c'est que nous allons mesurer la distance qui nous sépare d'un obstacle "devant nous", puis nous allons faire tourner le servomoteur de quelques degrés et nous allons à nouveau faire une mesure de distance. En itérant ainsi le procédé nous pouvons obtenir une cartographie à 180° de la distance qui nous sépare du premier obstacle; obtenant ainsi une vision partielle de la pièce dans laquelle nous sommes. Grace à la communication avec le PC nous pouvons, en prime, obtenir une jolie visualisation de ces informations à la façon "radar".

Pour ne pas changer les bonnes habitudes j'ai codé tout le bouzin en python :

  • Pour réaliser la communication j'utilise la librairie "pyserial"
  • Pour faire une "jolie visualisation" j'utilise pygame.

Avec ces deux éléments, un peu d'huile de coude, et 93 lignes de python (commentaires inclus), j'obtiens un joli écran "radar" ^^ Grace à la fonction pygame.image.save c'est un jeu d'enfant de sauvegarder chaque frame affichée, et grace à la toute puissance de la ligne de commande linux je peux vous faire partager le résultat facilement :

Radar - Creative Common par ozwald, janv. 2013

Pour ceux qui ne connaissent pas la pièce ça ne ressemble sans doute pas à grand chose, mais moi je vois bien les deux murs de ma pièce, la porte (ouverte) entre les deux murs, et mon armoire tout à droite (quand au gros obstacle collé tout à gauche du capteur...c'est moi -_-)

Conclusion

Et c'est tout pour ce résumé de la création d'un micro jouet conçu et bidouillé en quelques heures seulements :) Vous noterez que j'ai jeté un voile pudique sur la partie mécanique du projet, mais j'ai bien l'intention de revenir dessus dans un article ultérieur puisque c'est un domaine sur lequel j'ai également réalisé pas mal de recherches et progrès récemment, notamment grace à l'excellent "Guerrilla guide to CNC machining, mold making, and resin casting"8 et à ce produit miracle que l'on appelle "polymorph plastic".

En guise d'ultime conclusion un petit rappel des couts de construction (hors mécanique; comptez moins de 5€ de mécanique) :

Atmega8     1.50
UART-USB    1.80
servo       2
HC-SR04     1.50
============
TOTAL       6.80 €

Alexis le 2013/12/13 10:07

Bonjour,

Dans le cadre de notre projet de Terminale, mon groupe et moi-même sommes intéressés par ce capteur.

Cependant, une phrase nous bloque: "Appliquer à la pin "TRIGGER" une impulsion de niveau haut (5V) durant au moins 10µs pour que le module démarre sa lecture" Comment déclencher cette impulsion ? Est-ce difficile pour notre niveau ?

Merci d'avance.
Cordialement,

ozwald le 2013/12/16 14:44

Déclencher cette impulsion (dont la précision n'est pas importante) est bien plus facile que de mesurer la durée d'impulsion retournée par le module... Par exemple : si vous utilisez un microcontrolleur atmel pour envoyer le signal un code comme celui-ci fera l'affaire (en supposant que la pin TRIGGER du module est branchée sur le pin PB1 du microcontrolleur) : PORTB |= 1<<PB1; _delay_us(10); PORTB &= ~(1<<PB1);. En arduino on aurai un code similaire (en supposant cette fois que la pin TRIGGER du module est branchée sur le pin 13 de l'arduino) : digitalWrite(13, HIGH); delayMicroseconds(10); digitalWrite(13, LOW);

  1. Sans compter la main d'oeuvre, évidemment ;-)
  2. Vous pouvez également utiliser un arduino, c'est juste un peu plus cher
  3. Attention, le fonctionnement de ce module HC-SR04 vu "de l'extérieur" est trivial mais je vous rassure : son fonctionnement complet est un poil plus complexe. En effet, afin d'éviter d'être perturbé par des bruits parasites, le module se charge d'envoyer en réalité plusieurs trains d'ondes séparés pour fiabiliser sa mesure. C'est l'intérêt d'un module "tout fait" par rapport à un système bricolé à la main à partir des éléments d'émission et de réception ultra-son uniquement.
  4. Une méthode simple de faire ça sans consommer de temps de calcul de notre microcontrolleur c'est d'utiliser une PWM dont la fréquence avoisinne les 50/60Hz
  5. je ne parierai pas sur la linéarité de la relation entre l'angle du servomoteur et la largeur de l'impulsion ceci-dit...
  6. Petite parenthèse : le format "DIP" c'est le format standard du prototypage, c'est facilement manipulable et soudable à la main, les pattes des composants sont espacés de 2.54mm. Le format "TQFP" en revanche est nettement plus petit et, bien qu'il soit soudable à la main, vous allez galérer pour le manipuler et le mettre en place correctement puisque les pates peuvent s'y trouver espacées de seulement 0.4mm.
  7. Les attiny n'embarquent qu'une version "light" de l'UART que je n'ai pas testé, mais j'ose espérer que ça marche quand même
  8. Guide rédigé par Michal Zalewski alias lcamtuf qui ne se contente pas d'être un expert reconnu en sécurité informatique mais qui réalise aussi de bien jolis robots. Bref : un gars certainement très bien :-D !

Petites astuces vite fait

Par Oz le - Geek
code source python

Pour renouer avec les quelques billets express que j'ai déjà pu faire voici, en vrac, une poignée d'astuces (principalement python) qui pourraient être utiles.

Swiss Knife - Creative Common by "focusforaword" on Flickr

Python2 ou Python3 ? Les deux !

J'ai l'impression que tout le monde code en python2, malheureusement "Python 3.x is the present and future of the language" donc il va bien falloir y passer. Pour ma part j'étais réticent à passer à Python3 principalement à cause d'une unique différence print "toto" en python2 (que je trouvais très pratique) devient print("toto") en python3, or des "print" j'en colle à toutes les sauces dans mes scripts. L'habitude étant ce qu'elle est je me retrouvait toujours à coller un print "toto" quelque part dans mon code et je me disait "oh et puis zut" et je basculais en 100% python2. Aujourd'hui je partage avec vous une "astuce" que j'utilise à présent pour rendre mes scripts compatibles python2 ET python3 : je remplace les print par les fonctions du module standard logging.

import logging
logging.basicConfig(level = logging.DEBUG, format="%(message)s")

//équivalent de 
// print "toto" # en python2
// print("toto") # en python3
logging.debug("toto")

Autre avantage du module logging : ça gère directement les différents niveau de verbosité1. Entre logging.debug, logging.info, logging.warning, et leurs consoeurs vous trouverez forcément le niveau de log qui vous plait; ainsi il n'est plus besoin de commenter en masse les print entre votre version de "dev" et de "prod" ^^

Le C c'est bien, mais parfois il faut l'oublier

J'aime beaucoup le C, mais sur ce coup là ça m'a joué un mauvais tour : quand j'ai voulu parser les arguments de la ligne de commande de mes scripts python je me suis intéressé au module getopt qui ressemble furieusement à ce qui existe en C. Erreur grossière : j'aurai mieux fait de regarder argparse qui est beaucoup plus pythonique et infiniment plus pratique ! A titre de comparaison voilà un exemple typique avec les deux modules. Les arguments possibles sont -h/- -help pour afficher l'aide, ou -v/- -verbose suivi d'un niveau de 0 à 4 pour régler la verbosité du programme. Voilà deux équivalents fonctionnels, l'un avec argparse, l'autre avec getopt :

import argparse, logging

parser = argparse.ArgumentParser(description='Super script de la mort qui tue')
parser.add_argument('--verbose', '-v', type=int, dest='verb', help='verbosity level [0-4]', default=4, choices=[0,1,2,3,4])
args = parser.parse_args()

logging.basicConfig(level = logging.CRITICAL-args.verb*10, format="%(message)s")
logging.debug("Debug") # cette ligne et les 2 suivante servent juste a voir que le setting du niveau de verbose fonctionne
logging.info("Info")
logging.warning("Warning")

Contre :

def usage():
    print "La on a un souci puisqu'on doit utiliser 'print'..."
    print "Et puis surtout on doit ecrire son 'usage' soit-meme alors qu'argparse l'ecrit automatiquement"
    print "Usage : %s [--verbose/-v LEVEL]"%sys.argv[0]
    print " --verbose/-v LEVEL\\tVerbosity level between 0-5 [default to 4]" 

import sys, getopt, logging
try:
    opts, args = getopt.getopt(sys.argv[1:], "v:h", ["verbose", "help"])
except getopt.GetoptError, err:
        print str(err) # will print something like "option -a not recognized"
        usage()
        sys.exit(2)

verbose = 4
for o, a in opts:
    if o in ["-v","--verbose"]:
        try:
            verbose = int(a)
        except:
            usage()
            sys.exit(2)
        if not verbose in [0,1,2,3,4]:
            usage()
            sys.exit(2)
    elif o in ['-h', '--help']:
        usage()
        sys.exit(0)

logging.basicConfig(level = logging.CRITICAL-verbose*10, format="%(message)s")
logging.debug("Debug") # cette ligne et les 2 suivante servent juste a voir que le setting du niveau de verbose fonctionne
logging.info("Info")
logging.warning("Warning")

L'avantage d'argparse est évident pour tout le monde j'espère ! Par contre, bien qu'argparse soit censé être un module standard depuis python2.7, et qu'il soit par défaut présent sur ma ubuntu 12.04, j'ai été obligé de l'installer explicitement sur ma gentoo emerge dev-python/argparse...dommage.

  1. Même si ce mot n'existe pas

« Page 3 / 11 »