Source: https://grim7reaper.rolinh.ch/
Timestamp: 2017-05-28 16:42:16+00:00

Document:
La semaine dernière j’ai aidé à résoudre un problème assez surprenant : une
partition racine pleine selon df (ou
dfc) :
df -h1
FILESYSTEM               (=) USED      FREE (-)  %USED AVAILABLE  TOTAL MOUNTED ON
/dev/dm-0                [====================]   100%     15.6G  15.6G /
Mais lorsque l’on essaye de trouver les coupables via
ncdu, la somme est loin de faire 16Go :
ncdu -x /1
.   3.8GiB [##########] /var
1.2GiB [###       ] /srv
885.2MiB [##        ] /opt
223.0MiB [          ] /lib
.  95.4MiB [          ] /etc
Les fichiers perdus dans les limbes
Après réflexion, je me suis souvenu que certains fichiers pouvaient rester
bloqués dans les limbes.
En effet, il faut savoir que, sous Linux, quand un fichier est supprimé (avec
rm par exemple), il n’est pas réellement supprimé tant qu’au moins un
processus utilise ledit fichier. Par conséquent, même si le fichier n’est plus
affiché par ls ou find, il occupe toujours de l’espace sur le disque.
Pour lister ces fichiers, on ne peut pas utiliser les commandes habituelles
telles que ls ou find, mais on peut utiliser lsof qui est une
commande qui liste les fichiers ouverts (lsof => list open files).
La sortie de lsof est très verbeuse, heureusement en lisant sa page de manuel
(man 8 lsof) on y trouve l’option L qui peut être suivi d’un nombre.
Lorsque L est suivi d’un nombre N, lsof va uniquement lister les
fichiers qui sont référencé un nombre de fois strictement inférieur à N.
Dans notre cas, comme on veut lister les fichiers supprimés (qui ne sont donc
plus référencés) il faut utiliser L1 pour lister les fichier ayant 0
En combinant lsof +L1 avec la commande sort on peut trier par taille
pour voir quels sont les fichiers occupant le plus de place.
La commande finale est donc lsof -s +L1 | sort -nk7 ce qui se traduit
par : liste (lsof) la taille (-s) de tout les fichiers supprimés et
encore ouvert (+L1) et trie (sort) cette liste de manière numérique
(-n) sur le champs 7 (-k7, car c’est le septième champs qui est le
champ de la taille).
Exemple de sortie1
% lsof -s +L1 | sort -nk7
COMMAND  PID        USER   FD   TYPE DEVICE    SIZE NLINK    NODE NAME
conky   5372 grim7reaper    0u   CHR 136,16             0      19 /dev/pts/16 (deleted)
conky   5372 grim7reaper    1u   CHR 136,16             0      19 /dev/pts/16 (deleted)
conky   5372 grim7reaper    2u   CHR 136,16             0      19 /dev/pts/16 (deleted)
systemd  709 grim7reaper  txt    REG  254,2 1018520     0 1619496 /usr/lib/systemd/systemd (deleted)
Cette commande a permis de découvrir que l’espace disque était utilisé par des
fichiers temporaires de
Terminator et libVTE
Reste à savoir pourquoi Terminator utilise autant d’espace disque.
Tout d’abord il faut savoir que le Terminator en cause ici était configuré pour
conserver toutes les sorties des terminaux, sans limite (option Inifinite
scrollback). Le problème c’est que les buffers des terminaux sont stockés
sur disques et donc que plus les terminaux sont utilisés longtemps et plus il
y’a de choses affichées dedans, plus l’espace disque occupé va augmenter.
Ce comportement ne vient pas de Terminator lui-même, mais de la
libVTE qui est une bibliothèque utilisée pour
implémenter des émulateurs de terminaux. Ce bug touche donc tout les terminaux
basés sur libVTE (Terminator, Gnome-Terminal, xfce4-terminal, guake, …).
Ce comportement existe depuis 2009, il est connu et ne sera probablement pas
corrigé, alors même que cela peut poser problème (je vous conseille de lire
ce rapport de bug
qui explique bien la situation)…
Je pense que je vais continuer d’utiliser
urxvt :D, qui est l’un des rares
terminaux à bien gérer l’espace insécable fine et qui n’est pas basé sur libVTE.
Cette petite mésaventure m’aura au moins fait découvrir un aspect fort utile
de lsof.
Depuis la version 7.01 (sortie le 2009/10/06), GDB embarque un interpréteur
Python cela qui permet d’utiliser Python comme langage de scripting pour étendre
les fonctionnalités de GDB.
Dans cet article je vais aborder la création de pretty-printer en Python.
Comme un exemple vaut mieux qu’un long discours, je vais montrer ici un cas
pratique : l’affichage d’adresses IP.
Les systèmes POSIX représentent les adresses via deux structures:
in_addr pour les IPv4 : une structure qui doit contenir au minimum un champ
s_addr de type in_addr_t (qui est équivalent à uint32_t).
in6_addr pour les IPv6 : une structure qui doit contenir au minimum un champ
s6_addr de type uint8_t[16].
Ces informations, parmi d’autres, sont disponibles
De par la définition de ces structures, si on veut les afficher dans GDB on
n’obtient quelque chose qui n’est pas très lisible pour un humain.
Soit le programme suivant :
exemple.c1
struct sockaddr_in  ipv4;
if (inet_pton(AF_INET,  &quot;82.66.107.250&quot;, &amp;ipv4.sin_addr) != 1)
if (inet_pton(AF_INET6, &quot;2a01:e35:2426:bfa0:215:ff:fe42:7fd3&quot;,
&amp;ipv6.sin6_addr) != 1)
Si on lance GDB dessus en utilisant le fichier de commande suivant :
command.gdb1
break example.c:17
print ipv4-&gt;sin_addr
print ipv6-&gt;sin6_addr
Sortie produite1
% gdb -q ./example --command=command.gdb 2&gt; /dev/null
Reading symbols from ./example...done.
Breakpoint 1 at 0x8065581: file example.c, line 17.
$1 = {s_addr = 4201333330}
$2 = {__in6_u = {__u6_addr8 = &quot;*\001\016\065$&amp;\277\240\002\025\000\377\376B&quot;, &lt;incomplete sequence \323&gt;, __u6_addr16 = {298, 13582, 9764, 41151, 5378, 65280, 17150, 54143}, __u6_addr32 = {890110250, 2696881700, 4278195458, 3548332798}}}
[Inferior 1 (process 2043) exited normally]
Le moins que l’on puisse dire c’est que ce n’est pas super human-friendly
($1 étant l’IPv4 et $2 l’IPv6).
Les pretty-printers
Nous allons créer un module python my_pp. Dans le répertoire my_pp, nous
aurons deux fichiers :
netinet.py qui contiendra le code des pretty-printers.
__init__.py qui se chargera d’enregistrer nos pretty-printers auprès
de GDB.
Commençons avec le premier fichier (j’ai omis les docstrings pour raccourcir
le code).
netinet.py1
from socket import inet_ntop, AF_INET, AF_INET6
from gdb import lookup_type
class ipv4Printer(object):
self.addr = pack(&#39;I&#39;, int(val[&#39;s_addr&#39;]))
return inet_ntop(AF_INET, self.addr)
class ipv6Printer(object):
# IPv6 addresses have a size of 128 bits (== 16 octets).
addr = val.cast(lookup_type(&quot;uint8_t&quot;).array(N))
self.addr = pack(&#39;B&#39;*N, *[int(addr[i]) for i in range(N)])
return inet_ntop(AF_INET6, self.addr)
Allons y étape par étape.
Les imports1
Les adresses IP étant stockées sous forme binaire, on importe le module struct
pour pouvoir les manipuler. Le module socket va nous servir à les convertir en
chaîne de caractères2. Enfin, on importe le module gdb.
Pretty-printer pour l’IPv41
Lorsque GDB va instancier un objet ipv4Printer, il va lui passer en argument
une structure de type in_addr. Comme dit précédemment, cette structure
contient un champ s_addr qui est un uint32_t. Notre méthode d’initialisation
se contente donc d’extraire ce champ dans le membre addr de notre objet.
Ensuite, la méthode to_string est appelée par GDB à chaque fois qu’il doit
afficher un objet de type in_addr. Cette méthode renvoie la chaîne de
caractère représentant l’adresse IP contenue dans addr.
Pretty-printer pour l’IPv61
En théorie; la version IPv6 devrait être aussi simple que la version IPv4. En
théorie… En pratique c’est malheureusement plus compliqué.
En effet, si POSIX requiert que la structure in6_addr_t possède un champ nommé
s6_addr le fait est que, la plupart du temps, ce membre n’existe pas vraiment:
c’est un symbole de préprocesseur. En tant que tel, il n’est pas accessible dans
GDB (sauf si on utilise certaines options de compilation bien spécifiques telle
que -g3 et -gdwarf-2 par exemple). Pis encore, Windows ne définit pas ce
membre du tout (Windows ne cherche pas vraiment à être conforme POSIX).
La solution que j’ai trouvée est de traiter la structure in6_addr comme un
tableau de 16 octets. Cela fonctionne et me semble relativement portable étant
donné que la structure in6_addr est définie comme une union dont l’un des
champs est effectivement un uint8_t[16] au moins sur les systèmes
d’exploitation suivant :
La méthode __init__ de notre objet va donc caster la valeur puis la stocker
dans l’attribut addr. Au niveau de la méthode to_string, c’est quasiment la
même chose que pour ipv4Printer (on remplace seulement AF_INET par AF_INET6).
Enfin, l’enregistrement de nos deux classes se fait dans le fichier
__init__.py1
from gdb.printing import RegexpCollectionPrettyPrinter
import netinet
def netinet_pp():
pp = RegexpCollectionPrettyPrinter(&quot;netinet&quot;)
pp.add_printer(&#39;in_addr&#39;,  &#39;^in_addr$&#39;,  netinet.ipv4Printer)
pp.add_printer(&#39;in6_addr&#39;, &#39;^in6_addr$&#39;, netinet.ipv6Printer)
On commence par importer le module gdb.printing, puis nos pretty-printers.
L’enregistrement se fait via un objet RegexpCollectionPrettyPrinter (via
lequel on peut donner un nom à notre groupe de pretty-printers) dans lequel nous
ajoutons nos pretty-printers. Lors de l’ajout, on donne un nom, une expression
rationnelle qui va définir les noms des types que nous pouvons afficher, puis la
classe du pretty-printer.
Maintenant que le code est fait, voyons comment utiliser tout ça.
La mise en œuvre est très simple, elle peut se faire via le fichier .gdbinit.
Il suffit d’y ajouter ces lignes :
from gdb.printing import register_pretty_printer
path.insert(0, &#39;/chemin/vers/répertoire/contenant/my_pp&#39;)
import my_pp
register_pretty_printer(gdb.current_objfile(), my_pp.netinet_pp())
Et maintenant, si on lance GDB sur notre exemple on obtient :
$1 = 82.66.107.250
$2 = 2a01:e35:2426:bfa0:215:ff:fe42:7fd3
[Inferior 1 (process 2424) exited normally]
Ce qui est, il faut l’admettre, bien plus lisible.
Pour conclure, je mentionnerai que la commande info pretty-printer permet
d’afficher la liste des pretty-printers actuellement chargé dans GDB. Il est
également possible d’utiliser l’affichage « brut » de GDB, sans passer par un
pretty-printer (même s’il en existe un pour le type) en utilisant le
modificateur /r de la commande print (par exemple print /r toto).
Voilà, ça sera tout pour cette introduction aux pretty-printers. La
documentation officielle est
pour ceux qui veulent approfondir.
je vous conseille d’utiliser au moins GDB 7.8, les versions antérieures ayant un bogue avec les types définis via typedef.↩
depuis Python 3.3, on utilise plutôt le module ipaddress↩
Premièrement, qu’est-ce que j’entends par « fonction de comparaison » ?
Je parle des fonctions qui prennent deux arguments en entrée et qui renvoie :
un nombre positif si le premier argument est strictement supérieur au
0 si les deux arguments sont égaux.
un nombre négatif si le premier argument est strictement inférieur au
Ce genre de fonction est utilisée pour ordonner des éléments (ordre total), et
elles sont donc naturellement utilisée par certaines fonctions de tri telles que
qsort.
Maintenant, pourquoi est-ce que je parle d’une erreur répandue ?
Et bien parce qu’en tapant « qsort » ou « qsort example » dans un moteur de
recherche, je tombe sur ça (il y a aussi des versions correctes, je vous
rassure) :
bien-programmer.fr
Alors, quel est le problème ?
Prenons l’exemple donné sur le site
cplusplus.com :
Si on teste cet exemple, on obtient bien le résultat attendu :
10 20 25 40 90 100
Bien, et si maintenant on utilise1 :
int values[] = { 40, INT_MIN, 100, INT_MAX, 20, INT_MIN + 42 };
Alors là, rien ne va plus ! En sortie on obtient :
40 100 2147483647 -2147483648 -2147483606 20
Ce qui ne correspond pas du tout à un tableau trié par ordre croissant.
Le souci vient de la fonction de comparaison :
Une mauvaise fonction de comparaison1
L’idée derrière cette fonction est que comme on doit renvoyer un nombre positif
si a > b, négatif is a < b et 0 si a = b alors autant renvoyer la
En théorie, ça fonctionne. En pratique, les nombres représentables dans un int
sont bornés et il y a donc risque d’overflow lorsque l’on approche les valeurs
limites. Et en effet, l’opération a - b peut produire un résultat non
représentable dans un int.
À toutes fins utiles, je rappelle que les overflow d’entiers signés en C sont
un comportement non-défini. Cela vient en partie du fait que le standard du C
n’impose rien sur la représentation des entiers signés (signe et magnitude,
complément à un ou complément à deux) et ne veut donc pas dicter le comportement
attendu pour ce genre de situation.
La solution est très simple. Étant donné que l’on souhaite faire une fonction de
comparaison il suffit d’utiliser les opérateurs de comparaison afin d’éviter
tout problème d’overflow :
Une fonction de comparaison sûre1
const int* left = a;
const int* right = b;
if (*left &gt; *right)
else if (*left &lt; *right)
Et pour ceux qui trouvent l’alternative précédente trop verbeuse, il est
toujours possible l’utiliser l’idiome suivant :
Une fonction de comparaison sûre et courte1
return (*left &gt; *right) - (*left &lt; *right);
Cette version se base sur le fait que les opérateurs > et < renvoie 0 ou 1,
ce qui est garanti par le standard (Cf. ISO/IEC 9899:TC3, 6.5.8 Relational
operators §6, page 86) :
[...]Each of the operators < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to) shall yield 1 if the specified relation is true and 0 if it is false.)The result has type int.[...]ISO/IEC 9899:TC3 www.open-std.org/jtc1/sc22/wg14/…
Cette version est donc absolument portable et devrait même fonctionner sur un
DS9K :P
on n’oubliera pas d’ajouter #include <limits.h>↩
J’ai été confronté à ce bogue le 2013/08/25, je n’avais donc pas encore
découvert le comportement indéfini lié à offsetof (que j’ai découvert lors
l’article précédent)
ce qui explique pourquoi je ne le mentionne pas ici.
Le 2013/08/25, j’ai été confronté à un bogue assez surprenant au premier abord.
Comme cela faisait longtemps que je n’étais pas tombé sur un bogue de ce genre,
j’ai eu envie de partager le raisonnement que j’ai suivi pour en venir à
bout (au cas où cela puisse servir à d’autres) sur le forum
ubuntu-fr.
Ce bogue fut aussi, d’une certaine façon, ce qui a conduit à l’ouverture de ce
J’étais en train d’implémenter une liste linéaire simplement chaînée. Une liste
générique, mais sans utiliser void* comme on le fait traditionnellement.
Au lieu de cela, j’avais décidé de m’inspirer d’une méthode assez connue qui est
utilisée dans le noyau Linux (utilisation d’une structure de données intrusive).
Le morceau de code qui suit permet d’avoir un aperçu de l’interface de la liste.
Il s’agit en fait d’une version minimale du véritable programme d’exemple auquel
j’ai retiré la gestion des erreurs (malloc) et la libération de la mémoire
allouée car ce n’est pas le sujet ici (ne vous inquiétez pas, dans le programme
d’origine tout cela est présent).
Exemple d’utilisation de la liste chaînée1
slist_node node;
unsigned from;
} pair_list;
slist_list list;
pair_list* elt;
slist_init(&amp;list);
/* Populate the list. */
elt = malloc(sizeof *elt);
elt-&gt;from = i;
elt-&gt;to   = i+1;
slist_add(&amp;list, &amp;(elt-&gt;node));
/* Iteration with `slist_foreach_elt`. */
slist_foreach_elt(&amp;list, elt, pair_list, node)
printf(&quot;from = %u | to = %u\n&quot;, elt-&gt;from, elt-&gt;to);
Voyons maintenant ce que l’on obtient en sortie en l’exécutant.
Compilation avec gcc -O01
from = 1 | to = 2
from = 0 | to = 1
Compilation avec clang -O01
Compilation avec gcc -O21
Compilation avec clang -O21
Tiens, tiens. Comme c’est intéressant : un bogue qui n’apparaît que lorsque l’on
active les optimisations, et sur un seul compilateur en plus !
Quand on a une différence de comportement entre deux compilateurs, il y a deux
solutions possibles :
l’un des deux compilateurs a un bogue (peu probable, mais cela arrive).
notre code contient :
un comportement non défini (undefined behavior, que je vais abréger par
UB par la suite).
OU un comportement non spécifié (unspecified behavior).
OU un comportement dépendant de l’implémentation
(implementation-specific behavior).
La différence entre les trois étant parfois oubliée, je vais faire un petit
rappel ici. Tout d’abord, il faut savoir que le standard décrit une machine
abstraite C (C abstract machine) qui peut être vu comme un interpréteur C.
un comportement dépendant de l’implémentation (comme la taille d'un int
par exemple) peut être vu comme un paramètre de la machine abstraite. Le
standard peut proposer plusieurs comportements possibles, auquel cas
l’implémentation doit en choisir un parmi ceux proposés. Si le standard ne
spécifie rien, alors l’implémentation est libre de choisir son comportement.
Dans tous les cas, l’implémentation doit documenter ses choix (on trouve ceux
de GCC ici).
un comportement non spécifié (comme l’ordre d’évaluation des arguments d’une
fonction) peut être vu comme un comportement non-déterministe de la machine
abstraite. Comme pour un comportement dépendant de l’implémentation, le
standard peut proposer plusieurs possibilités et l’implémentation doit faire
un choix parmi eux. Sinon, elle est libre de choisir le comportement
qu’elle souhaite. En revanche, ces comportements ne sont pas forcément
documentés par l’implémentation.
un comportement non défini (comme déréférencer un pointeur NULL) est un
comportement pour lequel le standard n’offre aucune garantie. Un compilateur
qui rencontre un UB est libre de faire ce qu’il veut, ABSOLUMENT TOUT
ce qu’il veut. Ce qui inclut (entre autres) :
produire une erreur de compilation
supprimer le code qui contient l’UB
ignorer le standard (oui, un seul UB à un endroit rend TOUT le code
indéfini)
générer un programme qui va crasher à l’exécution
générer un programme qui va produire de mauvais résultats
générer un programme qui va fonctionner comme prévu (et oui, cela arrive)
générer un programme qui va formater votre disque dur
générer un programme qui va afficher 42
faire sortir des démons de votre nez.
Ceci dit, les UB ont aussi leurs bons côtés : c’est en partie grâce à leur
existence que les compilateurs C peuvent faire de nombreuses optimisations. À ce
sujet, je vous conseille cet excellent article en trois parties écrit par
Chris Lattner (qui sait donc très
bien de quoi il parle) :
Tant que j’en suis à donner des liens, cet article en trois parties de John
Regehr est aussi très instructif sur les UB :
Étant donné que :
je connaissais les articles précédemment cités.
mon bogue apparaît seulement avec les optimisations activées (avec clang, il
est présent à partir de O1 et avec gcc le programme s’exécute correctement
quel que soit le niveau d’optimisation).
mon code joue avec des pointeurs, des adresses, des structures et des macros.
Je suis donc directement partie sur la solution 2.
J’ai commencé par utiliser Memcheck sur la version compilé et optimisé par clang
(entre temps, j’ai pris soin de recompiler avec le flag -g en plus de -O2
pour avoir les informations de déboguage).
Sortie de Memcheck1
==7949== Invalid read of size 4
==7949==    at 0x4006A0: main (min_bug.c:34)
==7949==  Address 0xfffffffffffffff8 is not stack&#39;d, malloc&#39;d or (recently) free&#39;d
Petite précision utile : la ligne 34 c’est le printf dans la boucle.
Donc a priori, la boucle fait trop d’itérations et essaye d’afficher des
éléments en dehors de la liste. Étrange, sachant que le même code sans
optimisations est tout à fait correct (Memcheck ne rapporte rien d’anormal).
Donc une boucle tout à fait correcte, deviens boguée après optimisations. Hum…
Suite à cela, j’ai tenté de passer par gdb. En vain… La variable d’intérêt,
elt, était optimized out (même en O1, et comme en O0 le bogue ne se
manifeste pas…).
Du coup, j’ai ajouté un printf pour afficherelt et elt->node (ce sont
les variables utilisées dans la condition d’arrêt du foreach) à chaque
itération et j’ai pu constater que quand elt->node atteint la valeur
nécessaire à l’arrêt de la boucle, et bien cette dernière ne s’arrête pas.
Bien, ma condition semble avoir disparue, c’est très ennuyeux ça. Je dois le
vérifier. Et pour cela, il n’y a qu’une seule façon fiable : regarder le code
assembleur généré.
En avant donc. Jetons d’abord un coup d’œil au code produit par gcc (je commence
par celui là, car je sais que même optimisé ma boucle est toujours correcte).
Sortie de gcc -g -O1 -S (code de la boucle uniquement)1
call    slist_first
leaq    -8(%rax), %rdx
.loc 1 34 0 discriminator 2
movl    16(%rdx), %esi
movl    (%rdx), %edx
.loc 1 33 0 discriminator 2
call    slist_next
Dans les dernières lignes on voit bien une comparaison (testq) suivi d’un
saut conditionnel (jne) en début de boucle : tout va bien.
Sortie de clang -g -O1 -S (code de la boucle uniquement)1
.loc    1 33 0                  # examples/min_bug.c:33:0
callq   slist_first
.LBB0_3:                                # =&gt;This Inner Loop Header: Depth=1
.loc    1 34 0                  # examples/min_bug.c:34:0
movl    -8(%rbx), %edx
movl    8(%rbx), %esi
callq   slist_next
Et là : BAM !!!
La dernière instruction de la boucle est un saut inconditionnel en début de
boucle. Pas de cmp ou test. Mais un bon gros jmp tout ce
qu’il y a de plus inconditionnel. La différence saute au yeux !
C’est donc une belle boucle infinie, ce qui explique que je continue à boucler
même après avoir rencontré ma condition d’arrêt (et donc cela explique également
le fait que j’aille lire des adresses pourries qui me font faire une jolie
Maintenant, on est sûr que le problème vient de la boucle, et donc de
slist_foreach_elt. Regardons donc ça de plus près :
slist_foreach_elt1
#define slist_foreach_elt(list, curr, type, fieldname)         \
for (curr = slist_elt(slist_first(list), type, fieldname); \
&amp;(curr-&gt;fieldname) != NULL;                           \
curr = slist_elt(slist_next(&amp;(curr-&gt;fieldname)), type, fieldname))
Hooooo yeaaah :)
Sachant que slist_elt est aussi une macro :
slist_elt1
#define slist_elt(node, type, fieldname) \
((type*)((char*)(node) - offsetof(type, fieldname)))
Bon, ça peut faire peur de voir ça comme ça, à froid et sans explication :D
Mais en réalité c’est très simple, et c’est le cœur même de l’astuce qui permet
d’avoir une liste générique sans passer par du void*.
Cela dit, je ne vais pas m’étendre sur le sujet du pourquoi du comment ça
fonctionne (voire
Donc sans plus d’explications, je vous montre la ligne fautive (qui est belle
est bien dans le foreach) :
&amp;(curr-&gt;fieldname) != NULL;                          \
Ça peut sembler bizarre comme test, mais en fait ça tient la route (enfin
presque, à un détail près et c’est bien pour cela que ça plante)
Pour information, voilà la version que l’on trouve dans le
noyau Linux :
&amp;pos-&gt;member != (head);    \
C’est presque exactement le même code. À un détail près.
Détail qui vient du fait que la version Linux est une liste circulaire
doublement chaînée. Et le point crucial ici est : circulaire.
Car grace à cette propriété, la condition d’arrêt est un test contre la tête de
la liste, pas contre NULL.
Différence minime ? C’est ce que je pensais. Mais pas du tout, c’est une énorme
différence : la différence entre un code correct et un UB.
Lorsque je suis sur le dernier nœud (dont le pointeur next vaut NULL), je
vais exécuter cette ligne (la partie incrémentation du foreach) :
Donc ici, je vais appeler slist_elt avec le contenu du pointeur next (car
j’appelle slist_next), c’est-à-dire avec NULL.
Cela donne :
Et donc dans le cas présent :
((pair_list*)((char*)(NULL) - offsetof(pair_list, node)))
offsetof renvoie, comme son nom l’indique, l’offset (en byte) d’un champ
Ici, je veux l’offset de mon nœud dans la structure. Ça me retourne donc
8 (en effet, dans la structure pair_list il y a un unsigned avant
ce champ, et les unsigned sur ma machine font 4 bytes et il y a 4
bytes de padding pour aligner le champ suivant).
NULL étant défini, au niveau du code source (ce n’est pas toujours vrai après
compilation), comme étant 0 cela donne donc : 0 - 8.
NULL étant un pointeur et les pointeurs étant non signés et sur 64 bits (sur
ma machine), on obtient l’adresse suivante : 0xfffffffffffffff8
Tiens, c’est l’adresse qui était apparue dans Valgrind ça ;)
Donc après exécution de cette partie « incrémentation » de la boucle for,
on retourne au début de la boucle et on va tester la condition :
Ce qui donne dans mon cas :
&amp;(elt-&gt;node) != NULL;                          \
curr vaut 0xfffffffffffffff8, l’offset du champ auquel je souhaite
accéder est 8. Cela donne donc 0 quand je les additionne. C’est bien égal à
NULL, ma boucle s’arrête et c’est parfait.
Car mon implémentation contient un UB et donc le compilateur peut produire
Du point de vue de la norme
Voici quelques extraits choisis de la norme du C99 :
6.3.2.3 Pointers[…]3An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.[…]ISO/IEC 9899:TC3 www.open-std.org/jtc1/sc22/wg14/…
6.5.6 Additive operators[…]7For the purposes of these operators, a pointer to an object that is not an element of an array behaves the same as a pointer to the first element of an array of length one with the type of the object as its element type.8When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the pointer operand points to an element of an array object, and the array is large enough, the result points to an element offset from the original element such that the difference of the subscripts of the resulting and original array elements equals the integer expression. In other words, if the expression P points to the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and (P)-N (where N has the value n) point to, respectively, the i+n-th and i−n-th elements of the array object, provided they exist. Moreover, if the expression P points to the last element of an array object, the expression (P)+1 points one past the last element of the array object, and if the expression Q points one past the last element of an array object, the expression (Q)-1 points to the last element of the array object. If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated.[…]ISO/IEC 9899:TC3 www.open-std.org/jtc1/sc22/wg14/…
Réfléchissons un peu. Qu’est-ce que je fais dans cette condition :
Je compare l’adresse d’un champ de ma structure (donc l’adresse d’un objet) avec
NULL (qui ne peut pas être l’adresse d’un objet, cf. §6.3.2.3/3), partant
de là le compilateur sait que le test ne renverra jamais faux et il peut donc le
De manière générale, toute opération arithmétique avec un pointeur NULL
débouche sur un UB. En effet, la condition 6.5.6/8 sera toujours violé (vu que
le §6.3.2.3/3 garanti que NULL est différent de tout sauf de lui-même, le
résultat de l’opération ne pourra jamais pointer sur le même objet que NULL
(vu que par définition il ne pointe sur rien)).
Même faire NULL - NULL c’est un UB. C’est presque le même principe que
précédemment, sauf que c’est le §6.5.6/9 au lieu de §6.5.6/8 qui s’applique (je
laisse les curieux consulter la norme).
Pour terminer sur une petite remarque,
la norme du C++ est un peu plus défini à ce
niveau-là car elle autorise deux opérations arithmétiques sur les pointeurs
nuls :
NULL + 0 ou NULL - 0 renvoie NULL
NULL - NULL renvoie 0
Pour citer la norme :
5.7 Additive operators[…]7If the value 0 is added to or subtracted from a pointer value, the result compares equal to the original pointer value. If two pointers point to the same object or both point one past the end of the same array or both are null, and the two pointers are subtracted, the result compares equal to the value 0 converted to the type std::ptrdiff_t.[…]ISO/IEC 14882 www.open-std.org/jtc1/sc22/wg21/…
Si vous voulez en savoir plus à propos de pourquoi C++ supporte cela, je vous
conseille la lecture de cet
Comme le problème vient du fait d’appeler slist_elt avec NULL il suffit de
reformuler la boucle for pour éviter ce cas de figure :
Nouvelle implémentation de slist_foreach_elt1
#define slist_foreach_elt(list, curr, type, fieldname)                     \
for (curr = slist_elt(slist_first(list), type, fieldname);         \
curr != NULL;                                                 \
curr = curr-&gt;fieldname.next == NULL ? NULL :                  \
slist_elt(slist_next(&amp;(curr-&gt;fieldname)), type, fieldname))
Deux autres solutions possibles :
implémenter une liste circulaire (comme le noyau Linux), du coup on testera
contre l’adresse de la tête.
créer un nœud global dont l’adresse servira de valeur nulle, on fera donc la
comparaison contre ce nœud au lieu de NULL.
Tester son code avec plusieurs compilateurs, c’est très utile.
Rien n’est magique, tout est logique (même si pour remonter le fil, il faut
parfois des connaissances en standard du C, optimisations faites par les
compilateurs et lecture de l’assembleur produit ^^).
Structures de données génériques en C
Contrairement à d’autres langages (tels que C++, Ada, Java, …) le C n’offre
pas de réel support pour la programmation générique. Cela ne signifie pas que la
programmation générique est impossible en C, par contre elle nécessite plus
d’effort pour être mise en œuvre. Dans cet article, je vais présenter deux
approches :
une approche à base de void*, que je nomme approche « traditionnelle ».
une approche intrusive, sur laquelle je m’attarderai un peu plus car elle me
semble moins connue.
Dans la suite de cet article, je vais prendre l’exemple d’une liste simplement
chaînée. En effet, c’est une des structures de données de bases et on la
retrouve dans d’autres structures de données (certaines tables de hachage par
exemple) ou algorithmes. Elle peut même servir de support à un langage (comme
les dialectes Lisp où tout est liste, le programme lui-même y compris).
Approche « traditionnelle »
L’approche la plus courante (on la retrouve dans la
et les EFL
entre autres) est l’utilisation d’un pointeurvoid*. Pour implémenter une liste
simplement chaînée générique on pourrait donc avoir le code suivant :
slist.h1
/* Head of a singly-linked list. */
typedef struct slist_s {
size_t length;              /* List length. */
struct slist_node_s* first; /* List head.   */
/* A node of a singly-linked list. */
typedef struct slist_node_s {
struct slist_node_s* next; /* Next element. */
void* data;                /* User data.    */
} slist_node;
Ici j’ai choisi d’avoir une structure à part pour la tête (ce qui permet d’avoir
accès à longueur de la liste sans avoir à la parcourir), mais ce n’est qu’un
détail d’implémentation. Chaque nœud de la liste contient un pointeur vers le
nœud suivant et un pointeur void* sur les données.
Maintenant que la liste est définie, il faudrait stocker quelque chose dedans.
Je vais partir sur une structure qui représente un labyrinthe :
maze.h1
/* A cell of the maze. */
/* A Maze. */
typedef struct maze_s {
Cell* maze;
size_t m;   /* Number of lines.             */
size_t n;   /* Number of columns.           */
size_t in;  /* Index of the starting point. */
size_t out; /* Index of the exit.           */
En combinant les deux fichiers précédents on peut donc créer une liste de
labyrinthes. Cela peut être représenté graphiquement de la manière suivante :
L’avantage de cette approche est qu’elle est relativement naturelle et simple à
comprendre. En revanche, elle comporte quelques inconvénients :
le fait d’utiliser un pointeur pour les données stockées implique qu’il
faudra presque toujours allouer de la mémoire pour les données. On se
retrouve donc avec deux allocations par élément de la liste : allocation de
la donnée et allocation du nœud lui-même.
un autre souci, même s’il est probablement rarement ressenti dans la plupart
des cas, est celui des performances : à chaque fois que l’on veut accéder à
un élément de la liste à partir du nœud on doit déréférencer un pointeur. Ce
n’est pas gratuit.
L’impact du point 1 peut être réduit en utilisant une stratégie d’allocation
adaptée (la GLib propose
dans ce sens).
Si la perte de performance due au point 2 se fait vraiment sentir, il est
toujours possible remplacer l’implémentation naïve de la liste par une
liste « déroulée » afin
d’augmenter la localité des données et de bénéficier de l’usage du cache.
Approche intrusive
Tout d’abord, il me semble utile de définir ce que l’on appelle une structure de
données intrusive.
Dans le cas « normal », le conteneur et le contenu sont totalement indépendants.
Le conteneur n’a pas besoin de savoir s’il va servir à stocker des int ou
une structure Maze pour être défini. De même, une structure Maze n’a pas
besoin de savoir si elle va être stockée dans une liste pour être définie
(souvenez vous de la première partie de cet article : slist.h et maze.h sont
totalement indépendants).
En revanche, dans le cas d’une structure de données intrusive, la donnée
contenue (la structure Maze dans mon exemple) devra être définie en tenant
compte du conteneur (dans notre exemple, cela signifie que maze.h dépendra de
slist.h).
La liste est toujours indépendante de son contenu (comme dans l’approche
« traditionnelle »).
Définition quasiment identique à la définition précédente. À un détail près :
chaque nœud de la liste ne contient qu’un pointeur vers le nœud suivant et rien
d’autre. Aucune référence à son contenu (le champ data a disparu).
En effet, le lien entre la liste et la donnée est maintenant embarqué dans la
donnée elle-même (on voit bien le côté intrusif) comme le prouve le nouveau
champ node ajouté à la structure Maze :
labyrinthes. Cela peut être représenté graphiquement de la manière suivante (on
voit bien la différence par rapport à l’approche « traditionnelle ») :
Avec cette approche, nous n’avons plus les inconvénients dus à l’utilisation de
void* :
plus besoin d’allouer le nœud ET la donnée : une seule allocation suffit.
plus de déréférencement pour accéder à la donnée à partir du nœud.
Contrairement à ce que l’on pourrait penser, une liste intrusive n’est pas plus
difficile à utiliser qu’une liste « traditionnelle » et ne nécessite pas plus de
code. D’ailleurs, on retrouve ces structures intrusives dans le noyau Linux
(liste doublement chaînée
exemple), dans l’en-tête sys/queue.h chez les
*BSD (Linux en
propose aussi une implémentation) et dans
pour ne citer que quelques exemples connus.
Cependant, cette approche n’est pas parfaite non plus :
on utilise de l’espace pour le nœud de la liste même si la donnée n’est pas
stockée dans une liste.
il faut avoir le contrôle sur la définition des données que l’on veut stocker
afin pouvoir ajouter le membre nécessaire à l’utilisation de la liste.
il faut avoir un moyen d’accéder à la donnée à partir d’une référence au
Les problèmes 1 et 2 peuvent être contournés en encapsulant la donnée dans une
structure dédiée.
/* Interesting fields. */
struct data_wrapper {
struct Data       data;
struct slist_node node;
Cependant, on perd la facilité d’utilisation.
La solution au problème 3 sera abordée plus loin dans cet article.
Voici un petit exemple d’utilisation : on crée une liste de 5 éléments, on
l’affiche puis on libère la mémoire.
Exemple d’utilisation1
slist list;
slist_node* curr;
slist_node* tmp;
pair_list*  elt;
/* Display the list. */
slist_foreach(&amp;list, curr) {
elt = slist_elt(curr, pair_list, node);
slist_foreach_safe(&amp;list, curr, tmp) {
slist_del(&amp;list);
free(elt);
if (!slist_is_empty(&amp;list)) {
Lignes 6-10 on définit notre structure (qui pourrait représenter un intervalle).
On n’oublie pas d’y intégrer un membre slist_node afin de pouvoir en faire
Ligne 14 on déclare notre liste puis on l’initialise ligne 20. S’en suit une
boucle (lignes 22-30) pour la peupler : on alloue 5 structures et on les ajoute
à la liste les unes à la suite des autres. On remarque que pour ajouter un
élément à la liste on ne passe pas la structure elle-même mais uniquement son
champ node (ligne 29, second paramètre de slist_add).
La seconde boucle (lignes 32-35) affiche le contenu de notre liste.
slist_foreach permet de parcourir la liste nœud par nœud. À partir du nœud,
on récupère notre structure et on l’affiche.
Enfin, la dernière boucle (lignes 37-41) libère la mémoire allouée. On remarque
que l’on supprime d’abord le nœud de la liste via slist_del avant de libérer
la mémoire allouée à notre structure (ce qui est logique étant donné que la
structure contient le nœud, si on la désalloue en premier on perd l’accès au
nœud).
L’exécution de ce code nous donne :
from = 4 | to = 5
from = 3 | to = 4
from = 2 | to = 3
Le code est relativement clair et concis, rien de bien compliqué. Cependant, la
ligne 33 nous rappelle le problème 3 : comment peut-on récupérer la structure à
partir du nœud ?
Dans l’approche traditionnelle, c’est très simple : étant donné que le nœud
contient un pointeur sur les données, il suffit de faire node->data pour
accéder aux données. Mais dans l’approche intrusive, c’est la donnée qui
contient notre nœud : comment peut on accéder à une structure à partir d’un de
ses champs ?
Champ en première position
La norme (ISO/IEC 9899:TC3, 6.7.2.1 Structure and
union specifiers § 13, page 103) nous dit :
Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared. A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.ISO/IEC 9899:TC3 www.open-std.org/jtc1/sc22/wg14/…
Cela signifie qu’il n’y a jamais de padding avant le premier membre d’une
structure et que son adresse correspond donc à l’adresse de la structure
Par conséquent, si le champ slist_node est le premier de la structure, il
suffit d’un simple cast pour obtenir l’adresse de la structure. Cette solution
est très simple et très efficace (un cast en lui-même ne coute rien à
l’exécution) mais souffre de deux inconvénients :
manque de flexibilité : l’utilisateur doit faire en sorte que le champ
slist_node soit le premier de la structure.
il est impossible de stocker la structure dans plus d’une liste (comme
qui peut à la fois être dans une liste de bus (champ node) et dans une liste
de bus enfant (champ children)) car on ne peut avoir qu’un seul
slist_node en première position.
Utilisation d’offsetof
La solution pour être indépendant de la position du champ (et donc de pouvoir
avoir plusieurs champs slist_node par structure) est d’utiliser une macro
méconnue mais pourtant fort utile : offsetof (définie dans stddef.h).
Cette macro prend deux arguments :
un nom de structure (pair_list par exemple).
un nom de champ (node par exemple).
Elle renvoie alors l’offset1 du champ dans la structure, en byte2. À
partir de là, c’est très simple : il suffit de calculer l’offset via offsetof
puis de soustraire cette valeur à l’adresse du champ qui contient le nœud afin
d’obtenir l’adresse de la structure.
Pour éviter de se retrouver avec une dépendance au nom du champ, on passe par la
macro suivante :
* Returns a pointer to the structure which contains the node.
* \param node      a list node (slist_node*).
* \param type      type of the structure which contains the node.
* \param fieldname name of the node (field name) in the structure.
* \pre `node` must be not NULL.
* \remarks Complexity: O(1)
Cette macro prend trois arguments :
l’adresse du nœud (l’adresse du champ slist_node).
le type de la structure (pair_list par exemple) : nécessaire pour convertir
le type de l’adresse retournée.
le nom du champ slist_node utilisé comme premier argument.
Seul point un peu délicat : il faut absolument convertir node en char*
sinon la soustraction ne va pas renvoyer l’adresse que l’on souhaite. En effet,
soit P un pointeur T* et N un nombre entier : P-N est
équivalent à P-N*sizeof(T) (arithmétique des pointeurs de base). Or,
l’offset renvoyé par offsetof est exprimé en byte, il faut donc faire la
soustraction avec un pointeur sur un type T vérifiant sizeof(T)==1.
Sachant que la norme (ISO/IEC 9899:TC3, 6.5.3.4 The
sizeof operator § 3, page 80) nous dit :
When applied to an operand that has type char, unsigned char, or signed char, (or a qualified version thereof) the result is 1.ISO/IEC 9899:TC3 www.open-std.org/jtc1/sc22/wg14/…
sizeof(char) renvoyant toujours 1 par définition, il suffit donc de convertir
node en char* avant la soustraction pour obtenir le résultat souhaité.
Cependant, cette solution a un gros problème3 : elle contient un comportement
indéfini ! En effet, l’arithmétique des pointeurs n’est pas aussi simple qu’elle
en à l’air. Il y a un certains nombre de règles à respecter, parmi lesquelles
ISO/IEC 9899:TC3, 6.5.6 Additive operator § 7, page 83 :
For the purposes of these operators, a pointer to an object that is not an element of an array behaves the same as a pointer to the first element of an array of length one with the type of the object as its element type.ISO/IEC 9899:TC3 www.open-std.org/jtc1/sc22/wg14/…
et ISO/IEC 9899:TC3, 6.5.6 Additive operator § 8, page 83 :
[...]If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.[...]ISO/IEC 9899:TC3 www.open-std.org/jtc1/sc22/wg14/…
Qu’est ce que cela implique dans notre cas ?
On applique une soustraction à un pointeur qui pointe sur le champ slist_node
d’une structure. De manière évidente, ce pointeur ne pointe pas sur un élément
contenu dans un tableau donc la première règle que j’ai cité va être appliquée
et on va considérer node comme un pointeur sur le premier élément d’un tableau
de char (et oui, on convertit node en char* avant d’appliquer la
soustraction) de taille 1. Jusque là, on est sauf.
Ensuite, on applique notre soustraction. On va donc soustraire un certain nombre
de byte à l’adresse contenue dans le pointeur : le résultat pointera donc à une
adresse située avant node. Et là, la seconde règle va nous frapper : le
pointeur node et l’adresse résultante ne pointe pas sur le même tableau (étant
donné que l’adresse résultante pointe avant le tableau), on a donc un undefined
behavior (comportement indéfini). GAME OVER !
En conclusion, le choix entre les deux approches dépend principalement de vos
objectifs, de vos contraintes et de vos données.
Si les données peuvent exister de manière indépendante, en dehors de toute
structure, la première approche est probablement plus logique. De la même
manière, si ce sont des données que vous ne pouvez pas modifier pour y ajouter
les champs nécessaires, il est probablement plus pratique d’utiliser l’approche
« traditionnelle ».
D’un autre côté, si les données font nécessairement partie d’une liste, on peut
envisager l’approche intrusive (en plaçant le champ requis en premier dans la
structure). En revanche, si les données doivent pouvoir être stockées dans
plusieurs conteneurs à la fois il faudra revenir à la première approche ou
utiliser l’approche à base d’offsetof (tout en étant conscient que cela se
base sur un comportement indéfini et qu’un compilateur pourrait « casser » le
code en l’optimisant).
D’autres critères peuvent bien sûr entrer en compte (si les ajouts/suppressions
sont très frèquents, une approche intrusive peut être plus efficace). Il n’y a
pas de règle absolue en la matière.
Pour ceux que cela intéresse, le code complet de la liste simplement chaînée
(version intrusive) est disponible ici.
Attention : Ce code ayant été écrit longtemps avant la rédaction de cet
article, il est donc basé sur la méthode offsetof qui contient un
comportement indéfini (dont j’ignorai l’existence au moment de l’écriture du
Le code est sous licence BSD-3. Il est écrit en C89 et aucune dépendance
particulière n’est requise (hormis Check pour
les tests unitaires). La compilation se fait via CMake, une documentation peut
être générée par Doxygen. Deux exemples sont disponibles dans le dossier
« décalage » en bon français.↩
« multiplet » en bon français. J’en profite pour signaler qu’un byte n’est pas forcément égal à un octet (même si, de nos jours, c’est le cas le plus répandu) et que ce n’est pas non plus un synonyme de bit (Cf. Wikipédia pour plus d’information).↩
problème que j’ai remarqué lors de la rédaction de cet article.↩
Crackme — Python
Le crackme étudié dans cet article provient
Cinquième crackme, et cette fois nous allons travailler non pas sur du code
machine, mais sur du bytecode Python.
Un fichier Python compilé reste un fichier binaire, donc nous pouvons commencer
par la traditionnelle commande strings.
% strings ch19.pyc
__main__u$
Welcome to the RootMe python crackmeu
Enter the Flag: u&#39;
I know, you love decrypting Byte Code !i
You Winu
Try Again !N(
__name__u
SOLUCEu
KEYOUTu
crackme.pyu
Bon rien de bien concluant. Il est maintenant temps de se renseigner sur ce
fameux fichier de bytecode, et plus particulièrement sur son format. Après
quelques recherches, je suis tombé sur
de la liste de diffusion Python-ideas. La partie intéressante est la
Currently .pyc files use a very simple format:- MAGIC number (4 bytes, little-endian)- last modification time of source file (4 bytes, little-endian)- code object (marshaled)Gabriel Genellina
Et bien ça c’est une bonne nouvelle :-). Le format de ce fichier est extrêmement
simple. Deux entiers 32-bit et un objet code sérialisé.
Cela dit, le fait que l’objet code soit sérialisé via le module
marshal plutôt que via le
module pickle implique qu’il
va nous falloir utiliser une version de Python relativement proche de celle qui
a généré le bytecode, sinon on risque de ne pas pouvoir désérialiser l’objet.
Le crackme ayant été publié le 3 juillet 2013, je vais tenter ma chance avec
le Python 3.3 de mon Archlinux.
Essayons donc de lire ce fichier :
% bpython
&gt;&gt;&gt; pyc = open(&#39;ch19.pyc&#39;, &#39;rb&#39;)
&gt;&gt;&gt; magic_number = struct.unpack(&#39;&lt;i&#39;, pyc.read(4))[0]
&gt;&gt;&gt; hex(magic_number)
&#39;0xa0d0c4f&#39;
&gt;&gt;&gt; timestamp = struct.unpack(&#39;&lt;i&#39;, pyc.read(4))[0]
&gt;&gt;&gt; time.strftime(&#39;%Y/%m/%d&#39;, time.localtime(timestamp))
&#39;2013/07/02&#39;
&gt;&gt;&gt; code = marshal.load(pyc)
&lt;code object &lt;module&gt; at 0x7fb7b60e80c0, file &quot;crackme.py&quot;, line 8&gt;
&gt;&gt;&gt; pyc.close()
Ok, c’est bien du Python 3. À titre de comparaison, voilà ce qui arrive si l’on
essaye de lire le fichier avec du Python 2.7.
Python 2.7.5 (default, Sep  6 2013, 09:55:21)
[GCC 4.8.1 20130725 (prerelease)] on linux2
Ok, on arrive donc bien à désérialiser notre binaire en un objet code. Et
maintenant, on fait quoi avec cet objet ?
C’est là que l’excellente et très complète bibliothèque standard de Python
intervient. Dans cette bibliothèque on trouve une section
qui contient le module dis. Et
c’est exactement ce qu’il nous faut !
Allez, faisons un petit script pour désassembler ce fichier compilé et avoir du
bytecode à nous mettre sous la dent.
disass.py1
def disassemble(filepath):
with open(filepath, &#39;rb&#39;) as pyc:
# Read the magic number.
_ = struct.unpack(&#39;&lt;i&#39;, pyc.read(4))
# Read the timestamp
# Read the code.
code = marshal.load(pyc)
disassemble(sys.argv[1])
print(&#39;Error: missing argument&#39;, file=sys.stderr)
print(&#39;Usage: {} file.pyc&#39;.format(sys.argv[0]))
Et maintenant, appliquons-le à notre crackme.
% ./disass.py ch19.pyc
8           0 LOAD_NAME                0 (__name__)
3 LOAD_CONST               0 (&#39;__main__&#39;)
9 POP_JUMP_IF_FALSE      219
9          12 LOAD_NAME                1 (print)
15 LOAD_CONST               1 (&#39;Welcome to the RootMe python crackme&#39;)
10          22 LOAD_NAME                2 (input)
25 LOAD_CONST               2 (&#39;Enter the Flag: &#39;)
31 STORE_NAME               3 (PASS)
14          34 LOAD_CONST               3 (&#39;I know, you love decrypting Byte Code !&#39;)
37 STORE_NAME               4 (KEY)
16          40 LOAD_CONST               4 (5)
43 STORE_NAME               5 (I)
17          46 LOAD_CONST               5 (57)
49 LOAD_CONST               6 (73)
52 LOAD_CONST               7 (79)
55 LOAD_CONST               8 (16)
58 LOAD_CONST               9 (18)
61 LOAD_CONST              10 (26)
64 LOAD_CONST              11 (74)
67 LOAD_CONST              12 (50)
70 LOAD_CONST              13 (13)
73 LOAD_CONST              14 (38)
76 LOAD_CONST              13 (13)
79 LOAD_CONST               7 (79)
82 LOAD_CONST              15 (86)
85 LOAD_CONST              15 (86)
88 LOAD_CONST              16 (87)
91 BUILD_LIST              15
94 STORE_NAME               6 (SOLUCE)
19          97 BUILD_LIST               0
100 STORE_NAME               7 (KEYOUT)
22         103 SETUP_LOOP              75 (to 181)
106 LOAD_NAME                3 (PASS)
109 GET_ITER
&gt;&gt;  110 FOR_ITER                67 (to 180)
113 STORE_NAME               8 (X)
23         116 LOAD_NAME                7 (KEYOUT)
119 LOAD_ATTR                9 (append)
122 LOAD_NAME               10 (ord)
125 LOAD_NAME                8 (X)
128 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
131 LOAD_NAME                5 (I)
134 BINARY_ADD
135 LOAD_NAME               10 (ord)
138 LOAD_NAME                4 (KEY)
141 LOAD_NAME                5 (I)
144 BINARY_SUBSCR
145 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
148 BINARY_XOR
149 LOAD_CONST              17 (255)
152 BINARY_MODULO
153 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
156 POP_TOP
24         157 LOAD_NAME                5 (I)
160 LOAD_CONST              18 (1)
163 BINARY_ADD
164 LOAD_NAME               11 (len)
167 LOAD_NAME                4 (KEY)
170 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
173 BINARY_MODULO
174 STORE_NAME               5 (I)
177 JUMP_ABSOLUTE          110
&gt;&gt;  180 POP_BLOCK
30     &gt;&gt;  181 LOAD_NAME                6 (SOLUCE)
184 LOAD_NAME                7 (KEYOUT)
187 COMPARE_OP               2 (==)
190 POP_JUMP_IF_FALSE      206
31         193 LOAD_NAME                1 (print)
196 LOAD_CONST              19 (&#39;You Win&#39;)
199 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
202 POP_TOP
203 JUMP_ABSOLUTE          219
33     &gt;&gt;  206 LOAD_NAME                1 (print)
209 LOAD_CONST              20 (&#39;Try Again !&#39;)
212 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
215 POP_TOP
216 JUMP_FORWARD             0 (to 219)
&gt;&gt;  219 LOAD_CONST              21 (None)
222 RETURN_VALUE
On constate que le bytecode Python produit par l’interpréteur de référence CPython est :
un bytecode pour stack machine
Si l’on ajoute à cela le fait que le script à l’origine de ce bytecode est court,
alors il est simple de faire une décompilation à la main, à partir du bytecode,
qui nous permet de retrouver le script suivant :
crackme.py1
print(&#39;Welcome to the RootMe python crackme&#39;)
PASS = input(&#39;Enter the Flag: &#39;)
KEY = &#39;I know, you love decrypting Byte Code !&#39;
SOLUCE = [57, 73, 79, 16, 18, 26, 74, 50, 13, 38, 13, 79, 86, 86, 87]
KEYOUT = []
for X in PASS:
KEYOUT.append(ord(X) + I ^ ord(KEY[I]) % 255)
I = I + 1 % len(KEY)
if SOLUCE == KEYOUT:
print(&#39;You Win&#39;)
print(&#39;Try Again !&#39;)
Maintenant que l’on connaît le résultat à obtenir et comment la chaîne en entrée
est transformée, il nous suffit d’appliquer la transformation inverse (et ici la
transformation est simple à inverser) sur le résultat attendu (SOLUCE) afin
d’obtenir ce que nous cherchons(PASS). Pour ce faire, j’ai fait un petit
script Ruby :
breakit.rb1
KEY = &#39;I know, you love decrypting Byte Code !&#39;.each_char.map(&amp;:ord).to_a
psw = SOLUCE.zip(KEY[I..-1]).each_with_index.map { |(x, k), i| (x^k) - (i+I) }
puts(psw.map(&amp;:chr).join)
Et il nous donne :
% ./breakit.rb
I_hate_RUBY_!!!
Si j’avais su ^^
Bon allez, testons cela :
% ./crackme.py
Welcome to the RootMe python crackme
Enter the Flag: I_hate_RUBY_!!!
Voilà, ce crackme un peu original est terminé. Il était relativement simple,
mais il aura fallu quelques recherches pour en venir à bout. En tout cas, ce fut
Crackme — Anti-Debug
Quatrième crackme, et cette fois nous allons rencontrer notre premier
mécanisme anti-débogueur.
Nous avons donc à faire à un exécutable statique avec un strings de 2069
lignes qui ne contient pas de mot de passe (cette fois j’ai vérifié ^^‘)
Ok, sortons le débogueur (comme pour le crackme 2, sauf que cette fois c’est
justifié :-P). Mais avant cela, voyons d’abord une exécution normale :
##        Bienvennue dans ce challenge de cracking        ##
Password : AAAAAAAA
Maintenant, avec notre ami GDB :
Starting program: /home/slaperche/Téléchargements/crackme/ch3.bin
[Inferior 1 (process 5244) exited with code 01]
Hoho, voyez-vous ça ? Monsieur n’aime pas être surveillé. Bon, on va y aller un
peu plus doucement cette fois :
Breakpoint 1 at 0x80483fe
Breakpoint 1, 0x080483fe in main ()
0x080483f0 &lt;+0&gt;:     lea    0x4(%esp),%ecx
0x080483f4 &lt;+4&gt;:     and    $0xfffffff0,%esp
0x080483f7 &lt;+7&gt;:     pushl  -0x4(%ecx)
0x080483fa &lt;+10&gt;:    push   %ebp
0x080483fb &lt;+11&gt;:    mov    %esp,%ebp
0x080483fd &lt;+13&gt;:    push   %ecx
=&gt; 0x080483fe &lt;+14&gt;:    sub    $0x14,%esp
0x08048401 &lt;+17&gt;:    movl   $0x80c2888,-0xc(%ebp)
0x08048408 &lt;+24&gt;:    push   $0x0
0x0804840a &lt;+26&gt;:    push   $0x1
0x0804840c &lt;+28&gt;:    push   $0x0
0x0804840e &lt;+30&gt;:    push   $0x0
0x08048410 &lt;+32&gt;:    call   0x8058a70 &lt;ptrace&gt;
0x08048415 &lt;+37&gt;:    add    $0x10,%esp
0x08048418 &lt;+40&gt;:    test   %eax,%eax
0x0804841a &lt;+42&gt;:    jns    0x8048436 &lt;main+70&gt;
0x0804841c &lt;+44&gt;:    sub    $0xc,%esp
0x0804841f &lt;+47&gt;:    push   $0x80c2894
0x08048424 &lt;+52&gt;:    call   0x80492d0 &lt;puts&gt;
0x08048429 &lt;+57&gt;:    add    $0x10,%esp
0x0804842c &lt;+60&gt;:    mov    $0x1,%eax
0x08048431 &lt;+65&gt;:    jmp    0x80484f9 &lt;main+265&gt;
0x08048436 &lt;+70&gt;:    sub    $0xc,%esp
0x08048439 &lt;+73&gt;:    push   $0x80c28b0
0x0804843e &lt;+78&gt;:    call   0x80492d0 &lt;puts&gt;
0x08048443 &lt;+83&gt;:    add    $0x10,%esp
0x08048446 &lt;+86&gt;:    sub    $0xc,%esp
0x08048449 &lt;+89&gt;:    push   $0x80c28f0
0x0804844e &lt;+94&gt;:    call   0x80492d0 &lt;puts&gt;
0x08048453 &lt;+99&gt;:    add    $0x10,%esp
0x08048456 &lt;+102&gt;:   sub    $0xc,%esp
0x08048459 &lt;+105&gt;:   push   $0x80c2930
0x0804845e &lt;+110&gt;:   call   0x80492d0 &lt;puts&gt;
0x08048463 &lt;+115&gt;:   add    $0x10,%esp
0x08048466 &lt;+118&gt;:   mov    $0x80c296e,%eax
0x0804846b &lt;+123&gt;:   sub    $0xc,%esp
0x0804846e &lt;+126&gt;:   push   %eax
0x0804846f &lt;+127&gt;:   call   0x8048f60 &lt;printf&gt;
0x08048474 &lt;+132&gt;:   add    $0x10,%esp
0x08048477 &lt;+135&gt;:   mov    0x80e549c,%eax
0x0804847c &lt;+140&gt;:   sub    $0x4,%esp
0x0804847f &lt;+143&gt;:   push   %eax
0x08048480 &lt;+144&gt;:   push   $0x9
0x08048482 &lt;+146&gt;:   lea    -0x16(%ebp),%eax
0x08048485 &lt;+149&gt;:   push   %eax
0x08048486 &lt;+150&gt;:   call   0x8048f90 &lt;fgets&gt;
0x0804848b &lt;+155&gt;:   add    $0x10,%esp
0x0804848e &lt;+158&gt;:   lea    0x8048497,%eax
0x08048494 &lt;+164&gt;:   inc    %eax
0x08048495 &lt;+165&gt;:   jmp    *%eax
0x08048497 &lt;+167&gt;:   mov    $0x8bea558a,%eax
0x0804849c &lt;+172&gt;:   inc    %ebp
0x0804849d &lt;+173&gt;:   hlt
0x0804849e &lt;+174&gt;:   add    $0x4,%eax
0x080484a1 &lt;+177&gt;:   mov    (%eax),%al
0x080484a3 &lt;+179&gt;:   cmp    %al,%dl
0x080484a5 &lt;+181&gt;:   jne    0x80484e4 &lt;main+244&gt;
0x080484a7 &lt;+183&gt;:   mov    -0x15(%ebp),%dl
0x080484aa &lt;+186&gt;:   mov    -0xc(%ebp),%eax
0x080484ad &lt;+189&gt;:   add    $0x5,%eax
0x080484b0 &lt;+192&gt;:   mov    (%eax),%al
0x080484b2 &lt;+194&gt;:   cmp    %al,%dl
0x080484b4 &lt;+196&gt;:   jne    0x80484e4 &lt;main+244&gt;
0x080484b6 &lt;+198&gt;:   mov    -0x14(%ebp),%dl
0x080484b9 &lt;+201&gt;:   mov    -0xc(%ebp),%eax
0x080484bc &lt;+204&gt;:   inc    %eax
0x080484bd &lt;+205&gt;:   mov    (%eax),%al
0x080484bf &lt;+207&gt;:   cmp    %al,%dl
0x080484c1 &lt;+209&gt;:   jne    0x80484e4 &lt;main+244&gt;
0x080484c3 &lt;+211&gt;:   mov    -0x13(%ebp),%dl
0x080484c6 &lt;+214&gt;:   mov    -0xc(%ebp),%eax
0x080484c9 &lt;+217&gt;:   add    $0xa,%eax
0x080484cc &lt;+220&gt;:   mov    (%eax),%al
0x080484ce &lt;+222&gt;:   cmp    %al,%dl
0x080484d0 &lt;+224&gt;:   jne    0x80484e4 &lt;main+244&gt;
0x080484d2 &lt;+226&gt;:   sub    $0xc,%esp
0x080484d5 &lt;+229&gt;:   push   $0x80c297a
0x080484da &lt;+234&gt;:   call   0x80492d0 &lt;puts&gt;
0x080484df &lt;+239&gt;:   add    $0x10,%esp
0x080484e2 &lt;+242&gt;:   jmp    0x80484f4 &lt;main+260&gt;
0x080484e4 &lt;+244&gt;:   sub    $0xc,%esp
0x080484e7 &lt;+247&gt;:   push   $0x80c298e
0x080484ec &lt;+252&gt;:   call   0x80492d0 &lt;puts&gt;
0x080484f1 &lt;+257&gt;:   add    $0x10,%esp
0x080484f4 &lt;+260&gt;:   mov    $0x0,%eax
0x080484f9 &lt;+265&gt;:   mov    -0x4(%ebp),%ecx
0x080484fc &lt;+268&gt;:   leave
0x080484fd &lt;+269&gt;:   lea    -0x4(%ecx),%esp
0x08048500 &lt;+272&gt;:   ret
Tiens, tiens, mais que vois-je ? Un appel à
ptrace à l’adresse 0x08048410. Il faut
savoir que, sous Linux du moins, ptrace est l’appel système à la base des
débogueurs. Il permet de mettre en pause un programme, de faire du pas à pas, de
lire et écrire sa mémoire et ses registres. Il permet également de détecter si
un programme est sous la surveillance de ptrace (et donc, potentiellement
sour la surveillance d’un débogueur). Et c’est ce qui est fait ici. Le code :
correspond à :
Qui permet de savoir si le processus courant est sous surveillance.
Maintenant que l’on sait comment le binaire se défend, nous allons pouvoir
contourner cette protection. Pour cela, nous pouvons placer un point d’arrêt sur
l’instruction qui teste la valeur de retour de ptrace et la changer (sachant
que l’appel renvoi -1 si le processus est tracé et 0 si ce n’est pas le cas)
avant de continuer l’exécution.
Breakpoint 2 at 0x8048418
Breakpoint 2, 0x08048418 in main ()
Avant de continuer, regardons le code de plus près. Bon déjà, il n’y a pas
d’appel à strcmp. Cependant, en examinant le code avec attention on remarque
cette partie :
Tiens tiens, on dirait bien qu’il y a un pattern ici :
on charge un octet dans dl
on charge une valeur dans eax
on fait une opération arithmétique sur eax (add ou inc)
on ramène eax sur un octet.
on compare al avec dl
on saute à l’adresse 0x80484e4 si la comparaison précédente génère un résultat
différent de 0 (ce qui signifie que les deux opérandes étaient différents).
Cela ressemble étrangement à une boucle (boucle déroulée certes, mais boucle
quand même) pour comparer deux chaînes de caractères. Et comme ce pattern se
répète quatre fois, on pourrait émettre l’hypothèse que le mot de passe
recherché a une longueur de 4 caractères.
Vérifions nos hypothèses en examinant les valeurs de al et dl à chaque
cmp. Commençons par poser un point d’arrêt sur le premier cmp.
(gdb) b *0x080484a3
Breakpoint 3 at 0x80484a3
Breakpoint 3, 0x080484a3 in main ()
(gdb) p (char)$al
$1 = 101 &#39;e&#39;
(gdb) p (char)$dl
$2 = 65 &#39;A&#39;
Ok, dl semble contenir notre chaîne (on reconnaît notre A), c’est donc al
qui va contenir les caractères du mot de passe à trouver (le premier caractère
du mot de passe est donc e).
Bien, maintenant que l’on sait quel registre surveiller, continuons avec les
autres cmp. Mais avant cela, il va falloir bidouiller un peu. Et oui, le
cmp ne va pas produire le résultat attendu et la prochaine instruction va
nous faire sauter (ce qui va nous empêcher d’analyser les autres cmp). La
prochaine instruction est un jne ce qui signifie Jump if Not Equal, qui
est le parfait équivalent de l’instruction jnz (qui signifie Jump if Not
Zero). Or le second nom (jnz) nous met la puce à l’oreille : cette
instruction se base sur la valeur du flag z. Pour éviter le saut, il va donc
falloir mettre ce flag à 1 après l’instruction cmp.
Allons-y :
$3 = [ CF AF SF IF ]
(gdb) set $eflags = $eflags | 64
$4 = [ CF AF ZF SF IF ]
Et voilà, le flag z est positionné (il y a peut-être une syntaxe plus simple
pour faire ça, mais je n’ai pas trouvé). D’où sort le 64 ? Et bien c’est tout
simple : d’après
cette page, le
flag z correspond au bit 6, et comme chacun sait 26 = 64. S’en suit
un petit OU bit à bit pour positionner le flag voulu. Et voilà le travail.
Allez, continuons et découvrons ce fichu mot de passe :
(gdb) b *0x080484b2
Breakpoint 4 at 0x80484b2
Breakpoint 4, 0x080484b2 in main ()
$5 = 97 &#39;a&#39;
(gdb) b *0x080484bf
Breakpoint 5 at 0x80484bf
Breakpoint 5, 0x080484bf in main ()
$6 = 115 &#39;s&#39;
0x080484c1 in main ()
(gdb) b *0x080484ce
Breakpoint 6 at 0x80484ce
Breakpoint 6, 0x080484ce in main ()
$7 = 121 &#39;y&#39;
Ok, le mot de passe serait donc easy.
Voyons voir :
% ./ch3.bin
Ça commence à devenir intéressant :-)
Crackme — ltrace
Avec ce troisième crackme, on reste dans du très simple mais ça va nous
permettre de voir une nouvelle approche lorsque l’on veut faire de la
rétroingénierie sur des exécutables dynamiques.
Bon, déjà voyons à quoi nous avons à faire.
% ./crackme
(*) -Syntaxe: ./crackme03 [password]
Ok, celui-ci prend le mot de passe via les arguments de la ligne de commande.
Commençons par le traditionnel strings qui, comme on pouvait s’y attendre,
ne nous apprend pas grand-chose.
% strings crackme
QVhT
_0cG
_.5T
(*) -Syntaxe: %s [password]
_0cGj35m9V5T3
9H95h3xdh
rification de votre mot de passe..
(!) L&#39;authentification a
&#39;+) Authentification r
ussie...
U&#39;r root!
sh 3.0 # password: %s
le voie de la sagesse te guidera, tache de trouver le mot de passe petit padawaan
Il y a bien quelques chaînes qui ressemblent vaguement à un mot de passe, mais
% ./crackme _0cGj35m9V5T3
Vérification de votre mot de passe..
% ./crackme 9H95h3xdh
Voyons à quel type de binaire nous avons là :
crackme03: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.8, not stripped
Hum, intéressant : cet exécutable est lié dynamiquement. Voyons donc ce que nous
donne un petit ltrace :
% ltrace ./crackme AAAAAAAA
__libc_start_main(0x8048554, 2, 0xbfa5d744, 0x8048840, 0x8048830 &lt;unfinished ...&gt;
malloc(29)                                                                                                     = 0x09a6d008
memcpy(0x09a6d008, &quot;_0cGj35m9V5T3\303\2078CJ0\303\2009H95h3xdh&quot;, 31)                                           = 0x09a6d008
memcpy(0xbfa5d60a, &quot;_Celebration&quot;, 13)                                                                         = 0xbfa5d60a
strcpy(0xbfa5d66e, &quot;AAAAAAAA&quot;)                                                                                 = 0xbfa5d66e
puts(&quot;V\303\251rification de votre mot de pa&quot;...Vérification de votre mot de passe..
)                                                              = 38
strcmp(&quot;AAAAAAAA&quot;, &quot;_0cGjc5m_.5\r\n\303\2078CJ0\303\2009&quot;)                                                     = -1
printf(&quot;le voie de la sagesse te guidera&quot;...le voie de la sagesse te guidera, tache de trouver le mot de passe petit padawaan
)                                                                  = 84
exit(0  &lt;unfinished ...&gt;
Hum, c’est très instructif. On voit que le code fait un strcmp contre une chaîne
un peu bizarre. Essayons d’utiliser cette chaîne bizarre en tant que mot de
passe. Étant donné que la chaîne contient la séquence de caractères de contrôles
\r\n, je préfère utiliser Ruby pour passer la chaîne en argument (d’autres
solutions étaient bien sûr possible) plutôt que d’essayer de la taper
% ./crackme &quot;$(ruby -e &#39;print(&quot;_0cGjc5m_.5\r\n\303\2078CJ0\303\2009&quot;)&#39;)&quot;
&#39;+) Authentification réussie...
sh 3.0 # password: liberté!
Le mot de passe est donc liberté!.
Crackme — GDB
Ce second crackme est un peu moins simple que le précédent, mais il est tout
de même trivial. En revanche, je me suis compliqué la vie : la solution était en
fait bien plus simple que ce que l’on peut penser. Mais au moins, ça me permet
de présenter l’usage de GDB ^^
Comme la dernière fois, on va commencer par lancer strings sur notre
binaire. Cependant, cette fois le nombre de chaîne de caractères est beaucoup
plus important (1506 chaînes). Ayant la flemme de regarder cela en détail
(pourtant j’aurai dû, comme vous le verrez par la suite), je décide de passer à
Déjà, on peut remarquer (via la commande file
ou ldd) que l’exécutable est compilé en mode statique (ce
qui explique le grand nombre de chaîne de caractères), ça signifie que l’on peut
oublier d’office les bidouilles à base de ltrace et autres LD_PRELOAD
(ne vous inquiétez pas, on aura l’occasion de s’amuser avec ça dans d’autres
défis).
Partant de ces premières observations, je décide de partir sur
GDB. Mais avant cela,
voyons une exécution normale :
% ./ch2.bin
Ok, il faut donc fournir un nom d’utilisateur avant de pouvoir fournir le mot de
Maintenant, passons à GDB. Première chose à faire : placer un point d’arrêt sur
la fonction main, puis regarder le code assembleur.
Breakpoint 1 at 0x8048317
Starting program: ch2.bin
Breakpoint 1, 0x08048317 in main ()
0x08048309 &lt;+0&gt;:     lea    0x4(%esp),%ecx
0x0804830d &lt;+4&gt;:     and    $0xfffffff0,%esp
0x08048310 &lt;+7&gt;:     pushl  -0x4(%ecx)
0x08048313 &lt;+10&gt;:    push   %ebp
0x08048314 &lt;+11&gt;:    mov    %esp,%ebp
0x08048316 &lt;+13&gt;:    push   %ecx
=&gt; 0x08048317 &lt;+14&gt;:    sub    $0x24,%esp
0x0804831a &lt;+17&gt;:    movl   $0x80a6b19,-0xc(%ebp)
0x08048321 &lt;+24&gt;:    movl   $0x80a6b1e,-0x10(%ebp)
0x08048328 &lt;+31&gt;:    movl   $0x80a6b2c,(%esp)
0x0804832f &lt;+38&gt;:    call   0x8048de0 &lt;puts&gt;
0x08048334 &lt;+43&gt;:    movl   $0x80a6b6c,(%esp)
0x0804833b &lt;+50&gt;:    call   0x8048de0 &lt;puts&gt;
0x08048340 &lt;+55&gt;:    movl   $0x80a6bac,(%esp)
0x08048347 &lt;+62&gt;:    call   0x8048de0 &lt;puts&gt;
0x0804834c &lt;+67&gt;:    movl   $0x80a6bea,(%esp)
0x08048353 &lt;+74&gt;:    call   0x8048db0 &lt;printf&gt;
0x08048358 &lt;+79&gt;:    mov    -0x8(%ebp),%eax
0x0804835b &lt;+82&gt;:    mov    %eax,(%esp)
0x0804835e &lt;+85&gt;:    call   0x804826a &lt;getString&gt;
0x08048363 &lt;+90&gt;:    mov    %eax,-0x8(%ebp)
0x08048366 &lt;+93&gt;:    mov    -0xc(%ebp),%eax
0x08048369 &lt;+96&gt;:    mov    %eax,0x4(%esp)
0x0804836d &lt;+100&gt;:   mov    -0x8(%ebp),%eax
0x08048370 &lt;+103&gt;:   mov    %eax,(%esp)
0x08048373 &lt;+106&gt;:   call   0x80502f0 &lt;strcmp&gt;
0x08048378 &lt;+111&gt;:   test   %eax,%eax
0x0804837a &lt;+113&gt;:   jne    0x80483d0 &lt;main+199&gt;
0x0804837c &lt;+115&gt;:   movl   $0x80a6bf5,(%esp)
0x08048383 &lt;+122&gt;:   call   0x8048db0 &lt;printf&gt;
0x08048388 &lt;+127&gt;:   mov    -0x8(%ebp),%eax
0x0804838b &lt;+130&gt;:   mov    %eax,(%esp)
0x0804838e &lt;+133&gt;:   call   0x804826a &lt;getString&gt;
0x08048393 &lt;+138&gt;:   mov    %eax,-0x8(%ebp)
0x08048396 &lt;+141&gt;:   mov    -0x10(%ebp),%eax
0x08048399 &lt;+144&gt;:   mov    %eax,0x4(%esp)
0x0804839d &lt;+148&gt;:   mov    -0x8(%ebp),%eax
0x080483a0 &lt;+151&gt;:   mov    %eax,(%esp)
0x080483a3 &lt;+154&gt;:   call   0x80502f0 &lt;strcmp&gt;
0x080483a8 &lt;+159&gt;:   test   %eax,%eax
0x080483aa &lt;+161&gt;:   jne    0x80483c2 &lt;main+185&gt;
0x080483ac &lt;+163&gt;:   movl   $0x80a6c00,0x4(%esp)
0x080483b4 &lt;+171&gt;:   movl   $0x80a6c0c,(%esp)
0x080483bb &lt;+178&gt;:   call   0x8048db0 &lt;printf&gt;
0x080483c0 &lt;+183&gt;:   jmp    0x80483dc &lt;main+211&gt;
0x080483c2 &lt;+185&gt;:   movl   $0x80a6c52,(%esp)
0x080483c9 &lt;+192&gt;:   call   0x8048de0 &lt;puts&gt;
0x080483ce &lt;+197&gt;:   jmp    0x80483dc &lt;main+211&gt;
0x080483d0 &lt;+199&gt;:   movl   $0x80a6c5f,(%esp)
0x080483d7 &lt;+206&gt;:   call   0x8048de0 &lt;puts&gt;
0x080483dc &lt;+211&gt;:   mov    $0x0,%eax
0x080483e1 &lt;+216&gt;:   add    $0x24,%esp
0x080483e4 &lt;+219&gt;:   pop    %ecx
0x080483e5 &lt;+220&gt;:   pop    %ebp
0x080483e6 &lt;+221&gt;:   lea    -0x4(%ecx),%esp
0x080483e9 &lt;+224&gt;:   ret
Information intéressante : il y a deux appels à strcmp. Ça pourrait bien
correspondre aux vérifications du nom d’utilisateur et du mot de passe. On va
donc placer un point d’arrêt sur strcmp afin de pouvoir examiner son code.
(gdb) b *0x80502f0
Breakpoint 2 at 0x80502f0
username: AAAAAAAA
Breakpoint 2, 0x080502f0 in strcmp ()
Dump of assembler code for function strcmp:
=&gt; 0x080502f0 &lt;+0&gt;:     push   %ebp
0x080502f1 &lt;+1&gt;:     xor    %edx,%edx
0x080502f3 &lt;+3&gt;:     mov    %esp,%ebp
0x080502f5 &lt;+5&gt;:     push   %esi
0x080502f6 &lt;+6&gt;:     push   %ebx
0x080502f7 &lt;+7&gt;:     mov    0x8(%ebp),%esi
0x080502fa &lt;+10&gt;:    mov    0xc(%ebp),%ebx
0x080502fd &lt;+13&gt;:    lea    0x0(%esi),%esi
0x08050300 &lt;+16&gt;:    movzbl (%esi,%edx,1),%eax
0x08050304 &lt;+20&gt;:    movzbl (%ebx,%edx,1),%ecx
0x08050308 &lt;+24&gt;:    test   %al,%al
0x0805030a &lt;+26&gt;:    je     0x8050328 &lt;strcmp+56&gt;
0x0805030c &lt;+28&gt;:    add    $0x1,%edx
0x0805030f &lt;+31&gt;:    cmp    %cl,%al
0x08050311 &lt;+33&gt;:    je     0x8050300 &lt;strcmp+16&gt;
0x08050313 &lt;+35&gt;:    movzbl %al,%edx
0x08050316 &lt;+38&gt;:    movzbl %cl,%eax
0x08050319 &lt;+41&gt;:    sub    %eax,%edx
0x0805031b &lt;+43&gt;:    mov    %edx,%eax
0x0805031d &lt;+45&gt;:    pop    %ebx
0x0805031e &lt;+46&gt;:    pop    %esi
0x0805031f &lt;+47&gt;:    pop    %ebp
0x08050320 &lt;+48&gt;:    ret
0x08050321 &lt;+49&gt;:    lea    0x0(%esi,%eiz,1),%esi
0x08050328 &lt;+56&gt;:    movzbl %cl,%edx
0x0805032b &lt;+59&gt;:    neg    %edx
0x0805032d &lt;+61&gt;:    mov    %edx,%eax
0x0805032f &lt;+63&gt;:    pop    %ebx
0x08050330 &lt;+64&gt;:    pop    %esi
0x08050331 &lt;+65&gt;:    pop    %ebp
0x08050332 &lt;+66&gt;:    ret
On prend soin d’utiliser une chaîne très facilement reconnaissable en tant que
nom d’utilisateur, ça peut être utile par la suite.
En analysant le code on voit que la comparaison (instruction cmp à l’adresse
0x0805030f) se fait sur le contenu des registres cl et al. Ces registres
correspondent en fait aux registres 32-bit ecx et eax traités comme des
registres 8-bit. Or, on voit que les valeurs de ces registres sont chargées à
partir des adresses contenues dans esi (pour eax) et ebx (pour
ecx) via des instructions movzbl. Il suffit donc de mettre un point
d’arrêt après le chargement des adresses dans esi et ebx afin de pouvoir
afficher les chaînes de caractères :
(gdb) b *0x08050300
Breakpoint 3 at 0x8050300
Breakpoint 3, 0x08050300 in strcmp ()
(gdb) p (char*)$esi
$1 = 0x80c8688 &quot;AAAAAAAA&quot;
(gdb) p (char*)$ebx
$2 = 0x80a6b19 &quot;john&quot;
On reconnaît notre chaîne AAAAAAAA dans esi, c’est donc ebx qui contient
le nom d’utilisateur. On découvre alors que le nom d’utilisateur est john.
Comme nous avons donné un mauvais mot de passe, le programme va se terminer
avant que nous puissions découvrir le mot de passe. Pour éviter cela, nous
allons modifier la valeur de retour de strcmp. On commence par ajouter un
point d’arrêt juste après le premier appel de strcmp dans main, puis on
placera 0 (valeur de retour de strcmp lorsque les deux chaînes sont
identiques) dans le registre eax (registre utilisé pour stocker la valeur de
retour, ce comportement est défini par la
convention d’appel utilisée)
(gdb) b *0x08048378
Breakpoint 4 at 0x8048378
Breakpoint 4, 0x08048378 in main ()
password: BBBBBBBB
Comme prévu, il nous demande le mot de passe et l’on se retrouve à nouveau dans
strcmp. En appliquant le même principe que précédemment on obtient le mot de
$3 = 0x80a6b1e &quot;the ripper&quot;
Étant maintenant en possession des identifiants, on peut récupérer le code
secret :
password: the ripper
Bien joue, vous pouvez valider l&#39;epreuve avec le mot de passe : 987654321 !
Gagné ! Le mot de passe était donc 987654321.
Pour information, tout cela était inutile. En effet, la sortie de strings
nous donnait déjà toutes les informations nécessaires (pour peu que l’on prenne
le temps de la regarder de plus près) :
% strings ch2.bin
On voit donc que le nom d’utilisateur et le mot de passe était en clair dans le
binaire. Pis encore : le code à obtenir (987654321) est lui aussi en clair dans
le binaire. On n’avait même pas besoin de connaître le nom d’utilisateur et le
mot de passe pour y accéder.
Crackme — strings
Ce premier crackme va nous permettre de voir la première chose à faire en
rétroingénierie : lorsque l’on cherche une information textuelle dans un
binaire, il faut commencer par lancer un coup de
Ici, on obtient :
% strings ch1.bin
%s : &quot;%s&quot;
Bien joue, vous pouvez valider l’epreuve avec le pass : %s!
Dommage, essaye encore une fois.
La chaîne 123456789 ressemble étrangement à un mot de passe.
Essayons :
% ./ch1.bin
Veuillez entrer le mot de passe : 123456789
Bien joue, vous pouvez valider l’epreuve avec le pass : 123456789!
Celui-ci était vraiment simple, le mot de passe était stocké en clair dans le
binaire. Les prochains ne seront pas aussi évident ;-)
Copyright © 2016 - Sylvain Laperche -

References: §6
 §6
 §6
 §6
 §6
 § 13
 § 3
 § 7
 § 8