Voyage en C++ie : les objets

Magazine
Marque
MISC
Numéro
96
Mois de parution
mars 2018
Spécialité(s)


Résumé

Cet article est le second d'une mini-série sur le C++, ou plutôt sur les binaires compilés depuis C++, leurs particularités, comment les concepts du langage se retrouvent parfois dans le binaire final.


Body

 

Comprendre le lien entre du code C++ et sa version compilée fournit parfois une aide précieuse à l’analyste. Dans cette optique, cette mini-série explore différentes facettes d’un binaire et leur lien avec le source d’origine quand ce dernier est du C++.

Pour cette fois, nous allons nous intéresser aux objets, à l'héritage, aux fonctions virtuelles et à comment ces concepts se retrouvent dans le fichier ELF final — on restera dans le monde Linux. Le compilateur utilisé est clang++ 5.0, et c’est le standard C++11 qui est utilisé. Et enfin, on cible du x86 64 bits.

1. Un objet, c’est bien peu de choses

Les concepts de classe, d'héritage, de fonctions membres virtuelles n'existent pas au niveau assembleur. Ils ne sont même pas présents explicitement au niveau LLVM, c'est dire. Parfois, ils ne changent rien, comme la l’attribut de fonction membre override, ou les visibilités public, private, etc. Parfois, ils laissent une trace dans le binaire, et c'est ça qui va nous intéresser.

Commençons simplement, avec un constructeur par défaut :

struct S {
    int field;
} s;

Si je compile ce code avec GCC, comme j'ai déclaré une variable globale, j'obtiens :

s:
    .zero    4
    .size    s, 4

Si je remplace le constructeur par une factory pour appeler le constructeur dynamiquement :

struct S {
    int field;
};
 
S make_s() {
    return {};
}

On obtient (en compilant avec clang++ -O2 a.cc -S -o - -std=c++11 -masm=intel | grep -E -v '^\s*[\.#]' | sed 's/ *#.*//' un code qui peut paraître troublant :

_Z6make_sv:
    xor eax, eax
    ret

OK pour le xor qui donne une valeur par défaut au champ field, mais on remarque que la valeur de retour se confond avec l'unique membre de S. Cela correspond au fait que la notion de type a disparu, notre objet n'est plus qu'une séquence de champs qui tient là dans un registre. Si on enrichit un peu notre objet :

#include <tuple>
 
std::tuple<int, double> make_tuple() {
    return {};
}

On obtient :

_Z10make_tuplev:
    xorps    xmm0, xmm0
    xor    eax, eax
    ret

On notera que même si on s’attend, pour notre architecture, à ce que le tuple fasse 4 + 8 = 12 octets, un sizeof nous confirme qu’elle en fait 16, il y a eu du padding.

Et si notre objet grossit un peu, on ne se contente plus de passer une valeur de retour, mais on modifie un pointeur vers l'objet non initialisé, passé en paramètre :

#include <tuple>
 
std::tuple<int, double[2]> make_tuple() {
    return {};
}

Se compile en :

_Z10make_tuplev:
    xorps    xmm0, xmm0
    movups    xmmword ptr [rdi], xmm0
    mov    dword ptr [rdi + 16], 0
    mov    rax, rdi
    ret

On l'a compris, une classe, ce n'est jamais rien d'autre qu'un paquet d'octets rangés les uns à côté des autres. On notera au passage que l'utilisation d'un std::tuple<...> est correctement optimisée par le compilateur.

2. L'héritage Mitsouko

Si un objet est équivalent à la suite de ses variables membres, qu'en est-il d'un objet dont la classe est définie par héritage ? On a un premier élément de réponse avec le code de la section précédente, puisque dans la libstdc++, les tuples sont définis par héritage : quand on hérite d'une classe, on ajoute (récursivement) les champs de la classe héritée aux nôtres, comme l'illustre ce petit exemple :

struct P {
  double p ;
};
struct Q {
  int p  ;
};
struct R {
  double p ;
};
 
struct S : P, Q, R {
};
 
S make_s() {
  return {};
}

Qui une fois compilé, donne :

_Z6make_sv:
    xorps    xmm0, xmm0
    movups    xmmword ptr [rdi], xmm0
    mov    qword ptr [rdi + 16], 0
    mov    rax, rdi
    ret

Toutes les règles d'héritage virtuel ne changent rien à cette organisation de la mémoire. Du moins tant qu'il n'y a pas de fonctions virtuelles, mais on verra ça un peu plus loin.

3. Les fonctions membres

Les familiers de Python seront certainement ravis d'observer le résultat de la compilation de la fonction suivante :

class S {
  int field;
  public:
  __attribute__((noinline)) int get() const  { return field; }
};
 
int foo(S s) {
   return s.get();
}

Et là, stupeur et tremblements :

_Z3foo1S:
    push    rax
    mov    dword ptr [rsp], edi
    mov    rdi, rsp
    call    _ZNK1S3getEv
    pop    rcx
    ret
 
_ZNK1S3getEv:
    mov    eax, dword ptr [rdi]
    ret

La fonction S::get() const prend en premier paramètre, à travers rdi, la valeur de this.

4. Les fonctions virtuelles

Ahhh les fonctions virtuelles. Elles ont fait la gloire de Java et permettent de briller en société en parlant de vtable. Le principe est simple. Si on a un appel de méthode virtuelle, la résolution de méthode se fait à l'exécution et non à la compilation :

struct S {
  virtual int get() const;
};
 
int foo(S& s) {
   return s.get();
}

Le code compilé est assez curieux au premier abord :

_Z3fooR1S:
    mov    rax, qword ptr [rdi]
    jmp    qword ptr [rax]

Le mov déréférence le pointeur passé en paramètre (oui, une référence n'est jamais qu'un pointeur forcément initialisé) et la valeur obtenue est utilisée pour faire un appel de fonction indirect à travers un jmp. Ce premier déréférencement correspond à l'accès à la vtable, comme on peut le voir si on crée une instance de classe dérivant de S :

struct S {
  virtual int get() const;
};
 
struct P : S {
  virtual int get() const { return 1; }
};
 
 
P make_p() {
   return {};
}

Le code compilé pour la fonction make_p n'est pas aussi vide qu'on l'y croirait :

_Z6make_pv:
    mov    qword ptr [rdi], _ZTV1P+16
    mov    rax, rdi
    ret
 
_ZNK1P3getEv:
    mov    eax, 1
    ret
[...]
_ZTV1P:
    .quad    0
    .quad    _ZTI1P
    .quad    _ZNK1P3getEv
    .size    _ZTV1P, 24
 
    .type    _ZTS1P,@object
    .section    .rodata._ZTS1P,"aG",@progbits,_ZTS1P,comdat
    .weak    _ZTS1P
_ZTS1P:
    .asciz    "1P"
    .size    _ZTS1P, 3
 
    .type    _ZTI1P,@object
    .section    .rodata._ZTI1P,"aG",@progbits,_ZTI1P,comdat
    .weak    _ZTI1P
_ZTI1P:
    .quad    _ZTVN10__cxxabiv120__si_class_type_infoE+16
    .quad    _ZTS1P
    .quad    _ZTI1S
    .size    _ZTI1P, 24

Alors il y a pas mal de choses à dire. Si on regarde _Z6make_pv, on voit que l'objet P n'est pas vide, mais contient un champ, qui est initialisé à _ZTV1P+16, soit _ZNK1P3getEv, l'adresse de la fonction à effectivement appeler dans le cas d'un appel virtuel.

On notera aussi que _ZTV1P+8 contient les fameuses RTTI les RunTime Type Informations. D'ailleurs, si on utilise le drapeau de compilation -fno-rtti, cette valeur est bien mise à 0.

On parle de virtual table parce qu'il y a une entrée pour chaque fonction virtuelle, comme dans le cas suivant :

struct S {
  virtual int get() const;
  virtual int& get();
};
 
struct P : S {
  int n;
  virtual int get() const { return n; }
  virtual int& get() { return n; }
};
 
 
P make_p() {
   return {};
}

Qui, une fois compilé et un peu nettoyé, donne :

_Z6make_pv:
    mov    qword ptr [rdi + 8], 0
    mov    qword ptr [rdi], _ZTV1P+16
    mov    rax, rdi
    ret
 
_ZNK1P3getEv:
    mov    eax, dword ptr [rdi + 8]
    ret
 
_ZN1P3getEv:
    lea    rax, [rdi + 8]
    ret
 
_ZTV1P:
    .quad    0
    .quad    0
    .quad    _ZNK1P3getEv
    .quad    _ZN1P3getEv
    .size    _ZTV1P, 32

On identifie rapidement dans _ZTV1P+16 les adresses des deux fonctions virtuelles de P. Voilà pour les fonctions virtuelles !

5. Fonctions virtuelles et héritage multiple

Si on combine ce qu'on a découvert sur les vtable et sur l'héritage, on peut facilement prédire ce qui se passera lors de la compilation de ce fragment de code :

struct P {
  virtual int get() const { return 1;}
};
struct S {
  int field;
  virtual int &get() { return field;}
};
struct F : P, S {
};
 
F make_f() {
  return {};
}

A priori, on devrait avoir un champ dans F, ainsi que l'initialisation de la vtable de P et celle de S. Vérifions :

_Z6make_fv:
    mov    qword ptr [rdi + 16], 0
    mov    eax, _ZTV1F+40
    movq    xmm0, rax
    mov    eax, _ZTV1F+16
    movq    xmm1, rax
    punpcklqdq    xmm1, xmm0
    movdqu    xmmword ptr [rdi], xmm1
    mov    rax, rdi
    ret
 
_ZNK1P3getEv:
    mov    eax, 1
    ret
 
_ZN1S3getEv:
    lea    rax, [rdi + 8]
    ret
 
_ZTV1F:
    .quad    0
    .quad    _ZTI1F
    .quad    _ZNK1P3getEv
    .quad    -8
    .quad    _ZTI1F
    .quad    _ZN1S3getEv
    .size    _ZTV1F, 48

Et voilà ! Une table virtuelle pour _ZTV1F+40 qui stocke _ZN1S3getEv et une pour _ZTV1F+16 qui stocke _ZNK1P3getEv. D'ailleurs, un static_assert(sizeof(F) == 24, "large!") nous garantit que notre petit objet, même s'il ne contient qu'un seul champ field de 4 octets sur ma machine, contient aussi des champs cachés : les deux adresses des vtables (8 octets chacune). Les 4 octets restants sont là pour l'alignement, d'ailleurs si on ajoute la directive #pragma pack(1), on obtient sizeof(F) == 20.

Conclusion

Déjà, merci à Lancelot Six, Paul Blottière et Adrien Guinet pour leurs relectures attentives /o/. Et en guise de conclusion, on ne peut qu'apprécier ce fameux principe du costless abstraction en C++. Par rapport à la complexité du langage, l'assembleur généré est d'une surprenante légèreté !

 



Article rédigé par

Par le(s) même(s) auteur(s)

Des soucis à la chaîne

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Spécialité(s)
Résumé

L’histoire, ou plutôt l’Histoire, est une coquine. Et quand Dennis Ritchie et Ken Thompson inventent le langage C en 1972, ils prennent une décision anodine, une micro-optimisation qui fait gagner quelques octets, mais qui aura un impact important sur la sécurité de nombreux systèmes : en C, les chaînes de caractères sont terminées par un octet positionné à zéro.

Édito

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Résumé

En regardant la liste des 25 failles les plus dangereuses éditées par MITRE chaque année, on ne peut qu’être frappé par la présence (ou la persistance) de thèmes bien connus : écriture illégale dans une zone mémoire, utilisation d’une zone mémoire désallouée, lecture illégale d’une zone mémoire, déréférencement de pointeur NULL, dépassement de la capacité d’un entier… Autant de sujets qui sont pourtant abordés dans les premiers chapitres de tout bouquin traitant de la sécurité logicielle. Ce qui n’en fait pas pour autant des sujets faciles dès lors que les considérations de base de code existant et de performances rentrent en compte. C’est compliqué l’optimisation multicritère !

Smash Bros

Magazine
Marque
MISC
HS n°
Numéro
30
Mois de parution
octobre 2024
Spécialité(s)
Résumé

En 1996, Aleph One publiait dans l’e-zine Phrack un article intitulé « Smashing the Stack for Fun and Profit ». C’était il y a plus de 25 ans et les principes énoncés dans cet article sont toujours valides, même si leur exploitation est devenue plus technique.De manière plus conventionnelle, l’écriture dans une zone mémoire non-autorisée est un vecteur d’attaque classique connu sous le doux nom de CWE-787.Dans cet article, on se concentrera sur les attaques ciblant la pile, en détaillant quelques bugs classiques, leur exploitation historique et quelques contre-mesures qui ont été mises en place au fil du temps.

Les derniers articles Premiums

Les derniers articles Premium

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Bash des temps modernes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les scripts Shell, et Bash spécifiquement, demeurent un standard, de facto, de notre industrie. Ils forment un composant primordial de toute distribution Linux, mais c’est aussi un outil de prédilection pour implémenter de nombreuses tâches d’automatisation, en particulier dans le « Cloud », par eux-mêmes ou conjointement à des solutions telles que Ansible. Pour toutes ces raisons et bien d’autres encore, savoir les concevoir de manière robuste et idempotente est crucial.

Les listes de lecture

11 article(s) - ajoutée le 01/07/2020
Clé de voûte d'une infrastructure Windows, Active Directory est l'une des cibles les plus appréciées des attaquants. Les articles regroupés dans cette liste vous permettront de découvrir l'état de la menace, les attaques et, bien sûr, les contre-mesures.
8 article(s) - ajoutée le 13/10/2020
Découvrez les méthodologies d'analyse de la sécurité des terminaux mobiles au travers d'exemples concrets sur Android et iOS.
10 article(s) - ajoutée le 13/10/2020
Vous retrouverez ici un ensemble d'articles sur les usages contemporains de la cryptographie (whitebox, courbes elliptiques, embarqué, post-quantique), qu'il s'agisse de rechercher des vulnérabilités ou simplement comprendre les fondamentaux du domaine.
Voir les 67 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous