mirror of https://github.com/dirtbags/moth.git
Finished, for the most part, the badmath flag.
This commit is contained in:
parent
cb7ce0d84d
commit
3892cd0a7d
|
@ -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
|
|
@ -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()
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
||||||
|
import Pi, irc
|
||||||
|
|
||||||
|
pi = Pi.pi(('irc.lanl.gov', 6667), '')
|
||||||
|
irc.run_forever()
|
Loading…
Reference in New Issue