Tags: csaw pwn pwnable heap-overflow heap 

Rating: 0

![Hungman challenge description](http://i.imgur.com/DW76cjS.png)

Hungman is a 300 point exploitation challenge from CSAW CTF Quals 2016. On the [challenge website](https://ctf.csaw.io/challenges), we are given two files for download: the [hungman executable](https://ctf.csaw.io/static/uploads/be71ed453f93d9cc30baa65314126248/hungman) and the [libc](https://ctf.csaw.io/static/uploads/d49d14b3cd81ede59498d62795565584/libc-2.23.so) used on the challenge server. By connecting to the service, you can play a difficult game of hangman. You only get three incorrect guesses before you lose, and the word to guess appears to be random lowercase letters.

![Hungman gameplay](http://i.imgur.com/Nk7Rcnu.png)

My first step in solving this challenge was to open the hungman executable with IDA Pro. The program keeps track of the player using a structure I named `hangman_game`, which looks like this:

// Game structure
typedef struct hangman_game {
int score;
int name_size; // Size of name (including \0)
char* name;
bool letters_found[26];
} hangman_game;

There are also three global variables of interest:

hangman_game* g_hangman;
char g_highscore_message[512];
int g_highscore;

Hungman consists of three functions, which I've named `main()`, `ask_name()`, and `play_hangman()`. The `main` function looks like this:

int main(void) {
char yes_or_no;
int dev_urandom_fd;

// Disable output buffering
setvbuf(stdout, NULL, _IONBF, 0);

// Set default highscore message
memset(g_highscore_message, '\0', 512);
memcpy(g_highscore_message, "Default Highscore ", 20);

// Default highscore is 64
g_highscore = 64;

// Source of random data
dev_urandom_fd = open("/dev/urandom", O_RDONLY);
if (dev_urandom_fd == -1) {

// Ask for the player's name and create game struct
g_hangman = ask_name();
printf("Welcome %s\n", g_hangman->name);

// Game loop
do {
// Play a game of hangman
play_hangman(g_hangman, dev_urandom_fd);

// Print high score
printf("%s ", g_highscore_message);
printf("score: %d\n", g_highscore);

// Ask to continue
printf("Continue? ");
scanf(" %c", &yes_or_no);
} while (yes_or_no != 'n');

return close(dev_urandom_fd);

There's not much of interest here. It just shows how the control flow works around the game. The `ask_name()` function is a little more interesting:

hangman_game* ask_name(void) {
char* name_buffer;
hangman_game* hangman;
int bytes_read;
char* name_end;
char input_str[248];

// Ask for the player's name
write(STDOUT_FILENO, "What's your name?\n", 18);
memset(input_str, '\0', 248);
bytes_read = read(STDIN_FILENO, input_str, 247);

// Ignore everything but the first line of input
name_end = strchr(input_str, '\n');
if (name_end != NULL) {
*name_end = '\0';

// Allocate just enough space to hold the name entered by the user
name_buffer = (char*)malloc(bytes_read);

// Allocate more than enough space to hold the game structure
hangman = (hangman_game*)malloc(128);

// Initialize the fields of the game structure and return it
memset(hangman, '\0', 128);
hangman->name = name_buffer;
hangman->name_size = bytes_read;
memcpy(hangman->name, input_str, bytes_read);
return hangman;

As you can see, `hangman->name` is allocated to hold just enough space for the name the player initially enters. This will be important very soon. Finally, `play_hangman()` is where all the fun happens. Here is the reversed C code for it:

void play_hangman(hangman_game* hangman, int dev_urandom_fd) {
char guess;
char yes_or_no;
int tries_remaining;
int letters_found;
int name_size;
int last_counter;
int bytes_read;
uint64_t i;
uint64_t j;
char* puzzle_word;
char* name_buffer;
char* end_of_name;

// Allocate space for the puzzle word
puzzle_size = hangman->name_size;
puzzle_word = (char*)malloc(puzzle_size);

if (puzzle_word != NULL) {
// Read random data for the puzzle
read(dev_urandom_fd, puzzle_word, puzzle_size);

// Each puzzle character is a random undercase letter
for (i = 0; i < puzzle_size - 1; i++) {
puzzle_word[i] ^= hangman->name[i];
puzzle_word[i] = (uint8_t)puzzle_word[i] % 26 + 'a';

// Variables that track gameplay progress
tries_remaining = 3;
letters_found = 0;
guess = '_';

// Loop once for each guess
while (tries_remaining > 0) {
// Print out the puzzle word, with blanks (_) where letters
// haven't yet been guessed.
for (i = 0; i < puzzle_size - 1; i++) {
if (hangman->letter_found[puzzle_word[i] - 'a']) {
write(STDOUT_FILENO, &puzzle_word[i], 1);
else {
write(STDOUT_FILENO, "_", 1);

// Print newline after puzzle word and read next guess
write(STDOUT_FILENO, "\n", 1);
scanf(" %c", &guess);

// If the guess isn't lowercase, it's definitely wrong
if (guess >= 'a' && guess <= 'z') {
// If the player already guessed this letter,
// that means this guess is wrong.
if (hangman->letter_found[guess - 'a']) {
else {
// Check if the guess is correct
last_counter = letters_found;
for (j = 0; j < puzzle_size - 1; j++) {
if (puzzle_word[j] == guess) {
// Mark this letter as found and increment
// correct letter counter.
hangman->letter_found[guess - 'a'] = true;

// Check if the guess was incorrect
if (last_counter == letters_found) {

// Check if this guess just solved the puzzle
if (letters_found >= puzzle_size - 1) {
hangman->score += 8 * (puzzle_size - 1);
goto gameover;
else {
// Guess wasn't a lowercase letter

// Formula for counting the score for losing
hangman->score += 0.25 * (puzzle_size - 1) * letters_found;

// Check if the player got a high score
if (hangman->score > g_highscore) {
// Ask the player to change names
puts("High score! change name?");
scanf(" %c", &yes_or_no);
if (yes_or_no == 'y') {
// Allocate space to read the new name
name_buffer = (char *)malloc(248);
memset(name_buffer, '\0', 248);

// [BUG1]
// Read in the new name and update the stored size
// in the game structure
bytes_read = read(STDIN_FILENO, name_buffer, 248);
hangman->name_size = bytes_read;

// Cut the name string at the first newline
end_of_name = strchr(name_buffer, '\n');
if (end_of_name != NULL) {
// [BUG2]
*end_of_name = '\0';

// [BUG3]
// Copy new name into the game structure's name buffer
memcpy(hangman->name, name_buffer, bytes_read);

// [BUG4]
// Update high score message and value
snprintf(g_highscore_message, 512, "Highest player: %s",
g_highscore = hangman->score;

// Clear game state
memset(hangman->letter_found, false, 26);

This function is much larger, and contains multiple bugs. The first one I noticed was the one I marked as `[BUG1]`. The issue here is that the call to `read()` may read up to 248 bytes which is the total size of the allocated name buffer. This could cause the name buffer to not be NUL-terminated. This bug is the cause for `[BUG2]` and `[BUG4]`.

`[BUG2]` is an OOB heap write bug that allows replacing a `0x0a` byte outside of the name buffer with a `0x00` byte. That could potentially be exploitable, though that would be very difficult and prone to failure.

`[BUG4]` is an infoleak bug allows leaking adjacent heap data (at least until the next `0x00` or `0x0a` byte).

`[BUG3]` is the most interesting bug in this case. It is a fairly standard heap buffer overwrite bug. The bug is the fact that the buffer is never resized after it is allocated in `ask_name()` to hold just enough space for the name that the user entered initially. So, by first setting the name to something small and then later changing to a longer name, this will write data outside of this object in the adjacent heap space.

Now that we know that there is a heap buffer overwrite vulnerability, it's time to see how the allocations are performed to see what we can corrupt using this bug. The first allocation performed is the name buffer (`g_hangman->name`). After this, the game structure (`g_hangman`) is allocated. By testing with [gdb-peda](https://github.com/longld/peda), I verified that `g_hangman` is always allocated directly after `g_hangman->name` (with some padding in between). Due to this ordering of allocations, triggering the heap overwrite from `[BUG3]` will allow us to corrupt the `hangman_game` structure.

I decided to focus on corrupting `g_hangman->name`, as we can both read and write the data pointed to by this pointer. I gained an arbitrary memory read primitive by setting `g_hangman->name` to the address I want to read from and reading the highscore player's name, with the caveat that we can't read NUL bytes because the message is created with `snprintf()`. This also becomes an arbitrary memory write primitive by getting a highscore again after setting `g_hangman->name` to the address to write. The name you send is written to that address.

My goal is to overwrite a GOT function pointer with the address of `system()` and call the overwritten function with `/bin/bash -i\0` as the first parameter to get a shell. Therefore I needed to pick a GOT function where I have control over the first argument. I chose to use `snprintf()`, because it is called in `play_hangman()` after the `memcpy()` call, and it is given `g_highscore_message` as its first parameter. Since I can have an arbitrary memory write primitive, I can control the contents of this buffer.

The first step of my exploit is to leak the address of `snprintf()` in the GOT. I compare this to the address of `snprintf()` in the provided libc to determine the ASLR slide of standard library functions. After this, I replace the GOT entry for `snprintf()` with the address of `system()`, adjusted for ASLR. I use a single memory write to do both this and overwrite `g_highscore_message`, as this buffer is just after the GOT in the executable. This means I'm going to be overwriting every entry in the GOT after `snprintf()`, so I try to set those to the correct locations to prevent any early crashes. After the end of the GOT, I just write zeros until I get to `g_highscore_message`, which I fill with `/bin/bash -i\0`. After this, the `play_hangman()` function gets to this statement:

snprintf(g_highscore_message, 512, "Highest player: %s",

At this point, we've replaced `snprintf()` with `system()` and filled `g_highscore_message` with `/bin/bash -i\0`, so this code will actually do the following:

system("/bin/bash -i");

And this completes the exploit. Running it gives me a shell, which I use to read the flag: `flag{this_looks_like_its_a_well_hungman}`. The Python script I wrote using [Gallopsled's pwntools](https://github.com/Gallopsled/pwntools) can be viewed here: [hungman.py](https://ghostbin.com/paste/bxf7v)

![Hungman exploit](http://i.imgur.com/V9LW6DX.png)