F0ndueSav0yarde

SCHEDULE I : Broken casino slot machine

Context

Schedule_1_image_title

Schedule I is an open world crime simulator video game where players build a drug trafficking operation in a fictional city : It combines elements of economic simulation, tactical strategy, and dark comedy, allowing players to grow and sell various drugs.

It is possible to make money in this game by selling drugs or playing at the casino.
By playing the slot machine several times, I realized that it was very easy to win the jackpot. So I asked myself: “But how does the slot machine work ?”.

To answer this question, I had to reverse engineer the game.

Prices of the slot machine

While playing, you can come across a lemon, a grape, a cherry, a watermelon, a bell and a seven.
Here are the prices :

SlotMachine_prices

Bonus Symbol
Initial bet multiplied by 2 3 fruits of any kind
Initial bet multiplied by 10 Same fruit 3 times
Initial bet multiplied by 25 3 bells
Initial bet multiplied by 100 3 seven

It would be VERY interesting to land on 3 seven to hit the jackpot (x100 bonus).
But what is the probability of landing on a seven ? Let’s do some reverse engineering !

Il2CppDumper and Ghidra

Schedule I was created with the Unity game engine, that uses IL2CPP.

schema_il2cpp

IL2CPP stands for “Intermediate Language to C++” : this is a scripting backend used in Unity that converts C# code into C++ code for better performance and compatibility across different platforms. It is possible to extract metadata and restore .NET DLLs from IL2CPP binaries using Il2CppDumper tool.

90TOKENWIDTHil2cppdumper

70TOKENWIDTHfilesystem

Then, reverse engineering becomes significantly easier in Ghidra : IL2CPP provides Python scripts that load the dumped metadata directly into the decompiled C++ native code.

The ghidra_with_struct.py script will apply function signatures and inject IL2CPP structures from generated script.json.

Dump.cs

I did a simple string search in dump.cs for “casino” and slot machine and I found these precious informations inside ScheduleOne.Casino::SlotMachine class :

 193903
 2	[ServerRpc(RequireOwnership = False, RunLocally = True)]
 3	// RVA: 0x86DAA0 Offset: 0x86C2A0 VA: 0x18086DAA0
 4	public void SendStartSpin(NetworkConnection spinner, int betAmount) { }
 5
 6	[ObserversRpc(RunLocally = True)]
 7	// RVA: 0x86E240 Offset: 0x86CA40 VA: 0x18086E240
 8	public void StartSpin(NetworkConnection spinner, SlotMachine.ESymbol[] symbols, int betAmount) { }
 9
10	// RVA: 0x86C200 Offset: 0x86AA00 VA: 0x18086C200
11	private SlotMachine.EOutcome EvaluateOutcome(SlotMachine.ESymbol[] outcome) { }
12
13	// RVA: 0x86C3E0 Offset: 0x86ABE0 VA: 0x18086C3E0
14	private int GetWinAmount(SlotMachine.EOutcome outcome, int betAmount) { }
15
16	// RVA: 0x86BF20 Offset: 0x86A720 VA: 0x18086BF20
17	private void DisplayOutcome(SlotMachine.EOutcome outcome, int winAmount) { }
18
19	// RVA: 0x86C2F0 Offset: 0x86AAF0 VA: 0x18086C2F0
20	public static SlotMachine.ESymbol GetRandomSymbol() { }
21
22	// RVA: 0x86C880 Offset: 0x86B080 VA: 0x18086C880
23	private bool IsFruit(SlotMachine.ESymbol symbol) { }

It seems that public static SlotMachine.ESymbol GetRandomSymbol() is what I’m looking for. I looked at decompiled code at 0x18086C2F0 virtual address in Ghidra :

100TOKENWIDTHGhidraGetRandomSymbol

These lines are interesting :

131
2    lVar5 = System.Enum$$GetValues(uVar4,0);
3    if (lVar5 != 0) {
4      uVar3 = System.Array$$get_Length(lVar5,0);
5      uVar4 = UnityEngine.Random$$RandomRangeInt(0,uVar3,0);
6      return uVar4;
7    }

The function UnityEngine.Random$$RandomRangeInt() is responsible for choosing a random value inside uVar4 enum object :

122
2uVar4 = ScheduleOne.Casino.SlotMachine.ESymbol_var;

which can be found in dump.cs :

 193734
 2// Namespace: 
 3public enum SlotMachine.ESymbol // TypeDefIndex: 2440
 4{
 5	// Fields
 6	public int value__; // 0x0
 7	public const SlotMachine.ESymbol Cherry = 0;
 8	public const SlotMachine.ESymbol Lemon = 1;
 9	public const SlotMachine.ESymbol Grape = 2;
10	public const SlotMachine.ESymbol Watermelon = 3;
11	public const SlotMachine.ESymbol Bell = 4;
12	public const SlotMachine.ESymbol Seven = 5;
13}
14
15// Namespace: 
16public enum SlotMachine.EOutcome // TypeDefIndex: 2441
17{
18	// Fields
19	public int value__; // 0x0
20	public const SlotMachine.EOutcome Jackpot = 0;
21	public const SlotMachine.EOutcome BigWin = 1;
22	public const SlotMachine.EOutcome SmallWin = 2;
23	public const SlotMachine.EOutcome MiniWin = 3;
24	public const SlotMachine.EOutcome NoWin = 4;
25}

Dynamic analysis with WinDBG

Let’s see if the return value of UnityEngine.Random$$RandomRangeInt() impacts the choice of symbol in the slot machine using WinDBG.

I attached the debugger to the PID of Schedule I.exe process and listed all modules to look for base address :

90TOKENWIDTHwindbg_listmodules

The base address of GameAssembly.dll in memory is 0x7ffcd0a80000.
The relative virtual address of ScheduleOne.Casino::SlotMachine.GetRandomSymbol() is 0x86C2F0
So its virtual address is 0x7ffcd0a80000 + 0x86C2F0 = 0x7ffcd12ec2f0 : this is where i will put my breakpoint.

windbg_debug

If my assumptions are correct, according to enum SlotMachine.ESymbol i should land on :

result

Bingo !

Calculation of probabilities

We know that UnityEngine.Random$$RandomRangeInt(0,6) function represents a uniform discrete distribution where the probability of each symbol is 1/6. Because we need 3 symbols to get an outcome, the total number of all combinations is 6*6*6 = 216.

Here are the calculations :

Here is the probability of each outcome :

Outcome Probability
3 fruits of any kind 60/216 ~ 27.78%
Same fruit 3 times 4/216 ~ 1.85%
3 bells 1/216 ~ 0.46%
3 seven 1/216 ~ 0.46%
No win 150/216 ~ 69.44%

The probability of winning is roughly 30.56 % and the probability of loosing is 69.44 %.

To get the RTP (Return To Player), we sum each contribution where the contribution of each outcome is Multiplier x Probability :

Outcome Multiplier Probability Contribution
3 fruits of any kind x2 60/216 ~ 0.5556
Same fruit 3 times x10 4/216 ~ 0.1852
3 bells x25 1/216 ~ 0.1157
3 seven x100 1/216 ~ 0.4630
No win x0 150/216 0

So RTP is 0.5556 + 0.1852 + 0.1157 + 0.4630 + 0 which is… wait … 1.3195 ???

Conclusion

Because of RTP roughly equals to 1.3195, it theoretically means that for every $1 bet you expect to get back $1.32 on average.
All the casino slot machines are surprisingly player-favorable even if the probability of winning is only 30.56 %.

You could theoretically become rich by automating the game on a slot machine via a script !

In reality, a slot machine’s RTP is always less than 1, since a higher value would mean the casino would lose money.