keyboard_arrow_up

title: VikeCTF 2024 - Blackjack
date: Mar 13, 2024
tags: Reverse_Writeup VikeCTF2024 Reverse


VikeCTF 2024 - Blackjack

This writeup is about an interesting reverse challenge from the VikeCTF 2024, not too difficult, but I appreciated it because I hadn't done one for a while.. 😅


Task: As I stepped into the bustling casino, the air was thick with anticipation. My eyes scanned the room until they landed on the blackjack table. Sitting across from me was the dealer, their movements precise and mechanical. With each shuffle and deal, there was something eerily robotic about them, sending a chill down my spine. Yet, I couldn't resist the allure of the cards spread out before me, beckoning me to take a chance.

nc 35.94.129.106 3002

File: blackjack


📖 Overview

Blackjack rules

I discovered blackjack during this challenge so thanks to the author for that! 😄 Let me introduce the basics needed for this challenge.

Each card has a value corresponding to its number and for the figures they all have a value of 10. The goal is to reach 21 without going over.

You are against the dealer and you have to bet money.

Once your money is bet, you get 2 cards and the dealer too.

You can see your 2 cards and 1 card from the dealer (the other one is hidden).

Now you can hit or stand:

Note that if you hit and you overcome 21, you lose directly.

Same process for the dealer.

Now, the closest person to 21 wins.

If you win, you get the double of your bet, if you lose you lose your bet.

Easy, right?

Binary discovery

When we launch the binary, we have a basic implementation of the blackjack game:

root@Ruulian:/host# ./blackjack
welcome to blackjack!
to play, place a bet, and then hit [h] or stand [s]!
if you win big, we might have a special surprise for you...

balance: 100
place your bet: 

The you can bet, hit, stand and everything like in blackjack game! Let's start to reverse.

🔄 Reverse engineering

Main function

void main(void)
{
  time_t time_var;
  FILE *__stream;
  int turns_left;
  ulong balance;
  long in_FS_OFFSET;
  char flag_buffer [1000];
  long canary;

  turns_left = 50;
  canary = *(long *)(in_FS_OFFSET + 0x28);
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  time_var = time((time_t *)0x0);
  TIME_VAR = (undefined4)time_var;
  puts("welcome to blackjack!");
  puts("to play, place a bet, and then hit [h] or stand [s]!");
  puts("if you win big, we might have a special surprise for you...");
  balance = 100;
  do {
    balance = game(balance);
    turns_left = turns_left + -1;
    if (turns_left == 0) {
      if (balance < 100000000) {
        puts("it\'s closing time, thanks for playing!");
      }
      else {
        puts("you won! congrats!");
        __stream = fopen("flag.txt","r");
        fgets(flag_buffer,1000,__stream);
        printf("i think you dropped this: %s\n",flag_buffer);
      }
      fflush(stdout);
      goto exit;
    }
  } while (balance != 0);
  puts("you have no money, time to go home!");
exit:
  if (canary == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
  __stack_chk_fail();
}

Here is the pseudocode given by ghidra (already beautified, because you deserve it..).

The code is very straightforward, but here is a quick sum up:

Game function

This function has way more stuff in it, so let me sum up the behavior.

At this point nothing really new unless the get_card function that will help us. 😈

Then, it asks us if we want to hit or to stand and well no important information here, it follows the classical behavior of blackjack rules.

When we are done with hitting and standing, the code handles the dealer part.

For this part, the only thing that we have to remember is that the dealer hits only if his score is below or equal 16.

And then it handles the win, lose and push:

get_card function

Here is the interesting part of the challenge, how to break the code.

Here is the get_card function (from ghidra decompiler):

int get_card(void) {
  uint res;

  if (INC == '\0') {
    INC = '\x01';
    TIME_VAR = TIME_VAR * 0x348882af + 0x394804e7;
    res = (uint)((ushort)((uint)TIME_VAR >> 0x10) & 0x7fff);
  }
  else {
    res = (uint)LAST_CARD;
    INC = INC + -1;
  }
  LAST_CARD = (ushort)res >> 8;
  return (res & 0xff) + ((res & 0xff) / 0x34) * -0x34;
}

If we remember well, a TIME_VAR variable was computed from the current timestamp.

As it can be seen, the cards are chosen from the current timestamp, so we can guess it! We don't even need to understand how the calculation is done here, the only thing that we have to understand is that only the first card is pseudo random, the choice of the rest of the cards is deterministic.

💣 Exploit

There are way more functions in the program, however, we don't need more than that to exploit the code. We can guess the cards so we can win the game!

First, let us code our card guesser.

Card guesser

For this part, we can code a little C program to be as close as possible to the challenge context.

Here is my card guesser (you can also find it here):

#include <time.h>
#include <stdio.h>
#include <stdlib.h>

int INC = 0;
char LAST_CARD = 0x0;
time_t TIME_VAR = 0x0;

char *SHCD = "shcd";

int get_random_card()
{
    unsigned int res;

    if (INC == '\0') {
      INC = '\x01';
      TIME_VAR = TIME_VAR * 0x348882af + 0x394804e7;
      res = (unsigned int)((unsigned short)((unsigned int)TIME_VAR >> 0x10) & 0x7fff);
    }
    else {
      res = (unsigned int)LAST_CARD;
      INC = INC + -1;
    }
    LAST_CARD = (unsigned short)res >> 8;
    return (res & 0xff) + ((res & 0xff) / 0x34) * -0x34;
}

// Returns an array of the x next cards given a seed
char *generate_cards(int x, long seed){
    TIME_VAR = seed;
    char *res = malloc(x);

    for (int i = 0; i < x; i++)
        res[i] = get_random_card();

    return res;    
}

// Function taken from ghidra decompiler
void print_card(char card)
{
    char cVar1;
    char bVar2;
    char type;

    cVar1 = SHCD[(unsigned short)(card / 0xd)];
    type = card + (char)(card / 0xd) * -0xd;
    if (type == 10) {
      putchar(L'J');
      putchar((int)cVar1);
      return;
    }
    bVar2 = type + 1;
    if (bVar2 != 12) {
      if (bVar2 != 13) {
        printf("%lu",(unsigned long)bVar2);
        putchar((int)cVar1);
        return;
      }
      putchar(L'K');
      putchar((int)cVar1);
      return;
    }
    putchar(L'Q');
    putchar((int)cVar1);
    return;
}

int main(int argc, char **argv){
    if(argc != 3 && argc != 4)
    {
        puts("Error in args");
        exit(0);
    }
    int nb = atoi(argv[1]);
    long seed = atol(argv[2]);

    char *cards = generate_cards(nb, seed);

    if(argc == 3){
        for (int i = 0; i < nb; i++)
        {
            printf("%d", cards[i]);
            if(i != nb - 1) printf(" ");
        }
        putchar('\n');
    }
    else
    {
        for (int i = 0; i < nb; i++)
        {
            print_card(cards[i]);
            if(i != nb - 1) printf(" ");
        }
        putchar('\n');
    }

}

I think that the code is clear and easy to understand, but its behavior is basically the following:

Here are the possible outputs:

root@Ruulian:/host# date +%s
1710375593
root@Ruulian:/host# ./exploit 10 1710375593
29 45 34 0 37 10 12 18 28 33
root@Ruulian:/host# ./exploit 10 1710375593 true
4c 7d 9c 1s Qc Js Ks 6h 3c 8c
root@Ruulian:/host#

If we want more cards, we can just increase the first argument, and we can change the timestamp in second argument.

Solver

It's time to solve this challenge! We have all the cards we want for a given timestamp, so now, we can use python and pwntools module to interact with the binary.

Here is the plan:

Here is my code part to handle the cards:

# Generate the card by calling our exploit
def generate_cards(x:int, seed:int, strings=False):
  added = ["true"] if strings else []

  q = process(["./exploit", str(x), str(seed)] + added)
  res = q.recvline(False).decode()
  return res.split(" ")

# Get a card value given its string representation
def get_card_value(card:str):
  v = card[0]
  if v == "J" or v == "Q" or v == "K" or (v == "1" and card[1] == "0"):
    return 10
  return int(v)

# Get a hand value
def get_hand_value(hand:list):
  res = 0
  for c in hand:
    res += get_card_value(c)
  return res

# Get our cards
CARDS = generate_cards(100000, int(time.time()), True)
NEXT = 0

def get_card(offset:int):
  return CARDS[NEXT + offset]

def get_next_card():
  global NEXT
  res = CARDS[NEXT]
  NEXT += 1
  return res

def make_choice(balance:int, debug=False):
  # Get our cards
  hand = [get_next_card(), get_next_card()]
  hand_val = get_hand_value(hand)

  # Get dealer's cards
  house_hand = [get_next_card(), get_next_card()]
  house_hand_val = get_hand_value(house_hand)

  # Compute how many times we have to hit to be close to 21
  nb_hits = 0
  while get_card_value(get_card(nb_hits)) + hand_val <= 21:
    hand_val += get_card_value(get_card(nb_hits))
    nb_hits += 1

  # Re compute our hand  
  for _ in range(nb_hits):
    hand.append(get_next_card())

  # Compute how many times the dealer is going to hit
  house_hit_nb = 0
  while house_hand_val <= 16:
    house_hand_val += get_card_value(get_card(house_hit_nb))
    house_hit_nb += 1

  # Re compute its hand  
  for _ in range(house_hit_nb):
    house_hand.append(get_next_card())

  # Choose if we should bet or not
  if house_hand_val > 21 or hand_val > house_hand_val:
    bet = balance
  else:
    bet = 0

  # Debug feature
  if debug:
    print("Your bet:", bet)
    print("Your future hand:", hand, f"({hand_val})")
    print("Your number of hits:", nb_hits)

    print("House's future hand:", house_hand, f"({house_hand_val})")
    print("House's number of hits:", nb_hits)

  # Return how many times we have to hit and the amount we should bet
  return nb_hits, bet

I tried to comment it as much as I can.

And now we have everything, we just have to loop over the game and call make_choice each time.

You can find the final solve.py here.

🚩 Getting the flag

Now we just have to run solve.py and the flag appears!

flag