On va voir les différentes attaques de contrôle de flow sur la stack. Pour vous rassurer, on s’en fout d’apprendre par coeur les noms de techniques, le but c’est pas ça. Le but c’est de comprendre les logiques derrières ces techniques, et peut-être en formaliser certaines. A un moment vous avez le mindset et on s’en fout du nom des techniques, surtout dans le milieu des CFA. Alors, commençons !
Le ret2win.. Bon, ça ça arrive qu’en CTF néophyte. Le principe est simple: donner à EIP la valeur de la fonction qui fait poper un shell, après qui sait.. un dev un peu trop surmené qui a oublié d’enlever sa fonction de debug ;) Voici le programme que l’on va prendre en exemple:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vuln (void);
void win (void);
int main (int argc, char *argv[])
{
vuln();
return EXIT_SUCCESS;
}
void vuln (void)
{
char buffer[32];
(void) gets(buffer);
}
void win (void)
{
setuid(0);
system("/bin/bash");
}
Et voici son makefile:
CC=gcc
NO_PIE=-fno-pie -no-pie
NO_SSP=-fno-stack-protector
NO_NX=-z execstack
NO_CET=-fcf-protection=none
PIE=-pie -fpie
SSP=-fstack-protector
NX=-z noexecstack
CET=-fcf-protection=full --enable-cet=yes
X86=-m32
x86_64=-m64
EXEC="prog"
all:
${CC} ${NO_PIE} ${NO_SSP} ${NO_CET} ${X86} -Wall -std=c89 code.c -o ${EXEC}
On compile:
Et.. Maintenant on PWN !! Bon, du coup on va faire simple: on va simplement faire en sorte que EIP = &win. Pour ça, on va y aller comme de gros cochons: on est en local donc on va foutre le merdier dans la stack, et voir qu’est-ce prend EIP comme valeur:
Ici on va juste attendre la fin de la fonction, puisque la dernière instruction, “ret”, va “pop eip”. Donc au final, on va prendre une valeur de la stack et la foutre dans EIP, et nous on va la controler.
On prend la valeur d’EIP. Si on a cette valeur, on va pouvoir l’appliquer à notre data de remplissage (notre junk), et les 4 octets suivants feront qu’on contrôle EIP ! Bon, notre offset est de 44, voici la preuve:
Voilà, on est au niveau du RET et ESP[0] vaut “BITE” (hehe), et maintenant si on continue..
Bah on peut pas exécuter le code situer sur “bite” :(.
Mais maintenant on va juste remplacer “bite” par l’adresse de win. Et n’oubliez pas de désactiver l’ASLR lol, ça serait bête de bloquer à cette étape:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Et c’est PWN !! Bon maintenant on va dire que vous êtes des pros de pwn, et on va passer à la suite !
Le principe est simple: faire en sorte qu’EIP arrive sur notre shellcode. Un shellcode c’est juste du code qui va exécuter un shell.
La question se pose: où diable va-t-on foutre ce shellcode dans le programme ? La réponse: lol, là où c’est exécutable & writable.
Aujourd’hui avec la protection NX/DEP/W^X on peut pas mettre notre shellcode n’importe où, mais bon, car on peut s’amuser avec mprotect(.data, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC) pour bypasser. Je vais juste vous montrer l’exemple simple.
Voici le code vulnérable:
#include <stdio.h>
#include <stdlib.h>
void vuln (void);
int main (int argc, char *argv[])
{
vuln();
return EXIT_SUCCESS;
}
void vuln (void)
{
char buffer[32];
(void) gets(buffer);
}
Voici le makefile associé:
CC=gcc
NO_PIE=-fno-pie -no-pie
NO_SSP=-fno-stack-protector
NO_NX=-z execstack
NO_CET=-fcf-protection=none
X86=-m32
EXEC="prog"
all:
${CC} ${NO_PIE} ${NO_SSP} ${NO_NX} ${NO_CET} ${X86} -Wall -std=c89 code.c -o ${EXEC}
On utilisera ce shellcode (attention, il est populaire :))
Donc au final, il faut juste jumper sur la stack, sur notre nopsled qui nous amènera gentillement à notre shellcode Alors y’en a qui mettent tout dans le payload, style
[junk] [eip] [nopsled] [shellcode]
Mais bon, faut une adresse de la stack, donc pas d’ASLR/PIE, ou alors faudrait un leak. La nopslef c’est juste un bunch d’instructions NOP (0x90).
D’autres fois on jump directement dans une variable d’environnement, mais là aussi c’est le même problème:
export ENV=[nopsled] [shellcode]
[junk] [eip]
Le plus propre est de jumper sur .data (ou autre section writable) et de mettre .data en exécutable, et là ça pète pas mal de choses, puisqu’on s’en fout d’ASLR et de NX. Sachez-le, mais là on va pas faire ça.
Nous on va faire l’exploit avec la variable d’environnement:
Voilà, c’est aussi simple que ça. Vous pouvez retrouver le programme getenv ici
Le ret2main est une technique qui vise à retourner sur la fonction main.
On va pas faire d’exemple, vous l’avez compris.. eip = main
:).
Dans quel cas va-t-on utiliser cette technique ? Quand vous pouvez pas atteindre 2 features d’un coup: vous allez par exemple leak une adresse, votre programme plante si y’a pas d’adresse de retour, donc on met main pour pouvoir pwn avec un élément en plus !
Le principe du ret2reg est assez cool, en gros ça consiste à jumper non pas sur une adresse hardcodée, mais sur la valeur d’un registre: eip = eax Donc c’est un peu bête, mais on va chercher des instructions comme:
call eax
jmp eax
jmp rsp
...
Le principe du ret2plt est d’appeler une fonction/procédure de la PLT. C’est utile car on a pas besoin de leak, les adresses de la PLT sont fixes (sauf si protection PIC).
eip = func@plt
Et voici votre stack:
+------------------------+
| junk |
+------------------------+
| saved EIP = func@plt |
+------------------------+
| ret de func@plt |
+------------------------+
| n-ième parametre |
+------------------------+
| ... |
+------------------------+
| 1er paramètre |
+------------------------+
Le ret2libc est pareil que le ret2plt, sauf que là nous appelons la fonction directement dans la libc. Le but c’est qu’on utilise les fonctions qu’on veut, par exemple on peut appeler la fonction system pour exécuter /bin/sh.
La stack est la même que pour le ret2plt.
+---------------------------+
| junk |
+---------------------------+
| Saved EIP = system@libc |
+---------------------------+
| XXXX |
+---------------------------+
| "/bin/sh\0" |
+---------------------------+
L’esp lifting est une technique pour exploiter les fonctions qui n’utilisent pas les pointeurs de stack (ESP/EBP).
Cette technique est utilisable contre les programmes utilisent donc le flag -fomit-frame-pointer
.
On va utiliser l’épilogue (la fin d’une fonction) d’une de ces fonctions, ça ressemble à ça:
add esp, <integer>
ret
En vrai y’a aussi des épilogues qui font juste du pop-ret, du coup on bouge de 4 octets.
pop reg
ret
Au final la technique ici est juste d’utiliser une de ces fonctions en tant qu’adresse de retour pour retourner dans une stack plus haute (et contrôlée)
+---------------------------+
| junk |
+---------------------------+
| Saved EIP = epiglogue | epilogue -> add esp, k; ret
+---------------------------+
| . . . | } k bytes
+---------------------------+
| controlled area |
+---------------------------+
Lorsque vous voulez vous amuser sur votre stack mais que celle-ci est instable.. vous n’avez pas le choix: il faut pivoter. Le principe comme vous avez pu l’imaginer est de faire en sorte que notre stack change d’endroit. Le principe du frame faking est d’écraser la sauvegarde d’EBP pour retourner ESP sur une fausse stackframe. Ce qui est cool c’est qu’on peut tromper ce que le code va lire, et donc détourner le flow d’exécution. Expliquations en image !
Ici, on va réécrire EBP pour par la suite contrôler ESP. On rappelle que l’instruction leave
enchaîne un ‘mov esp, ebp; pop ebp’. Donc notre frame 1 réécrit juste EBP, sur la frame 2 on doit faire en sorte que tout fonctionne pour qu’ensuite on refasse un leave
et qu’ESP prenne la valeur d’EBP, valeur qu’on a contrôlé dans la frame 1. Donc on va pouvoir bouger ESP sur notre frame personnalisée, là où on aura déjà eu un arbitrary write, puis c’est gagné. C’est gagné car on va faire un ret / pop eip
sur une stack contrôlée.
Les attaques de type off-by-one peuvent s’avérer fréquentes mais difficile d’exploitation. En fait c’est une attaque où on peut réécrire 1 octet de plus dans notre buffer. Dans certains cas, on peut réécrire une partie d’EBP, ce qui nous servirait à controler la stack.
Pour comprendre cette partie, vous devrez absolument comprendre cette partie Faut vraiment comprendre comment dl-runtime.c fonctionne.
Cette technique permet de tromper le linkage dynamique d’ELF et donc de pouvoir résolver une fonction (genre printf) par celle que l’on veut. Pas besoin de leak !
Il suffit de créer des structures JMPREL, DYNSYM & STRTAB et appeler une fonction pour la première fois pour tromper _dl_fixup(link_map, reloc_arg)
. C’est reloc_arg qui contiendra toutes les structures nécessaires pour que l’exploit puisse fonctionner.
STRTAB contiendra la chaîne “system\x00”.
C’est assez chiant, y’a pas mal d’histoires d’alignement etc.
Globalement le ret2csu s’applique à tous les binaires ELF x64 et permet de contrôler RDX
, perso dans la vraie vie j’y vois pas trop d’intérêt: on a des gadgets de partout; en CTF pourquoi pas. Ca permet d’exploiter la fonction __libc_csu_init
. Il y a 2 gadgets dans cette fonction:
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
ret
et
mov rdx, r15 <-------------
mov rsi, r14
mov edi, r13d
call QWORD PTR [r12 + rbx * 8]
add rbx, 0x1
cmp rbp, rbx
jne <__libc_csu_init+0x40>
add rsp, 0x8
Donc on a juste à contrôler RDX avec l’aide de R15, c’est tout.
La suite ici !