[PROLOG] 0x003, Un premier Buffer Overflow (Part 1) | (French version)

November 15, 2022    prolog bof

Hello,

Comme vous avez soigneusement lu les 3 premiers billets de la série PROLOG (si celà n’est pas le cas, je vous invite vivement à les lire avant celui-ci : Assembler(1), Mémoire(2), conventions d’appels(3)) est venu le temps de les mettre en application.

Et pour un premier exercice d’application, la compréhension d’un Buffer Overflow est parfaite. A ce stade de la pédagogie, nous allons prendre un exemple fictif avec une simplicité que vous ne trouverez pas sur le théatre des opérations. En effet, les OS et compilateurs ont depuis longtemps mis en place de nouveaux moyens de protection contre l’exploitation de ces dépassements de buffer : pile non-exécutable, allocation mémoire aléatoire, canary, …

Un peu plus tard, nous verons que ces moyens de protections sont eux aussi ‘bypassable’ …

Je ne ferai pas de rappel théorique dans ce billet, pour celà je vous renvoie aux billets PROLOG précédants.

Mais c’est quoi un BOF ?

Un Buffer OverFlow, c’est comme son nom l’indique le fait de transférer dans une zone mémoire pré-dimensionnée, une quantité d’informations dont la taille est supérieure à cette zone mémoire. Plus précisément, un BOF consiste à provoquer ce débordement et à exploiter les effets de bord provoqués par ce débordement.

Il existe plusieurs zones mémoire d’attaque possible pour un Buffer Overflow (Heap, Stack, …). Nous allons ici étudier la plus répendue : le STACK Buffer Overflow. Le Stack BOF consiste donc à utiliser une erreur de programation pour faire exécuter à un programme légitime, du code à nous (ex: un shell code pour nous donner un beau shell sur la machine).


Notre objectif est donc de venir écraser la valeur du registre EIP/RIP par une adresse de notre de choix, et ce, afin d'orienter le flux d'éxécution sur du code que nous aurions préalablement injecté en mémoire.

Le vilain petit programme

Afin de pouvoir étudier simplement un premier BOF, il nous faut un programme vulnérable. Dans un souci de pédagogie, nous allons prendre le vilain listing suivant :

Afin de pouvoir faire un peu joujou simplement, il nous faut compiler ce programme avec les directives qui suppriment les différentes protections mises en place par les compilateurs et OS récents.

$ gcc -fno-stack-protector -no-pie -m32 -z execstack bof.c -o bof

Notre outil : RIZIN

Pour nous changer un peu de ce bon vieux GDB ;-) , je vous propose dans cet exemple d’utiliser mon débogueur en ligne de commande préféré, le magnifique RIZIN.

RIZIN est un framework open source pour le Reverse engineering et l’analyse de binaire. R2 implémente une interface de ligne de commande riche pour désassembler, analyser des données, corriger des binaires, comparer des données, rechercher, remplacer. Il a de grandes capacités de script, il est disponible sur les principaux OS (Linux, Windows, OSX,…). R2 est extrement puissant et totalement gratuit.

RADARE2 se compose d’un ensemble d’utilitaires qui peuvent être utilisés ensemble à partir du shell R2 ou indépendamment.

Les premières utilisations du CLI RIZIN peuvent un peu déstabiliser au départ. En effet, les commandes sont nombreuses (car l’outil est puissant) et utilise une syntaxe courte (bien souvent 2 ou 3 lettres). Mais une fois vos commandes usuelles rapidement mémorisées, vous bénéficierez d’une vélocité très élevée. Il existe sur le net myriades de cheatsheets dont je vous conseille une belle impression en A3 pour vos premières semaines avec le bolide.

J’expliciterai ici les commandes que nous allons utiliser et je vous renvoie au site officiel de RADARE2 pour une formation détaillée à ce framework de reverse.

Allez, on charge notre vilain binaire sous notre microscope électronique RIZIN:

$ rizin -d bof-1 GREENPEACE

et on commence avec 3 commandes rizin :

1. Analyse intelligente du code désassemblé (AAA)
2. On pose un breakpoint sur la fonction main (db main) 
3. On affiche le code asm de la fonction main (pdf)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

 $ rizin -d bof-1

[0x7f91ee0422b0]> aaa

INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze all functions arguments/locals (afva@@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Skipping type matching analysis in debugger mode (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Use -AA or aaaa to perform additional experimental analysis

[0x7f91ee0422b0]> db @ main

[0x7f91ee0422b0]> dc
hit breakpoint at: 0x4011c9

[0x004011c9]> pdf
            ;-- rax:
            ;-- r13:
            ;-- rip:
            ; DATA XREF from entry0 @ 0x4010c8(r)
 139: int main (int argc, char **argv);
           ; arg int argc @ rdi
           ; arg char **argv @ rsi
           ; var int64_t var_4h @ rbp-0x4
           ; var int64_t var_ch @ rbp-0xc
           ; var int64_t var_14h @ rbp-0x14
           ; var int64_t var_20h @ rbp-0x20
           0x004011c9 b    f30f1efa       endbr64
           0x004011cd      55             push rbp
           0x004011ce      4889e5         mov rbp, rsp
           0x004011d1      4883ec20       sub rsp, 0x20
           0x004011d5      897dec         mov dword [var_14h], edi    ; argc
           0x004011d8      488975e0       mov qword [var_20h], rsi    ; argv
           0x004011dc      c745fce80300.  mov dword [var_4h], 0x3e8   ; 1000
           0x004011e3      837dec01       cmp dword [var_14h], 1
       ┌─< 0x004011e7      741c           je 0x401205
          0x004011e9      488b45e0       mov rax, qword [var_20h]
          0x004011ed      4883c008       add rax, 8
          0x004011f1      488b10         mov rdx, qword [rax]
          0x004011f4      488d45f4       lea rax, [var_ch]
          0x004011f8      4889d6         mov rsi, rdx
          0x004011fb      4889c7         mov rdi, rax
          0x004011fe      e86dfeffff     call sym.imp.strcpy         ; char *strcpy(char *dest, const char *src)
      ┌──< 0x00401203      eb28           jmp 0x40122d
      │└─> 0x00401205      488b45e0       mov rax, qword [var_20h]
          0x00401209      488b00         mov rax, qword [rax]
          0x0040120c      4889c6         mov rsi, rax
          0x0040120f      488d05210e00.  lea rax, str._n_tusage:__s__destinataire__n_n ; 0x402037 ; "\n\tusage: %s <destinataire>\n\n"
          0x00401216      4889c7         mov rdi, rax
          0x00401219      b800000000     mov eax, 0
          0x0040121e      e86dfeffff     call sym.imp.printf         ; int printf(const char *format)
          0x00401223      bf00000000     mov edi, 0
          0x00401228      e873feffff     call sym.imp.exit           ; void exit(int status)
          ; CODE XREF from main @ 0x401203(x)
      └──> 0x0040122d      8b55fc         mov edx, dword [var_4h]
           0x00401230      488d45f4       lea rax, [var_ch]
           0x00401234      89d6           mov esi, edx
           0x00401236      4889c7         mov rdi, rax
           0x00401239      e858ffffff     call sym.donner
           0x0040123e      488d050f0e00.  lea rax, str.Un_grand_MERCI ; 0x402054 ; "Un grand MERCI"
           0x00401245      4889c7         mov rdi, rax
           0x00401248      e833feffff     call sym.imp.puts           ; int puts(const char *s)
           0x0040124d      b800000000     mov eax, 0
           0x00401252      c9             leave
           0x00401253      c3             ret
[0x004011c9]>

Puis nous continuons l’exécution jusqu’à l’adresse 0x004011dc, pour afficher la valeur (1000) de notre paramètre montant.

La valeur de notre don est donc stockée à l’adresse mémoire (virtuelle) 0x0. Notons cette adresse quelque part, nous allons y revenir dans 2 minutes.

Le problème du vilain : strcpy(ong,argv[1])

Dans ce vilain programme C, notre développeur étourdi copie une zone mémoire dont la taille n’est ni définie, “ni cappée” dans une zone mémoire réservée pour notre variable montant; zone mémoire pour laquelle nous avons dit au compilateur: “Je ferai 7 caractères max, pas un de plus, promis”.

Évidemment l’erreur est flagrante, mais c’était ce genre de chose que l’on trouvait souvent à l’époque où GCC ne levait pas de warning lors d’une mauvaise utilisation de la fonction gets (par exemple).

Mais que se passe-t-il alors si je copie plus de 7 caractères dans cette variable locale ong ? Et bien, je vais prendre l’emplacement mémoire de quelqu’un …. :-( Et là, tout devient bizarre.

Plus précisément, je vais faire un Buffer Overflow en écrasant le contenu des adresses mémoires contiguës à celles de mon buffer : bref je propose du code binaire au CPU pour lequel le programme n’a pas été câblé …

Regardez ce que deviennent mes 1000 euros de don avec différentes valeurs/tailles de l’argument:

$ bof-1 AAA
Vous avez fait un don de 1000 euros a l'ONG AAA.
Un grand MERCI
$ bof-1 AAAAAAAA
Vous avez fait un don de 768 euros a l'ONG AAAAAAAA.
Un grand MERCI

aïe,

$ bof-1 AAAAAAAAAAAAAAA
Vous avez fait un don de 1094795585 euros a l'ONG AAAAAAAAAAAAAAA.
Un grand MERCI

Hey NNNONNN pas 1094795585 euros !!

Pour comprendre, il faut mettre cette exécution sous notre microscope R2:

Souvenez-vous de l’adresse mémoire précédemment notée de notre variable montant. On va donc regarder ce qui se passe vers cette zone:

  1. Dans un cas nominal (AAAAAAA)
  2. Dans un cas de buffer overflow (AAAAAAAAAAAAAAA)

Cas nominal

$ rizin -d bof-1 AAAAAAA

 -- It's not a bug, it's a work in progress
[0x7f8c21d742b0]> aaa

INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze all functions arguments/locals (afva@@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Skipping type matching analysis in debugger mode (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Use -AA or aaaa to perform additional experimental analysis

[0x7f8c21d742b0]> db @ main
[0x7f8c21d742b0]> dc

hit breakpoint at: 0x4011c9

[0x004011c9]> pdf
            ;-- rax:
            ;-- r13:
            ;-- rip:
            ; DATA XREF from entry0 @ 0x4010c8(r)
┌ 139: int main (int argc, char **argv);
│           ; arg int argc @ rdi
│           ; arg char **argv @ rsi
│           ; var int64_t var_4h @ rbp-0x4
│           ; var int64_t var_ch @ rbp-0xc
│           ; var int64_t var_14h @ rbp-0x14
│           ; var int64_t var_20h @ rbp-0x20
│           0x004011c9 b    f30f1efa       endbr64
│           0x004011cd      55             push rbp
│           0x004011ce      4889e5         mov rbp, rsp
│           0x004011d1      4883ec20       sub rsp, 0x20
│           0x004011d5      897dec         mov dword [var_14h], edi    ; argc
│           0x004011d8      488975e0       mov qword [var_20h], rsi    ; argv
│           0x004011dc      c745fce80300.  mov dword [var_4h], 0x3e8   ; 1000
│           0x004011e3      837dec01       cmp dword [var_14h], 1
│       ┌─< 0x004011e7      741c           je 0x401205
│       │   0x004011e9      488b45e0       mov rax, qword [var_20h]
│       │   0x004011ed      4883c008       add rax, 8
│       │   0x004011f1      488b10         mov rdx, qword [rax]
│       │   0x004011f4      488d45f4       lea rax, [var_ch]
│       │   0x004011f8      4889d6         mov rsi, rdx
│       │   0x004011fb      4889c7         mov rdi, rax
│       │   0x004011fe      e86dfeffff     call sym.imp.strcpy         ; char *strcpy(char *dest, const char *src)
│      ┌──< 0x00401203      eb28           jmp 0x40122d
│      │└─> 0x00401205      488b45e0       mov rax, qword [var_20h]
│      │    0x00401209      488b00         mov rax, qword [rax]
│      │    0x0040120c      4889c6         mov rsi, rax
│      │    0x0040120f      488d05210e00.  lea rax, str._n_tusage:__s__destinataire__n_n ; 0x402037 ; "\n\tusage: %s <destinataire>\n\n"
│      │    0x00401216      4889c7         mov rdi, rax
│      │    0x00401219      b800000000     mov eax, 0
│      │    0x0040121e      e86dfeffff     call sym.imp.printf         ; int printf(const char *format)
│      │    0x00401223      bf00000000     mov edi, 0
│      │    0x00401228      e873feffff     call sym.imp.exit           ; void exit(int status)
│      │    ; CODE XREF from main @ 0x401203(x)
│      └──> 0x0040122d      8b55fc         mov edx, dword [var_4h]
│           0x00401230      488d45f4       lea rax, [var_ch]
│           0x00401234      89d6           mov esi, edx
│           0x00401236      4889c7         mov rdi, rax
│           0x00401239      e858ffffff     call sym.donner
│           0x0040123e      488d050f0e00.  lea rax, str.Un_grand_MERCI ; 0x402054 ; "Un grand MERCI"
│           0x00401245      4889c7         mov rdi, rax
│           0x00401248      e833feffff     call sym.imp.puts           ; int puts(const char *s)
│           0x0040124d      b800000000     mov eax, 0
│           0x00401252      c9             leave
└           0x00401253      c3             ret
[0x004011c9]>

Puis on pose un breakpoint en 0x00401239 pour pouvoir observer la stack à ce moment de l’exécution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[0x004011c9]> dsu 0x00401239

[0x00401239]> pxw @ rsp

0x7ffe4cea4dc0  0x4cea4ef8 0x00007ffe 0x00000064 0x00000002  .N.L....d.......

0x7ffe4cea4dd0  0x00001000 0x41414141 0x00414141 0x000003e8  ....AAAAAAA.....

0x7ffe4cea4de0  0x00000002 0x00000000 0x21b4ad90 0x00007f8c  ...........!....
0x7ffe4cea4df0  0x00000000 0x00000000 0x004011c9 0x00000000  ..........@.....
0x7ffe4cea4e00  0x4cea4ee0 0x00000002 0x4cea4ef8 0x00007ffe  .N.L.....N.L....
0x7ffe4cea4e10  0x00000000 0x00000000 0xaf63bf43 0x2c9c30f6  ........C.c..0.,
0x7ffe4cea4e20  0x4cea4ef8 0x00007ffe 0x004011c9 0x00000000  .N.L......@.....
0x7ffe4cea4e30  0x00403e18 0x00000000 0x21d8e040 0x00007f8c  .>@.....@..!....
0x7ffe4cea4e40  0x3487bf43 0xd360a922 0xf5efbf43 0xd384739f  C..4".`.C....s..
0x7ffe4cea4e50  0x00000000 0x00007f8c 0x00000000 0x00000000  ................
0x7ffe4cea4e60  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x7ffe4cea4e70  0x00000000 0x00000000 0xd3274900 0x36dedb15  .........I'....6
0x7ffe4cea4e80  0x00000000 0x00000000 0x21b4ae40 0x00007f8c  ........@..!....
0x7ffe4cea4e90  0x4cea4f10 0x00007ffe 0x00403e18 0x00000000  .O.L.....>@.....
0x7ffe4cea4ea0  0x21d8f2e0 0x00007f8c 0x00000000 0x00000000  ...!............
0x7ffe4cea4eb0  0x00000000 0x00000000 0x004010b0 0x00000000  ..........@.....
[0x00401239]>

Ah, on voit des choses intéressantes en adresse 0x7ffe4cea4dd0 :

  1. Nous avons nos AAAAAAA (5A) passés en argument et copiés dans notre variable ong
  2. Et ce buffer a comme voisin de stack un octet dont la valeur Hexa est 0x3e8, soit 1000 en décimal: tient tient la valeur de montant.

Mais que se passerait-il si notre grossier voisin prenait un peu plus de place ? Essayons avec le gros voisin AAAAAAAAA (9A)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[0x0040122d]> pxw @ rsp
0x7fff8aaef840  0x8aaef978 0x00007fff 0x00000064 0x00000002  x.......d.......

0x7fff8aaef850  0x00001000 0x41414141 0x41414141 0x00000041  ....AAAAAAAAA...

0x7fff8aaef860  0x00000002 0x00000000 0x8f1f8d90 0x00007faa  ................
0x7fff8aaef870  0x00000000 0x00000000 0x004011c9 0x00000000  ..........@.....
0x7fff8aaef880  0x8aaef960 0x00000002 0x8aaef978 0x00007fff  `.......x.......
0x7fff8aaef890  0x00000000 0x00000000 0x306672a6 0x200a4e9c  .........rf0.N.
0x7fff8aaef8a0  0x8aaef978 0x00007fff 0x004011c9 0x00000000  x.........@.....
0x7fff8aaef8b0  0x00403e18 0x00000000 0x8f43c040 0x00007faa  .>@.....@.C.....
0x7fff8aaef8c0  0xc08272a6 0xdff55bc1 0x2aea72a6 0xdf5f50a3  .r...[...r.*.P_.
0x7fff8aaef8d0  0x00000000 0x00007faa 0x00000000 0x00000000  ................
0x7fff8aaef8e0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x7fff8aaef8f0  0x00000000 0x00000000 0x2cb28d00 0xd9efe9db  ...........,....
0x7fff8aaef900  0x00000000 0x00000000 0x8f1f8e40 0x00007faa  ........@.......
0x7fff8aaef910  0x8aaef990 0x00007fff 0x00403e18 0x00000000  .........>@.....
0x7fff8aaef920  0x8f43d2e0 0x00007faa 0x00000000 0x00000000  ..C.............
0x7fff8aaef930  0x00000000 0x00000000 0x004010b0 0x00000000  ..........@.....

[0x0040122d]> dc
Vous avez fait un don de 65 euros a l'ONG AAAAAAAAA.
Un grand MERCI
(124) Process exited with status=0x0

Notre 1000 est devenu 65, et justement 65 en décimal s’écrit 0x41 en Hexa : ça y est vous avez compris comment changer 1000 en 65 par buffer overflow.

Allez, pour enfoncer le cloud, on le passe avec un voisin encore plus envahissant : AAAAAAAAAAAA (12A):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[0x0040122d]> pxw @ rsp
0x7ffd48115ce0  0x48115e18 0x00007ffd 0x00000064 0x00000002  .^.H....d.......
0x7ffd48115cf0  0x00001000 0x41414141 0x41414141 0x41414141  ....AAAAAAAAAAAA
0x7ffd48115d00  0x00000000 0x00000000 0x72231d90 0x00007f40  ..........#r@...
0x7ffd48115d10  0x00000000 0x00000000 0x004011c9 0x00000000  ..........@.....
0x7ffd48115d20  0x48115e00 0x00000002 0x48115e18 0x00007ffd  .^.H.....^.H....
0x7ffd48115d30  0x00000000 0x00000000 0xe51b720b 0x6d485141  .........r..AQHm
0x7ffd48115d40  0x48115e18 0x00007ffd 0x004011c9 0x00000000  .^.H......@.....
0x7ffd48115d50  0x00403e18 0x00000000 0x72475040 0x00007f40  .>@.....@PGr@...
0x7ffd48115d60  0x5f3f720b 0x92b2c163 0xdf97720b 0x93c8b507  .r?_c....r......
0x7ffd48115d70  0x00000000 0x00007f40 0x00000000 0x00000000  ....@...........
0x7ffd48115d80  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x7ffd48115d90  0x00000000 0x00000000 0x85cbe700 0x1e818e20  ............ ...
0x7ffd48115da0  0x00000000 0x00000000 0x72231e40 0x00007f40  ........@.#r@...
0x7ffd48115db0  0x48115e30 0x00007ffd 0x00403e18 0x00000000  0^.H.....>@.....
0x7ffd48115dc0  0x724762e0 0x00007f40 0x00000000 0x00000000  .bGr@...........
0x7ffd48115dd0  0x00000000 0x00000000 0x004010b0 0x00000000  ..........@.....

[0x0040122d]> dc
Vous avez fait un don de 1094795585 euros a l'ONG AAAAAAAAAAAA.
Un grand MERCI
(125) Process exited with status=0x0
[0x7f40722f2ca1]>

1094795585 en décimal = 0x41414141 en hexa => CQFD :-)

On se résume

  1. Avec un binaire compilé sans les mécanismes de protection d’exécution de la pile
  2. Une erreur de programmation qui consiste à ne pas vérifier/limiter la taille de la source lors d’une copie dans une zone mémoire de destination plus petite
  3. Permets alors de modifier le comportement du binaire
  4. Soit pour modifier des valeurs de variables (comme le montant dans notre exemple)
  5. Soit pour en modifier le flux d’exécution

Pour modifier le flux d’exécution, on peut, par exemple, éviter ou influencer un saut conditionnel (pour se faire on pose des NOP 0x90 sur les adresses du test afin que le flux traverse le test, ou l’on change la valeur du prédicat pour faire basculer le test sur le chemin d’exécution qui nous arrange (ex: passWordIsCorrect() ;-). Je ne vais pas ici vous montrer cela, car c’est exactement la même chose qu’avec nos AAAAA.

Non, notre prochaine étape consiste à créer un STACK Buffer Overflow, c.-à-d., de faire exécuter un code complet nous appartenant par le binaire. Bref, on veut lui faire exécuter notre Shellcode.

Le billet suivant est ici.