PicoCTF 2018 - Buffer Overflow 0
Note: This article is part of our PicoCTF 2018 BinExp Guide.
Spot the Bug
This one is relatively easy - since the name is a complete giveaway:
void vuln(char *input){
char buf[16];
strcpy(buf, input);
}
// ...
vuln(argv[1]);
// ...
This is a classic buffer overflow. We are defining a variable on the stack of a fixed size (16 bytes), and then we are copying a user-controlled value (the first argument to the program) into that buffer without checking whether it would fit first.
Obviously, the first argument can be any length we want, and it certainly can be larger than 16 chars. Therefore, anything in memory that follows the buf
variable could potentially be overwritten (more on that in a bit). There are however some restrictions, namely that strcpy
will only copy values until the first null byte (0x00).
Strategy
Now that we found the bug, we need a plan to use that bug in a way that will allow us to capture the flag. Since this is the first challenge, they’ve made it particularly easy for us:
void sigsegv_handler(int sig) {
fprintf(stderr, "%s\n", flag);
fflush(stderr);
exit(1);
}
// ...
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(flag,FLAGSIZE_MAX,f);
signal(SIGSEGV, sigsegv_handler);
What this code does is load the content of the flag file (flag.txt) into a global variable named flag
. The code then registers a signal handler for the SIGSEGV
signal that will print the value of the flag
variable to stderr
. Getting the flag is just a matter of getting the program itself to segfault, which will call the segfault handler, and then print the flag.
Background Info
How does the vulnerable strcpy
help us generate a segfault? If you already know, then go ahead and solve the challenge now, it’s under /problems/buffer-overflow-0_0_6461b382721ccca2318b1d981d363924
on the shell server.
For those who don’t know, the secret is that the memory for the buf
variable is allocated on the stack. The stack is a region of memory that can shrink and grow over time, but it is also used for some very specific purposes on x86 architectures (32 bit programs make use of the stack heavily, much more so than 64 bit programs). Whenever you call a function, the address of the next instruction to execute after completing the function call is pushed onto the top of the stack (this is done automatically by the call
instruction). If the called function requires any stack itself (and on x86 it almost certainly will), then it will generally preserve the current value of the ebp
register by pushing it on the stack. It will then copy the current value of esp
(the address of the top of the stack) into the ebp
register, and then modify the stack by either pushing values or manually reserving space by modifying the esp
register. At the end of the function, it will have to restore the esp
and ebp
registers, and then call ret
, which is functionally equivalent to taking the value at the top of the stack and continuing execution from that address.
Generally, we say the stack grows “up”, which is to say that when we grow the stack, we actually start using memory with consecutively lower and lower addresses. (Which is upward if you were to look at memory vertically starting from low addresses at the top and incrementing to high addresses at the bottom).
Therefore, the layout in memory of the vuln function looks something like this
lower addresses | ⇒ | ⇒ | higher addresses |
---|---|---|---|
buf[16] | < padding > | <old ebp> | <return address> |
The padding exists because compilers will generally have alignment requirements, or preserve space to allow for “off-by-one” errors to not immediately crash to program.
As you can see, if you overwrite the memory past the end of buf
, you will eventually overwrite both the old ebp
value and the return address, since those values follow the stack variables in memory.
Here’s the corresponding assembly code of the vuln
function (I used objdump -M intel -S vuln
to look at the assembly)
push ebp
mov ebp,esp
sub esp,0x18
sub esp,0x8
push DWORD PTR [ebp+0x8]
lea eax,[ebp-0x18]
push eax
call 80484b0 <strcpy@plt>
add esp,0x10
nop
leave
ret
You can see here that the code actually reserves 0x18 (24) bytes for the stack buffer, so the “padding” in the above table is 8 bytes.
Exploitation
The easy way to approach this problem is to just execute the program with a sufficiently long argument, almost anything 32 chars or longer is guaranteed to overwrite both the old ebp
value AND the return address. If the return address isn’t valid code, or isn’t even mapped into the address space, then the program will immediately issue a segfault, the segfault handler will run, and the flag will be written to stderr.
/problems/buffer-overflow-0_0_6461b382721ccca2318b1d981d363924$ ./vuln 12345678901234567890123456789012
picoCTF{===REDACTED===}
The complications arise if your argument is long enough to overwrite the ebp
value on the stack, but not long enough to overwrite the return address. In this case, the program doesn’t immediately segfault, instead it continues but the value of the ebp
register is corrupt after returning from the function. Eventually that corrupt ebp
value ends up being used to populate the esp
register. At this point, the program has a non-functional stack (it likely points to memory that isn’t even mapped into the address space). With a corrupt esp
value, it is essentially impossible for code to run, because the x86 is so reliant on the stack. Therefore, even though you may have a SIGSEGV
handler registered, it will be impossible for that handler to even get called, and the program will have no choice but to crash.
/problems/buffer-overflow-0_0_6461b382721ccca2318b1d981d363924$ ./vuln 123456781234567812345678
Segmentation fault (core dumped)