Hacker1 CTF - Hello World!

The first "native" program hack I attempt in a Hacker1 CTF

Hello World!

Ah, the age old "first program" one typically writes when learning a language and/or toolchain. Fitting that this would be the first reverse engineering task I attempt on Hacker1. We're going to use some tools to help us, namely edb. This tool can, of course, be found as an easily installable package as part of a Kali Linux install.

Flag 0

The page presented is simple enough. A link labeled "Download Binary" and a single entry form to submit.

Let's look around in the binary (after downloading it locally and confirming that is an executable in ELF format. It's good to start at the beginning, but let's start at the beginning of the user code in hopes of saving some time searching around. Where is main? Aha

After having done so many CTFs for microcorruption, I'm fairly sure what to look for in a challenge of difficulty Moderate like this one. The program is bound to take some user input, and we'll want to find out where it puts it and what, if any, validation or defensive coding it has to prevent "errors". Looking at the disassembly shows that a memory buffer gets initialized with a fixed size (bad idea) and the user input is not checked for length (terrible idea). This is ripe for either a stack overflow or heap corruption.

Make It Go Boom

Well, since the stack appears to be so easily overwritten, let's go ahead and crash the thing. Yep, boom! 💥

What comes after the user input in the stack? Ah, that looks interesting, there is a return address of course. So can we write something to that memory and jump elsewhere in the code upon return?

Hunting around in the disassembled code doesn't take long since the program is so short and since, well one obvious thing appears to be a reference to the ASCII string "FLAGS". Even more obvious is the edb says the function's name is print_flags.

This code block uses the c stdlib function getenv to get the value of an environment variable named "FLAGS" and then attempts to print it out (whether it is found or not). If we manage to jump to that code with our stack overflow, we should be able to get the flag(s). Since we don't have the flags in our local environment, we'll have to trust that the server set the environment variable to the secret value and it will output on the page when exploited.

GETENV(3)                 Linux Programmer's Manual                 GETENV(3)

       getenv, secure_getenv - get an environment variable

       #include <stdlib.h>

       char *getenv(const char *name);

       char *secure_getenv(const char *name);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       secure_getenv(): _GNU_SOURCE

       The  getenv() function searches the environment list to find the envi‐
       ronment variable name, and returns  a  pointer  to  the  corresponding
       value string.

The Payload

OK, 40 bytes after the start of the user controllable memory is a return address. Let's just write a bunch of stuff until we get there and then write the address we want to jump to upon return, 0x004006ee1. Hmm, more segmentation faults. Aha! The addresses are 8 bytes and the upper 4 bytes of the address in the stack are not what we want, so we need to null those out.{ctf}/?stdin=%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%ee%06%40%00%00%00%00%00

Flag captured!

A Couple Last Notes

Why the %xx in the url? Remember we want to be able to write any bytes we want, and only inputting ASCII characters is going to restrict what values we can reasonably input. Rather than using the form input, I just use the URL since it was a GET anyways. You could also use cURL or similar.

Why is the input that matters "%ee%06%40%00 when we want to jump to address 0x004006ee? Because the target machine must be little endian so we have to input the bytes, which are written to memory in increasing order, in little endian order.

  1. Another thing that makes this challenge so easy it the fact that we don't have to worry about NULL bytes in the input. This is because the code (on purpose, I'm sure) uses a custom function read_all_stdin instead of something like gets or sprintf that would terminate input upon encountering a value of NULL (0x00)