Writeup
- Author: shieda
- Team: xSTF
CTF Info
- CTF: 1337UP LIVE CTF (2024)
- Challenge: Rigged Slot Machine 1
- Category: Warmup
- Author: CryptoCat
- Points: 50
- Solves: 132
Disclaimer
In this writeup I present an unintended solution for the challenge.
Rigged Slot Machine 1 is a warmup challenge from the 1337UP LIVE CTF 2024 organized by Intigriti.
In this challenge we are presented with a program rigged_slot1
that allow us to play on a slot machine by betting our budget of 100$ until we manage to hit the jackpot value. However, as you can see by the image below, the slot machine is clearly rigged.
So far we can assume that, in order to get the flag, we would need to beat the slot machine and reach the jackpot value.
I used Ghidra, a widely known reverse engineering tool, to understand the flow of the program.
First, I looked into the main
function, the entry point for the given program.
- A 3 minute alarm is setup, indicating the time limit of our playing session;
- The player input is validated to guarantee the bet amount provided is a valid numeric value;
- The bet amount is checked. The player can only bet as much as money as he has and this value cannot exceed 100$;
- The
play
function is called with our bet amount and our current balance amount as arguments; - If the current balance exceeds the jackpot value (133742$), then the
payout
function is called.
Now looking into the play
function:
- A random number is generated with
rand()
. Then, the usage of modulus 100 limits the range of possible numbers to [0..99]; - A multiplier value is defined based on the generated number;
- The amount of money the player wins or loses is calculated based on the bet amount and the multiplier;
- The new player’s balance is calculated.
Here is a table with the multipliers derived from the generated number:
Range | Multiplier |
---|---|
[30..99] | 0 |
[20..29] | 1 |
[15..19] | 2 |
[10..14] | 3 |
[1..9] | 5 |
0 | 100 |
Finally, the payout
function:
- Returns the flag, as expected.
I decided to rename the variables in Ghidra after understanding its functionality.
Renamed main
:
Renamed play
:
Intended Solution
By looking at the odds we can conclude that if we run the program about 100 times, chances are we will eventually hit a multiplier of 100x, and then we can continue betting the maximum amount until we reach the jackpot. 😴
But I’m not the type to play with odds that are against me! 😎
My Solution
What if we could predict each multiplier generated by the program on each bet? 🤔
That’s exactly what this solution is about.
If we look closely into the code we can see that in the beggining of main
the srand
function from the C standard library is being called.
srand
Description: Sets the seed for the random number generator used by rand
.
The seed is an unsigned integer used to initialize the random number generator. Typically, a unique value like the current time is used, and this program is no exception.
We also know that the rand
function is called to calculate the multiplier value before each bet.
The seed initializes the PRNG’s internal state, and the sequence of numbers it produces is entirely determined by this starting state. Since the algorithm always follows the same steps, providing the same seed will always result in the same sequence of numbers.
The image below demonstrates this phenomena for a certain seed.
What if we could replicate the seed used by the program when we connect to the server, making us able to deterministically predict the sequence of values generated by the rand
function?
In fact, if we predict the seed correctly, all we need to do is replicate the use of srand
locally and then repeat the sequence of calls to rand
in order to determine the sequence of values generated. From this sequence of values we can derive the multiplier that the server will respond with for every single bet.
The program uses time(0) as its seed. time(0) returns the current time as seconds since the Unix epoch. Turns out “seconds” is not precise enough for us to miss the prediction of the correct seed when we connect to the server. If the server were to use milliseconds as the seed, the delay and difference in time measured in the server versus locally would be enough to stop this attack.
Technically, three conditions have to be met for this to work:
- the random libraries in the two applications use the same implementation (that means I must locally use the C standard library to calculate the values);
- the clocks of the two applications are synchronized (local vs remote);
- the sequence of calls to
rand
is the same.
To make sure the response from the server was consistent with our predictions a sleep of 0.2 seconds was added. However, depending on the delay from the server and the internet quality a lower sleep value might be used to increase the speed of the script.
I added colors 🌈
- BLUE represents the local calculations
- GREEN is our bet
- ORANGE is the server’s repsonse
As you can see, our multiplier predictions match exactly with the server’s response of how much money we won. This way, for each bet I knew the multiplier would be 0x I would bet 1$ and for any other multiplier (1x, 2x, 3x, 5x and 100x) I would bet the maximum value, until we reach the flag.
The script runtime can vary a lot since we are still dependent on the seed to generate a good sequence of multipliers, despite us being able to predict it.
Flag: INTIGRITI{ju57_l1k3_7h47_y0u_4r3_4_m1ll10n41r3!}
Rigged Slot 2
The same exploit can be adapted to work on Rigged Slot 2. In fact, we can deterministically predict the multipliers on that program aswell.
However, there are a few key differences:
- modulus 1000 is used, resulting in a much higher range of outcomes;
- the max multiplier is 10;
- the jackpot value is 10 times higher
This makes it infeasible to hit the jackpot in 3 minutes. But it would eventually reach it, I guess.