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