Finished, for the most part, the badmath flag.

This commit is contained in:
Paul S. Ferrell 2009-10-05 14:33:30 -06:00
parent cb7ce0d84d
commit 3892cd0a7d
6 changed files with 472 additions and 0 deletions

31
badmath/Flagger.py Normal file
View File

@ -0,0 +1,31 @@
import asynchat
import asyncore
import socket
class Flagger(asynchat.async_chat):
"""Connection to flagd"""
def __init__(self, addr, auth):
asynchat.async_chat.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect((addr, 6668))
self.push(auth + b'\n')
self.flag = None
def handle_read(self):
msg = self.recv(4096)
raise ValueError("Flagger died: %r" % msg)
def handle_error(self):
# If we lose the connection to flagd, nobody can score any
# points. Terminate everything.
asyncore.close_all()
asynchat.async_chat.handle_error(self)
def set_flag(self, team):
if team:
eteam = team.encode('utf-8')
else:
eteam = b''
self.push(eteam + b'\n')
self.flag = team

255
badmath/Gyopi.py Normal file
View File

@ -0,0 +1,255 @@
import irc
import badmath
import time
import os
import traceback
import pickle
from hashlib import sha256
import Flagger
class Gyopi(irc.Bot):
STATE_FN = 'pi.state'
SALT = b'this is questionable.'
FLAG_DEFAULT = 'dirtbags'
MAX_ATTEMPT_RATE = 3
NOBODY = '\002[nobody]\002'
FLAG_HOST = b'ctf1.lanl.gov'
# FLAG_HOST = b'localhost'
def __init__(self, host, dataPath, flagger):
irc.Bot.__init__(self, host, ['gyupi'], 'Gyupi', ['#badmath'])
self._dataPath = dataPath
self._flag = flagger
try:
self._loadState()
except:
traceback.print_exc()
self._lvl = 0
self._flag.set_flag( self.FLAG_DEFAULT )
self._lastAttempt = {}
self._affiliations = {}
self._newPuzzle()
def err(self, exception):
"""Save the traceback for later inspection"""
irc.Bot.err(self, exception)
t,v,tb = exception
info = []
while 1:
info.append('%s:%d(%s)' %
(os.path.basename(tb.tb_frame.f_code.co_filename),
tb.tb_lineno,
tb.tb_frame.f_code.co_name))
tb = tb.tb_next
if not tb:
break
del tb # just to be safe
infostr = '[' + '] ['.join(info) + ']'
self.last_tb = '%s %s %s' % (t, v, infostr)
print(self.last_tb)
def cmd_join(self, sender, forum, addl):
"""On join, announce who has the flag."""
if sender.name() in self.nicks:
self._tellFlag(forum)
self._tellPuzzle(forum)
def _newPuzzle(self):
"""Create a new puzzle."""
self._key, self._puzzle, self._banned = badmath.mkPuzzle(self._lvl)
def _loadState(self):
"""Load the last state from the stateFile."""
statePath = os.path.join(self._dataPath, self.STATE_FN)
stateFile = open( statePath, 'br' )
state = pickle.load(stateFile)
self._lvl = state['lvl']
self._flag.set_flag( state['flag'] )
self._lastAttempt = state['lastAttempt']
self._affiliations = state['affiliations']
self._puzzle = state['puzzle']
self._key = state['key']
self._banned = state['banned']
self._tokens = state.get('tokens', [])
def _saveState(self):
"""Write the current state to file."""
state = {'lvl': self._lvl,
'flag': self._flag.flag,
'lastAttempt': self._lastAttempt,
'affiliations': self._affiliations,
'puzzle': self._puzzle,
'key': self._key,
'banned': self._banned,
'tokens': self._tokens}
# Do the write as an atomic move operation
statePath = os.path.join(self._dataPath, self.STATE_FN)
stateFile = open(statePath + '.tmp', 'wb')
pickle.dump(state, stateFile)
stateFile.close()
os.move( statePath + '.tmp', statePath)
def _tellFlag(self, forum):
"""Announce who owns the flag."""
forum.msg('%s has the flag.' % (self._flag.flag))
forum.msg('Difficulty level is %d' % self._lvl)
def _tellPuzzle(self, forum):
"""Announce the current puzzle."""
forum.msg('The problem is: %s' % ' '.join( map(str, self._puzzle)))
def _getStations(self):
stations = {}
with open(os.path.join(STORAGE, 'stations.txt')) as file:
lines = file.readlines()
for line in lines:
try:
name, file = line.split(':')
except:
continue
stations[name] = file
return stations
def _giveToken(self, user, forum):
"""Hand a Jukebox token to the user."""
token = self._jukebox.mkToken(user)
forum.msg('You get a jukebox token: %s' % token)
forum.msg('Use this with the !set command to change the music.')
forum.msg('This token is specific to your user name, and is only '
'useable once.')
def _useToken(self, user, forum, token, station):
"""Use the given token, and change the current station to station."""
try:
station = int(station)
stations = self._getStations()
assert station in stations
except:
forum.msg('%s: Invalid Station (%s)' % station)
return
if token in self._tokens[user]:
self._tokens[user].remove(token)
def cmd_privmsg(self, sender, forum, addl):
text = addl[0]
who = sender.name()
if text.startswith('!'):
parts = text[1:].lower().split(' ', 1)
cmd = parts[0]
if len(parts) > 1:
args = parts[1]
else:
args = None
if cmd.startswith('r'):
# Register
if args:
self._affiliations[who] = args
team = self._affiliations.get(who, self.NOBODY)
forum.msg('%s is playing for %s' % (who, team))
elif cmd.startswith('w'):
forum.msg('Teams:')
for player in self._affiliations:
forum.msg('%s: %s' % (player, self._affiliations[player]))
elif cmd.startswith('embrace'):
# Embrace
forum.ctcp('ACTION', 'is devoid of emotion.')
elif cmd.startswith('f'):
# Flag
self._tellFlag(forum)
elif cmd.startswith('h'):
# Help
forum.msg('Goal: Help me with my math homework, FROM ANOTHER DIMENSION!')
forum.msg('Goal: The current winner gets to control the contest music.')
forum.msg('Commands: !help, !flag, !register [TEAM], !solve SOLUTION,!? EQUATION, !ops, !problem', '!who')
elif cmd.startswith('prob'):
self._tellPuzzle(forum)
elif cmd.startswith('solve') and args:
# Solve
team = self._affiliations.get(who)
lastAttempt = time.time() - self._lastAttempt.get(team, 0)
answer = badmath.solve(self._key, self._puzzle)
try:
attempt = int(''.join(args).strip())
except:
forum.msg("%s: Answers are always integers.")
if not team:
forum.msg('%s: register first (!register TEAM).' % who)
elif self._flag.flag == team:
forum.msg('%s: Greedy, greedy.' % who)
elif lastAttempt < self.MAX_ATTEMPT_RATE:
forum.msg('%s: Wait at least %d seconds between attempts' %
(team, self.MAX_ATTEMPT_RATE))
elif answer == attempt:
self._flag.set_flag( team )
self._lvl = self._lvl + 1
self._tellFlag(forum)
self._newPuzzle()
self._tellPuzzle(forum)
# self._giveToken(who, sender)
self._saveState()
else:
forum.msg('%s: %s != %s' % (who, attempt, answer))
forum.msg('%s: That is not correct.' % who)
# Test a simple one op command.
elif cmd.startswith('?'):
try:
tokens = badmath.parse(''.join(args))
except (ValueError) as msg:
forum.msg('%s: %s' % (who, msg))
return
if len(tokens) > 3:
forum.msg('%s: You can only test one op at a time.' % who)
for num in self._banned:
if num in tokens:
forum.msg('%s: You can\'t test numbers in the '
'puzzle.' % who)
return
try:
result = badmath.solve(self._key, tokens)
forum.msg('%s: %s -> %d' % (who, ''.join(args), result))
except:
forum.msg("%s: That doesn't work at all." % who)
elif cmd == 'birdzerk':
self._saveState()
elif cmd == 'traceback':
forum.msg(self.last_tb or 'No traceback')
if __name__ == '__main__':
import optparse
p = optparse.OptionParser()
p.add_option('-h', '--host', dest='ircHost', default='localhost',
'IRC Host to connect to.')
p.add_option('-f', '--flagd', dest='flagd', default='localhost',
'Flag Server to connect to')
p.add_option('-p', '--password', dest='password',
default='badmath:::a41c6753210c0bdafd84b3b62d7d1666',
help='Flag server password')
p.add_option('-d', '--path', dest='path', default='/var/lib/badmath',
'Path to where we can store state info.')
opts, args = p.parse_args()
flagger = Flagger.Flagger(opts.flagd, opts.password.encode('utf-8'))
gyopi = Gyopi((opts.ircHost, 6667), opts.path, flagger)
irc.run_forever()

56
badmath/Jukebox.py Normal file
View File

@ -0,0 +1,56 @@
import subprocess
import os
class Jukebox:
SALT = 'this is unreasonable.'
def __init__(self, dataDir, tokens):
self._dataDir = dataDir
self.tokens = tokens
self.station = None
self._player = None
def getStations(self):
stations = {}
with open(os.path.join(STORAGE, 'stations.txt')) as file:
lines = file.readlines()
for line in lines:
try:
name, file = line.split(':')
except:
continue
stations[name] = file
return stations
def play(self, user, token, station):
"""Switch to the given station, assuming it and the token are valid.
raises a ValueError when either the station or token is unknown."""
station = int(station)
stations = self.getStations()
if station not in stations:
raise ValueError('Invalid Station (%s)' % station)
if token not in self.tokens:
raise ValueError('Invalid Token (%s)' % token)
self.tokens.remove(token)
self._changeStation( stations[station] )
def mkToken(self, user):
"""Generate a token for the given user. The token is a randomly
generate bit of text."""
hash = sha256(self.SALT)
hash.update(bytes(user, 'utf-8'))
hash.update(bytes(str(time.time()), 'utf-8'))
token = has.hex_digest()[:10]
self.tokens.append(token)
return token
def _changeStation(self, file):

118
badmath/badmath.py Normal file
View File

@ -0,0 +1,118 @@
import random
import math
OPS = [lambda a, b: a + b,
lambda a, b: a - b,
lambda a, b: a * b,
lambda a, b: a // b,
lambda a, b: a % b,
lambda a, b: a ^ b,
lambda a, b: a | b,
lambda a, b: a & b,
lambda a, b: max(a,b),
lambda a, b: min(a,b),
lambda a, b: a+b//2,
lambda a, b: ~b,
lambda a, b: a + b + 3,
lambda a, b: max(a,b)//2,
lambda a, b: min(a,b)*3,
lambda a, b: a % 2,
lambda a, b: math.degrees(b + a),
lambda a, b: ~(a & b),
lambda a, b: ~(a ^ b),
lambda a, b: a + b - a%b,
lambda a, b: math.factorial(a)//math.factorial(a-b) if a > b else 0,
lambda a, b: (b%a) * (a%b),
lambda a, b: math.factorial(a)%b,
lambda a, b: int(math.sin(a)*b),
lambda a, b: b + a%2,
lambda a, b: a - 1 + b%3,
lambda a, b: a & 0xaaaa,
lambda a, b: 5 if a == b else 6,
lambda a, b: b % 17,
lambda a, b: int( cos( math.radians(b) ) * a )]
SYMBOLS = '.,<>?/!@#$%^&*()_+="~|;:'
MAX = 100
PLAYER_DIR = ''
def mkPuzzle(lvl):
"""Make a puzzle. The puzzle is a simple integer math equation. The trick
is that the math operators don't do what you might expect, and what they do
is randomized each time (from a set list of functions). The equation is
evaluated left to right, with no other order of operations.
The level determins both the length of the puzzle, and what functions are
enabled. The number of operators is half the level+2, and the number of
functions enabled is equal to the level.
returns the key, puzzle, and the set of numbers used.
"""
ops = OPS[:lvl + 1]
length = (lvl + 2)//2
key = {}
bannedNums = set()
puzzle = []
for i in range(length):
num = random.randint(1,MAX)
bannedNums.add(num)
puzzle.append( num )
symbol = random.choice(SYMBOLS)
if symbol not in key:
key[symbol] = random.randint(0, len(ops) - 1)
puzzle.append( symbol )
num = random.randint(1,MAX)
bannedNums.add(num)
puzzle.append( num )
return key, puzzle, bannedNums
def parse(puzzle):
"""Parse a puzzle string. If the string contains symbols not in
SYMBOLS, a ValueError is raised."""
parts = [puzzle]
for symbol in SYMBOLS:
newParts = []
for part in parts:
if symbol in part:
terms = part.split(symbol)
newParts.append( terms.pop(0))
while terms:
newParts.append(symbol)
newParts.append( terms.pop(0) )
else:
newParts.append(part)
parts = newParts
finalParts = []
for part in parts:
part = part.strip()
if part in SYMBOLS:
finalParts.append( part )
else:
try:
finalParts.append( int(part) )
except:
raise ValueError("Invalid symbol: %s" % part)
return finalParts
def solve(key, puzzle):
puzzle = list(puzzle)
stack = puzzle.pop(0)
while puzzle:
symbol = puzzle.pop(0)
nextVal = puzzle.pop(0)
op = OPS[key[symbol]]
stack = op(stack, nextVal)
return stack

8
badmath/run Executable file
View File

@ -0,0 +1,8 @@
#! /bin/sh
[ -f /var/lib/ctf/disabled/badmath ] && exit 0
DATA_PATH=/var/lib/badmath
mkdir -p $DATA_PATH
exec envuidgid ctf python3.0 usr/lib/ctf/badmath/Gyopi.py --data=$DATA_PATH

4
badmath/test.py Normal file
View File

@ -0,0 +1,4 @@
import Pi, irc
pi = Pi.pi(('irc.lanl.gov', 6667), '')
irc.run_forever()