The casino thinks they've rigged their slots so well, they can give every player $100 free play! Of course, there's always some small print: you can't cash out unless you hit the jackpot and each player only gets 3 minutes playtime
This wasn't a challenge I intended to make, it's the result of an unintended solution in the Rigged Slot Machine pwn challenge (patched in part 2) so I figured I'd keep it in as a warmup reversing/programming exercise 🤷♂️
Solution
checksec indicates all binary protections are enabled so we can assume it's not a pwn challenge (warmup lol). Perhaps start by testing the basic functionality of the app.
./rigged_slot1WelcometotheRiggedSlotMachine!Youstartwith $100.Canyoubeattheodds?Enteryourbetamount (up to $100 perspin): 10Youlost $10.CurrentBalance: $90Enteryourbetamount (up to $100 perspin): 10Youwon $40!CurrentBalance: $130Enteryourbetamount (up to $100 perspin): 10Youlost $10.CurrentBalance: $120Enteryourbetamount (up to $100 perspin): 10Youlost $10.CurrentBalance: $110Enteryourbetamount (up to $100 perspin): 10Youwon $10!CurrentBalance: $120Enteryourbetamount (up to $100 perspin): 100Youwon $400!CurrentBalance: $520Enteryourbetamount (up to $100 perspin): 100Youlost $100.CurrentBalance: $420
Yes, I made this shortly after returning home from Vegas 😂
Since players only receive the binary (no sauce), let's check the winning condition in ghidra.
Static Analysis
I renamed variables already to make it clearer, here's the main function.
setup_alarm(180);balance =100;puts("Welcome to the Rigged Slot Machine!");puts("You start with $100. Can you beat the odds?");do {while( true ) {while( true ) { bet =0;printf("\nEnter your bet amount (up to $%d per spin): ",100); local_10 =__isoc99_scanf(&%d,&bet);if (local_10 ==1) break;puts("Invalid input! Please enter a numeric value.");clear_input(); }if ((bet <1) || (100< bet)) break;if ((int)balance < bet) {printf("You cannot bet more than your current balance of $%d!\n",(ulong)balance); }else {play(bet,&balance);if (133742< (int)balance) {payout(&balance); } } }printf("Invalid bet amount! Please bet an amount between $1 and $%d.\n",100);} while( true );
if (*balance <133743) {puts("You can\'t withdraw money until you win the jackpot!");exit(-1);}file =fopen("flag.txt","r");if (file == (FILE *)0x0) {puts("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server." );exit(0);}fgets(flag,0x40,file);printf("Congratulations! You\'ve won the jackpot! Here is your flag: %s\n",flag);fclose(file);
So, what's the problem? Look at those odds! 1/100 spins will get a 100x multiplier 👀
If I run the program 100 times with a starting bet of $100, the chances are one of those spins will win me $10,000 🤑 That's enough capital to continue betting MAX BET (of course we'll get some 2-5x along the way too).
Solve.py
Here's my solve script. It starts with a $25 bet and then switches to $100 once the balance exceeds $10,000. You can play around with these values and see what works best, e.g. $10 starting bet, then switch to $100 when you reach $5000.
import sysfrom pwn import*# Allows switching between local/GDB/remote executiondefstart(argv=[],*a,**kw):if args.GDB:return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)elif args.REMOTE:returnremote(sys.argv[1], sys.argv[2], *a, **kw)else:returnprocess([exe] + argv, *a, **kw)# Binary filenameexe ='./rigged_slot1'elf = context.binary =ELF(exe, checksec=False)context.log_level ='info'# Jackpot thresholdwinning_balance =133742got_flag =Falsewhilenot got_flag:# Start the program io =start(level='warn') balance =100 amount =b'25'# Skip the initial output io.recvlines(3) io.sendline(amount) count =1while balance >0andnot got_flag:try:# Adjust bet amountif balance >10000: amount =b'100' io.sendlineafter(b':', amount) io.recvuntil(b'Current Balance: ') balance =int(io.recvline().decode()[1:].strip())info(f'balance: {balance}') count +=1# Check if we've hit the jackpot (remote)if balance >= winning_balance:warn("Jackpot threshold reached. Attempting to retrieve flag.") flag_output = io.recvline().decode()info(flag_output) got_flag =TrueexceptEOFError:# Capture flag output in case of EOF (local) remaining_output = io.recvall(timeout=5).decode()if'Congratulations'in remaining_output:warn("Flag captured via EOF handling:")info(remaining_output) got_flag =TruebreakexceptExceptionas e:warn(f'Error after {count} bets: {str(e)}')breakwarn(f'Total bets placed: {count}') io.close()
Interestingly, solving the challenge locally is slightly different to remote. Notice that for the remote exploit, we get the flag by checking the winning balance whereas the local flag prints with an EOF error. I'm not sure if this is related to pwntools or something else, but maybe it will cause some confusion in the CTF (especially for a warmup challenge) 😬
Performance
Anyway, testing this locally is fast. Obviously it varies for each run due to the random element but with 3 attempts I got an average of 4 seconds*, with the fastest being 1s.
[*] balance: 126825[!] Flag captured via EOF handling:[*] You won $9900!CurrentBalance: $136725Congratulations!You've won the jackpot! Here is your flag: fake_flag[!] Total bets placed: 1294real 0m1.026suser 0m0.580ssys 0m0.472s
*Note that this is the time for all runs, not the winning run. For example, when you run the script it might lose 100 games in a row before it wins. Some of those games may take a few seconds, but could be longer. In other words, if time shows over 180 seconds and you still won, it's because the final game didn't exceed the limit.
Solving remotely certainly takes longer, but it's possible to solve well within the allocated 180 second time limit. In 3 attempts I got an average of 62 seconds, with the fastest being 34s.
[*] balance: 136800[!] Jackpot threshold reached. Attempting to retrieve flag.[*] Congratulations!You've won the jackpot! Here is your flag: INTIGRITI{ju57_l1k3_7h47_y0u_4r3_4_m1ll10n41r3!}[!] Total bets placed: 1446real 0m33.884suser 0m0.866ssys 0m0.678s
Note: I have to apologise to some players here. After the event I helped debug a players script and it turns out I didn't leave enough time for those really far from the server (India, Australia etc) with poor connections. I should of tested my solution with a VPN connected in various countries before the event, it didn't occur to me but I know for next time! 💜
Someone found a nice unintended that works on Rigged slot 1. Using careful timing, you could predict the "random" seed and therefore the outcome of each bet 🧠 Check out the writeup 😎