HTB Writeup - FlagCasino
The team stumbles into a long-abandoned casino. As you enter, the lights and music whir to life, and a staff of robots begin moving around and offering games, while skeletons of prewar patrons are slumped at slot machines. A robotic dealer waves you over and promises great wealth if you can win - can you beat the house and gather funds for the mission?
This is the description we are provided with when starting the challenge. The challenge belongs to the Reversing category and the Very easy difficulty.
We are also given a zip file containing a single executable: casino
Upon running the executable we get the following output:
[ ** WELCOME TO ROBO CASINO **]
, ,
(\____/)
(_oo_)
(O)
__||__ \)
[]/______\[] /
/ \______/ \/
/ /__\
(\ /____\
---------------------
[*** PLEASE PLACE YOUR BETS ***]
>
I input ’test'.
[ ** WELCOME TO ROBO CASINO **]
, ,
(\____/)
(_oo_)
(O)
__||__ \)
[]/______\[] /
/ \______/ \/
/ /__\
(\ /____\
---------------------
[*** PLEASE PLACE YOUR BETS ***]
> test
[ * INCORRECT * ]
[ *** ACTIVATING SECURITY SYSTEM - PLEASE VACATE *** ]
This definitely looks like an old-school crackme where you have to reverse engineer the assembly code to obtain the correct password, the flag, presumably.
So without further ado, I open Ghidra and analyze the single executable. This is what the main entry point of the program looks like in pseudocode:
undefined8 main(void)
{
int iVar1;
char local_d;
uint local_c;
puts("[ ** WELCOME TO ROBO CASINO **]");
// Very long puts() to draw the robot, omitted for better readability
puts("[*** PLEASE PLACE YOUR BETS ***]");
local_c = 0;
while( true ) {
if (0x1c < local_c) {
puts("[ ** HOUSE BALANCE $0 - PLEASE COME BACK LATER ** ]");
return 0;
}
printf("> ");
iVar1 = __isoc99_scanf(&DAT_001020fc,&local_d);
if (iVar1 != 1) break;
srand((int)local_d);
iVar1 = rand();
if (iVar1 != *(int *)(check + (long)(int)local_c * 4)) {
puts("[ * INCORRECT * ]");
puts("[ *** ACTIVATING SECURITY SYSTEM - PLEASE VACATE *** ]");
/* WARNING: Subroutine does not return */
exit(-2);
}
puts("[ * CORRECT *]");
local_c = local_c + 1;
}
/* WARNING: Subroutine does not return */
exit(-1);
}
After carefully cleaning it up a bit it looks like this:
int main(void)
{
int rand_num;
char user_input;
int scan_result;
while(true)
{
if (i > 28) {
return 0; // We have succesfully extracted all 28 characters from the flag
}
scan_result = scanf("%c", &user_input);
if(scan_result != 1) break; // Error when reading from STDIN
srand((int)user_input); // Set the seed for the RNG
rand_num = rand();
if (rand_num != check[i])
{
exit(-2); // Wrong character
}
i++;
}
}
We can deduce the following program logic from this:
- Read one character from standard input
- Set the seed of the random number generator to the user input
- Generate a random number based on that seed
- Check if the number matches the next character of the check array
If all characters match the array, then we have essentially found the flag. However, since we are not comparing the user input directly, but rather the random number, generated with the user-input seed, we can techincally say that the flag has been “encrypted” with the rng algorithm before being stored inside the check array.
From here we have two options to try and decrypt the flag:
- Reverse engineer the rng algorithm
- Good ol’ brute-forcing the flag
I opted for the second option for one simple reason. The program actually tells us if the character we have provided is correct or not, so we are not guessing completely blindly. Thanks to this we can make an automated script that checks each character one by one. So, if we assume the flag is entirely composed of printable ASCII characters, we can say that in the worst case scenario we will have to check $95 \times 28 = 2660$ times.
In a normal scenario where we have to guess the entire flag at once we would be put in a worst case scenario of $95^{28}$ guesses, a number with more than 50 digits, a slightly less desireable scenario if you ask me.
Now comes the real question, how do we automate the guessing process so we don’t have to spend hours manually writing characters?
There are many ways to approach this problem, but the easiest one I found is to use the subprocess python library. This library can run programs in a controlled environment where you can write to stdin and read from stdout with code.
After a bit of playing around I ended up making the following script:
import subprocess
import time
import string
def try_chars(char_list):
proc = subprocess.Popen(
["stdbuf", "-oL", "./casino"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
bufsize=1
)
for c in char_list:
proc.stdin.write(c)
proc.stdin.write('\n')
proc.stdin.flush()
# Give time to process input
time.sleep(0.01)
proc.terminate()
try:
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
proc.kill()
output_lines = []
try:
for line in proc.stdout:
output_lines.append(line.strip())
except Exception as e:
print("Error:", e)
last_line = output_lines[-1]
return "CORRECT" in last_line
charset = list(string.digits + string.ascii_letters + string.punctuation)
correct_chars = 0
flag = []
while correct_chars < 28:
for c in charset:
if try_chars(flag + [c]):
flag.append(c)
print(flag)
correct_chars = correct_chars + 1
break
print(''.join(flag))
Let’s break it up and see how it works.
The main block of code here is the try_chars function. It takes a list of charaters and returns true if the characters are part of the flag and false if one or more of the characters are wrong.
This function is used inside a while loop, which will test all characters in all positions and store the correct ones, to finally display the full flag on screen.
As I mentioned previously, we create an environment where we will run the stdbuf -oL ./casino command. Initially I was running just ./casino, but I kept having problems when reading from stdout so I had to make that adjustment.
Then, a for loop writes all the charaters provided to the function to stdin and we flush. At this point we may or may not have guessed correctly, to check this we kill the program and return a value depending on the contents of the last line.
That is the main idea behind the script, quite simple really.
So now the moment of truth, running the script.
['H']
['H', 'T']
['H', 'T', 'B']
['H', 'T', 'B', '{']
['H', 'T', 'B', '{', 'r']
['H', 'T', 'B', '{', 'r', '4']
['H', 'T', 'B', '{', 'r', '4', 'n']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r', '3']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r', '3', 'd']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r', '3', 'd', '1']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r', '3', 'd', '1', 'c']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r', '3', 'd', '1', 'c', 't']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r', '3', 'd', '1', 'c', 't', '4']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r', '3', 'd', '1', 'c', 't', '4', 'b']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r', '3', 'd', '1', 'c', 't', '4', 'b', 'l']
['H', 'T', 'B', '{', 'r', '4', 'n', 'd', '_', '1', 's', '_', 'v', '3', 'r', 'y', '_', 'p', 'r', '3', 'd', '1', 'c', 't', '4', 'b', 'l', '3']
HTB{r4nd_1s_v3ry_pr3d1ct4bl3
And just like that we get the flag. Thanks to the power of post-production you didn’t have to see all the failed attempts and different iterations of the script. Now we can all act as if I got it right on my first try :-)
According to my shell, it takes 9 seconds for the script to run. So, yeah, there is definitely some room for improvement; however, since I’m the greatest programmer in history, I’m going to blame python for it.
And that wraps everything up.
To whoever is reading this, thank you, stay safe, and more importantly, stay invisible.
PS: I just noticed the output of the script is actually missing the last character of the flag. That is because on the last iteration, instead of outputting “CORRECT”, the program says “HOUSE BALANCE $0 - PLEASE COME BACK LATER”. It’s not a mistake, it’s an easter egg! :-)