[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)
|
|
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:
- Dans un cas nominal (AAAAAAA)
- 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:
|
|
Ah, on voit des choses intéressantes en adresse 0x7ffe4cea4dd0
:
- Nous avons nos AAAAAAA (5A) passés en argument et copiés dans notre variable
ong
- 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)
|
|
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):
|
|
1094795585 en décimal = 0x41414141 en hexa => CQFD :-)
On se résume
- Avec un binaire compilé sans les mécanismes de protection d’exécution de la pile
- 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
- Permets alors de modifier le comportement du binaire
- Soit pour modifier des valeurs de variables (comme le montant dans notre exemple)
- 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.