DFIR – Hack The Box – FlagCasino
Hack the box provides a collection of Capture The Flag (CTF) style challenges focused on cyber security, digital forensics, and incident response. The objective is to obtain the flag, typically a small message/password, as proof you were able to complete the challenge.
Challenge: FlagCasino
FlagCasino is a reverse engineering challenge on a binary executable. The description conveys it’s a game you are to play and gain funds. Let’s dive in.
Downloads
The CTF provides the following scenario files:
Filename | MD5 |
rev_flagcas ino.zip | 24F23D0194B0CA3A3550884F286DE53B |
rev_flagcasino.zip/Casino | 0478C3A259655AFCD1CC67E808C20861 |
Our interest is in the Casino
file.
Static Analysis
There’s no extension, so the first thing we can do is check the header of the file.
PowerShell: |
((Get-Content .\casino -Encoding Byte -TotalCount 50) | ForEach-Object { [char] $_ }) -join "" |
ELF> @(; |
An Executable Linkable Format (ELF) is a Linux based binary executable/library. Let’s further analyse the file in Detect It Easy:
Detect It Easy v3.10 – .\casino |
.\casino 78 msec |
This gave us some more useful information, it’s a x64 architecture binary, written in C, and compiled with GCC.
An area of initial interest will be the strings stored within the file. There are many methods to extract the strings, many applications simply run searches for consecutive printable characters within a specific threshold; larger than 3 characters, less than 50.
Upon first look, we can see the following strings which were extracted with Detect It Easy:
Strings: |
Address |
We can see the same data running the `readelf
` command on Linux to obtain the read only data section (.rodata
) of the ELF file:
Command: readelf -x .rodata ./casino |
0x00002000 01000200 00000000 00000000 00000000 ................ |
Of course it would be too easy if the flag was present in plaint text here.
We have a Linux executable, and based on the strings we can see the basic premise of the game: a bet is placed, it’s correct or incorrect and there may be a condition which ends the game prematurely, or we’ve emptied the house funds from winning.
Dynamic Analysis
Let’s perform some dynamic analysis. A Linux Ubuntu 24.04.2 LTS virtual machine is setup, the casino file transferred across, and executed within a terminal window:
Output: |
jamie@VM-Linux-Ubuntu:~/Documents$ ./casino |
As presumed, the game asks for a bet, and a correct/incorrect response is given. Upon the incorrect bet, the game ended immediately. A few more attempts were made with numbers (small, large, negative, positive), letters, punctuation, all of various lengths but to no correct response being given. This Blackbox style of experimentation is not getting any further in obtaining the flag. We could go deeper with `gdb
` and `ltrace
`:
gdb Output: |
jamie@VM-Linux-Ubuntu:~/Documents$ gdb casino |
ltrace Output: |
jamie@VM-Linux-Ubuntu:~$ ltrace ./casino |
All we gained from this is the application uses the functions `puts
`, `printf
`, `scanf
`, `srand
`, `rand
`, and `exit
`.
We need to figure out the game condition of what is considered a correct or incorrect response.
Decompilation & Reversing
The application is processed within Ghidra 11.3.2 using the standard analysers.
Through Ghidra, we navigate to the main entry point of the application. The tool has done a good job at decompiling the raw byte code to its C equivalent, and walking through the code we can rename some of the variables to give more meaning:
int main(void) { int int_ValidationCheck; char char_UserInput; uint index; puts("[ ** WELCOME TO ROBO CASINO **]"); puts( " , ,\n" + " (\\____/)\n" + " (_oo_)\n" + " (O)\n" + " __||__ \\)\n" + " []/______\\[] /\n" + " / \\______/ \\/\n" + " / /__\\\n" + "(\\ /____\\\n" + "---------------------"); puts("[*** PLEASE PLACE YOUR BETS ***]"); index = 0; while(true) { // `0x1c` = 28 if (0x1c < index) { puts("[ ** HOUSE BALANCE $0 - PLEASE COME BACK LATER ** ]"); return 0; } printf("> "); int_ValidationCheck = __isoc99_scanf(&DAT_001020fc,&char_UserInput); if (int_ValidationCheck != 1) { break; } srand((int)char_UserInput); int_ValidationCheck = rand(); if (int_ValidationCheck != *(int *)(check + (long)(int)index * 4)) { puts("[ * INCORRECT * ]"); puts("[ *** ACTIVATING SECURITY SYSTEM - PLEASE VACATE *** ]"); /* WARNING: Subroutine does not return */ exit(-2); } puts("[ * CORRECT *]"); index = index + 1; } /* WARNING: Subroutine does not return */ exit(-1); }
Reviewing the decompiled code we can see the following actions are taking place:
Line Number(s) | Action | Source Code |
3-5 | Variable setup: int_ValidationCheck char_UserInput index | int, char, and uint declaration. |
6-18 | A message banner is displayed, and the user is requested to enter their bet. | Collection of puts commands with some of the strings we saw earlier. |
19 | Initialise an index to 0. | index = 0; |
20 | Enter an infinite loop. | while(true) |
23 | Conditional game end. Check if the index is more than 28, and if so, exit the application, otherwise continue. | if (0x1c < index) |
28-33 | Wait for input from the user and store the first character in char_UserInput. There is a validation check for the input string to safely exit out of the application – this is ignored for the CTF purposes, but is considered good practice. | int_ValidationCheck = __isoc99_scanf(&DAT_001020fc,&char_UserInput); |
34 | The application then seeds the standard library’s random number generator with the user’s input character (cast to an integer). | srand((int)char_UserInput); |
35 | A random number is then generated, and stored in a variable. | int_ValidationCheck = rand(); |
36-42 | Game winning condition check is made against the random number and a calculation. An incorrect guess instantly informs the user and ends the application. A correct guess matches the random number with values stored in the .data segment of the binary, given the name `check`. | if (int_ValidationCheck != *(int *)(check + (long)(int)index * 4)) |
43-44 | On a correct guess, the user is informed, and the index increments by 1. | puts("[ * CORRECT *]"); index = index + 1; |
45 -> 20 | The game continues, iterating again from the infinite loop. |
This is a basic number guessing game. There’s no sign of a flag being outputted from correct guesses, therefore the characters we enter in a specific order must be the flag, with the positive feedback of a ‘CORRECT’ message.
Reversing the condition, would also mean reversing all possible seeds to the random number generator, and comparing them with the `check
` table. The alternative, is to brute force the solution, repeatedly attempt to guess the answer, and adjust the next iteration with the feedback. Given we are working on checking a single character at a time, we can limit each guess to a range of 256 options. We could infer as it’s a console application written in C, it is using the base ASCII table of 128 options. Even further, just printable characters within the ASCII range, resulting in 94 characters to test:
ASCII Sub-Set | Characters |
Digits: | 0123456789 |
Uppercase Letters: | ABCDEFGHIJKLMNOPQRSTUVWXYZ |
Lowercase Letters: | abcdefghijklmnopqrstuvwxyz |
Punctuation and Symbols: | !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ |
Solution
The brute force script is rather simple, run the application, provide an attempt, and depending on the feedback, mark the attempt as correct and reuse it for the next iteration, or try the next character.
Here it is in Python:
import subprocess printable_chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' flag = '' for i in range(29): for j in range(len(printable_chars)): attempt = f'{flag}{printable_chars[j]}' print(attempt) result = subprocess.run(['./casino'], stdout = subprocess.PIPE, text = True, input = attempt) output_lines = result.stdout.strip().split('\n') last_line = output_lines[-1] if 'VACATE' not in last_line: flag = attempt break print('Flag:', flag)
Running the script provides the following flag output:

Flag: | HTB{r4nd_1s_v3ry_pr3d1ct4bl3} |
We can now submit the flag!
