![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:
```c// Game structuretypedef 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:
```changman_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:
```cint 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) {        exit(EXIT_FAILURE);    }     // 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:
```changman_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:
```cvoid 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']) {                    puts("nope");                    --tries_remaining;                }                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;                            ++letters_found;                        }                    }                     // Check if the guess was incorrect                    if (last_counter == letters_found) {                        --tries_remaining;                    }                     // 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                puts("nope");                --tries_remaining;            }        }         // Formula for counting the score for losing        hangman->score += 0.25 * (puzzle_size - 1) * letters_found; gameover:        // 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);                free(name_buffer);            }             // [BUG4]            // Update high score message and value            snprintf(g_highscore_message, 512, "Highest player: %s",                hangman->name);            g_highscore = hangman->score;        }         // Clear game state        memset(hangman->letter_found, false, 26);        free(puzzle_word);    }}```
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:
```csnprintf(g_highscore_message, 512, "Highest player: %s",    hangman->name);```
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:
```csystem("/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)